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!