I don’t know Scala but I know that Kotlin’s when
is pretty powerful for pattern matching. If your interest lies in FP, especially in closed polymorphism, the combination of when
and sealed class
will be of interest for you.
Kotlin might perform closed polymorphism pattern matching differently than you’re used to it. For example, a very close pendant to Kotlins sealed class
/ when
is Rusts enum
/ match
. The two concepts definitely still have some differences, but what matters here is that in Rust you will use an expression like
match enum_value {
SomeEnum::OptionA => {
println!("It was A!!");
value_for_a
},
SomeEnum::OptionB(param1, param2) => foo(param1, param2),
SomeEnum::OptionC(param) => param,
}
You see that you use an expression like Enum::Variant(parameters)
to extract the parameters and use them in the following code. Kotlins approach is fundamentally different in this point. “Pattern Matching” as such isn’t even realized as its own feature. Instead, it works like this:
To replicate the above Rust example, we cannot use enum class
in Kotlin because they are completely different to enums in Rust. Instead, what comes closest is an inheritance hierarchy:
open class SomeEnum {
// common code
}
class OptionA : SomeEnum() {
// no fields
}
class OptionB(val param1: Int, val param2: String) : SomeEnum() {
// possibly some functions
}
class OptionC(val param: Double) : SomeEnum() {
// possibly some functions
}
Note that I used the identifier SomeEnum
even though we’re not looking at an enum. I did this to amplify the similarities to the Rust sample.
Now, without the need of another feature, we can make use of the when
expression, which supports instance checks using the is
operator on the subject:
when (enumValue) {
is OptionA -> {
println("It was A!!")
valueForA
}
is OptionB -> foo(enumValue.param1, enumValue.param2)
is OptionC -> enumValue.param
else -> TODO("We will come back to this")
}
The when
expression, in this case, does not perform any pattern matching, like Rust would match the subject to the different provided enum variants. In fact, this use-case of when
is semantically the same as an if
/ else if
/ else
cascade, each clause having the condition enumValue is (whatever the type in the when expression was
. So, in short, when
(in this case) just performs instance checks until one branch returns true.
And there’s another basic Kotlin feature playing a role here: Smart Casts. In short, every time the compiler has enough information to narrow down the type of some expression, it is automatically upcasted. In this case, we performed instance checks on enumValue
. The compiler can infer that in the is OptionB
branch, the type of enumValue
can be upcasted from SomeEnum
to OptionB
. That is why we were able to access the two properties of OptionB
in the corresponding branch in a type-safe way.
What this means is that in Kotlin, you perform something that looks like pattern matching just by utilizing OOP and some basic language features.
The only thing that is left is the TODO marker I left at the else
branch. If you use when
as an expression (that means you use its result), it has to be either exhaustive or include an else
branch. You will notice that the declaration open class SomeEnum
hints at the fact that we are in the field of open polymorphism. So there’s no way of making this when
expression exhaustive. But by just changing open class
to sealed class
, we can switch to closed polymorphism and the when
expression is exhaustive (which means we can leave out the else
branch now). Why? Because all subclasses of a sealed class
have to be declared in the same file. All possible subclasses of SomeEnum
can be enumerated at compile-time, thus it can be verified that the when
expression is exhaustive.
So, to refer to your question:
What is the difference between when in Kotlin and match in Scala? If you have a binary tree, what is the equivalent of
case Node(left, right, value) => …
?
As said, I don’t know Scala but I can only assume that this expression works in a similar manner as Rusts match
. I guess so because you include a pattern in the branch declaration (Node(left, right, value)
). In Kotlin, you couldn’t write it like this. Instead, you’d declare the Node
class as a subclass of a sealed superclass. Then, you’d use when
in combination with the is
operator to land in the right branch. And in the branches’ code you’d just access the properties left
, right
, value
of Node
, while the smart cast ensures type safety. Example code:
sealed class BinaryTreeNode<T>
class Node<T>(
val left: BinaryTreeNode<T>,
val right: BinaryTreeNode<T>,
val value: T
) : BinaryTreeNode<T>
class Leaf<T>(val leafValue: T): BinaryTreeNode<T>
Usage:
val node = getTreeNode() // you will either obtain a regular node or a leaf
when (node) {
is Node -> {
// node is smart casted from BinaryTreeNode<T> to Node<T>
// access left (BinaryTreeNode<T>) via node.left
// access right (BinaryTreeNode<T>) via node.right
// access value (T) via node.value
}
is Leaf -> {
// node is upcasted from BinaryTreeNode<T> to Leaf<T>
// access leafValue (T) via node.leafValue
}
}
Hope that helps!