FP language features in Kotlin?

Hello,

I’ve used Scala in the last years and OCaml before that. I have seen that Kotlin offers some FP constructs and I’d like to know how it compares to Scala and other functional languages. In particular:

  • Algebraic Data Types and Pattern Matching: Can you use Kotlin’s data class to create ADTs, as in Scala with case classes? And can you pattern match them with “when”?

  • Option monad: It it appears that Kotlin’s T? type is similar to Option[T] in Scala. Are there corresponding map and flatMap methods?

  • Monads and Functors: Can you create your own, e.g. an Either[A,B] monad with corresponding map and flatMap methods?

  • Are there streams and lazy evaluation?

No, data classes share a many features with Scala’s case classes but you cannot pattern match them.

No, instead, the language has features to deal with nulls (like the null-safe operator and elvis operator) and allows creation of extension functions to operate on nullable types/generics.

Somewhat, it is a rather popular pattern for those trying to avoid exceptions. A sealed class would be fitting for this.

sealed class Either<out A, out B> {
    class Left<A>(val value: A): Either<A, Nothing>()
    class Right<B>(val value: B): Either<Nothing, B>()
}

fun main() {
    val either: Either<String, Error> = Either.Left("hello world")
    
    when(either){
        is Either.Left -> println(either.value)
        is Either.Right -> either.value.printStackTrace()
    }
}

Yes. Kotlin’s Sequence and kotlinx.coroutines’ Flow are examples of streams. Kotlin’s lazy delegate is an example of, well, lazy evaluation. Note that latter isn’t a direct feature of the language, but a result of composing other features.

Thanks, at least Either looks rather familiar. So I can add my own flatMap like this?

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) => …
?

You may want to check out ArrowKt. It’s adds a lot of functional programming facilities to Kotlin.

Of course you can already do plenty of functional programming in Kotlin using higher order functions, first class function types, the many built-in (stdlib) FP methods.

3 Likes

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!

Check Optional vs Nullable for a comparison. It shows how Kotlin’s built-in operators / functions can be used instead of map / flatMap.

No, instead, the language has features to deal with nulls (like the null-safe operator and elvis operator).

There indeed are stdlib functions that can be used. Like for example “let” instead of optional.map/flatMap or “takeIf” instead of optional.filter. This is described in above link also.

1 Like

Kotlin’s pattern matching capabilities are a far cry of Scala’s. Though in most use cases they are very sufficient.

In Kotlin you would rely on smart casts, which indeed would mitigate Kotlin’s shortcomings on pattern matching in many cases.

Your example would look like this in Kotlin:

when (o) {
  is Node -> doSomething(o.left, o.right)
}

Of course this will not work as well for deeply nested pattern matching use cases. For example the following Scala pattern cannot be nicely expressed in Kotlin:

val simplified = arithmeticExpression match {
   case Addition(Addition(n1, n2), n3) => Addition(n1 + n2, n3)
   case o => o
}

In such cases you would need nested when expressions in Kotlin. If the nesting becomes too complex, I would usually restructure my code in ways I would not do in Scala.

3 Likes

Thank you for this comprehensive answer!

Meanwhile, I’ve seen that there was a proposal for pattern matchin in Kotlin https://github.com/Kotlin/KEEP/pull/213 but no result was reached in the discussion.

1 Like

Community-driven keeps like this one will cook for some years. When the Kotlin developers decided to start design work on some popular features lately, they closed the corresponding community keeps and created clean new ones. This does not mean that they don’t have value though.

As for pattern matching, this keep has only 1 comment from a Kotlin developer, as far as I can tell, and it is annotated as “his personal opinion” and not an official statement.

The last official statement that I remember before that is from the ending panel video of Kotlinconf 2019. They acknowledged that pattern matching is cool, but very complex to design. They would like to wait for this feature to be added to Java first. Java developers are working on designing this feature for Java since some years already.

1 Like

I prefer:

when (myVar) {
  Client(val name, ActiveMembership(PlatinumMember(var id, val membershipStartDate))) -> incrementDate(id, membershipStartDate)
  val Client(name, ActiveMembership(RegularMembership(id)))                   -> updateSettings(id)
  Client(val name, Expired(val anyMembershipType))                                -> sendWarning(anyMembershipType)
  VipClinet(val name, val membershipStatus)                                       -> freeRenewal(membershipStatus)
}