Software Development

Improved Pattern Matching in Kotlin

Kotlin doesn’t have true pattern matching, and that’s fine. In order to make matchable classes in Scala, there is an awful lot of overhead required to make it work, and I highly respect Kotlin’s goal of not adding much overhead anywhere. But that doesn’t mean we can’t try to make our own way to get something closer to pattern matching.

Using when

Kotlin’s when block is incredibly handy; It has several ways that it can work. The first way is simple equality check:

when (x) {
    1 -> print("x == 1")
    2 -> print("x == 2")
    else -> print("x is neither 1 nor 2")
}

And cases can be combined using a comma:

when (x) {
    0, 1 -> print("x == 0 or x == 1")
    else -> print("otherwise")
}

It can also do is and in checks:

when(x) {
    in 1..10 -> print("in range")
    is String -> print("I guess it's not even a number")
}

And with the last one, you can see that you can combine any of the previous into one when block. You also don’t need else if you’re using when as a statement instead of an expression. You also don’t need else if the expression version has all possibilities listed (as far as the compiler can tell).

You can also use when without a value on top, so that it simply works like a set of if-else if blocks:

when {
    a == b -> doSomething()
    b == c -> doSomethingElse()
    else -> doThatOtherThing()
}

With all these possibilities, do you know which version we’re going to use to build our pattern matching system? Surprisingly, it’s the simplest one with equality checks.

Now, I realize that you can do sealed classes as a sort of union type and is with when to match on those, but that’s has a limited set of use-cases. With the following system, I believe you can cover all use cases.

So How Do We Do It?

First, we realize that equality checks use equals() and that equals() is something we can override. So, we make some sort of Pattern type to use in the when block, and equals() checks if the object is Pattern and proceeds to use the Pattern to calculate “equality”.

Here’s a glimpse at how it loosely looks:

interface Pattern<in Subject> {
    fun match(subject: Subject): Boolean
}

class MySubject {
    …
    fun equals(other: Any): Boolean {
        if(other is Pattern<*>)
            return other.match(this)
        else …
    }
}

class SomePattern {
    override fun match(subject: Any): Boolean {
        …
    }
}

And it would be used as follows:

val x = MySubject()
…
when(x) {
    SomePattern() -> doSomething()
    SomeOtherPattern() -> doSomethingElse()
}
So, you probably get the idea now.

Tweaks

There’s quite a few things that can be done to alter this idea to make it more palatable in different situations.

Shortcutting

First, you can try to make the patterns a little more accessible by shortcutting them on the subject class. If a pattern is parameterized – for example, a List could have a parameterized pattern that checks for a certain length, IsLength which would need to take in a parameter for the length – you can put a shortcut function on the companion object instead of directly calling the class’ constructor. If it’s not parameterized, you can cache an instance of the pattern as a value on the companion object of the subject class.

Lambda Pattern

The Pattern interface only has one method. You know what that means? It’s a functional interface (in Java 8 terms). That means, in Kotlin, Pattern doesn’t even need to exist. Instead of equals() checking if the object is a Pattern, have it check if it’s a Function1<SubjectType, Boolean>. You can obviously still shortcut some built-in patterns, but now you can even put in some on-the-fly lambdas into your when block:

when(x) {
    {it: Subject -> it.isTheCoolest} -> doSomething()
}

This is sadly not all that useful, since type inference won’t be able to determine the type for the input parameter. You need to. At that point, you might as well use the unparameterized when block:

when {
    x.isTheCoolest -> doSomething()
}

That doesn’t mean using lambdas for the pattern is a bad thing. You can still use method references, which makes quick and simple on-the-fly patterns possible (even for properties):

when(x) {
    Subject::isTheCoolest -> doSomething()
}

That’s certainly better than the fully qualified lambda. More complex lambdas can be defined as functions or in values instead:

fun moreComplexCheck(subject: Subject): Boolean {
    …
}

val moreComplexCheck2 = {subject: Subject -> …}

when(x) {
    ::moreComplexCheck -> doSomething()
    moreComplexCheck2 -> doSomethingElse()
}

Outro

So, there you have it! Better pattern matching in Kotlin! What do you think? I realize it’s a misuse of equals(), but I think it’s worth it in some cases.

Reference: Improved Pattern Matching in Kotlin from our JCG partner Jacob Zimmerman at the Programming Ideas With Jake blog.

Jacob Zimmerman

Jacob is a certified Java programmer (level 1) and Python enthusiast. He loves to solve large problems with programming and considers himself pretty good at design.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button