Kotlin - Pattern Matching Rehash

We already touched on Pattern Matching in Java, but what about Kotlin? Let's explore writing a similar library to our Java example, but in Kotlin!

Kotlin - Pattern Matching Rehash

Real quick, let's revisit our last blog post. To rehash, we looked briefly at Scala's pattern matching syntax and it's power, then we took what we learned and applied it to Java to create an extremely basic pattern matching framework. I will not be going too in-depth into the logic behind the pattern matching in this post, you can always read the original if you're interested though!

This time, in our first Kotlin post, we're going to explore implementing the same basic framework, but in Kotlin. To be very clear, this is going to be a close approximation of our existing Java implementation, except this time in Kotlin. For those who are unaware, Kotlin is a JVM language built by Jetbrains which has been taking the Android world by storm. I think it's best described as a "modern" Java. We'll take a look at some of the features in this post as part of building out our pattern matching.

To start, I'm going to post the entirety of the code so we can talk about it

fun <T, N> T.match(vararg cases: Case<T, N>):N? {
	val matchingCase = cases
			.filter { (matchFunc) -> matchFunc(this) }
			.sortedBy { c -> c.isDefault }
			.firstOrNull()
	if (matchingCase == null) {
		throw RuntimeException("No match found")
	} else {
		return matchingCase.result(this)
	}
}

data class Case<in O, out N> (val matchFunc: (O) -> Boolean, val result: (O) -> N?, val isDefault: Boolean = false)

fun <O, N> case(matchFunc: (O) -> Boolean, result:(O) -> N): Case<O, N> = Case(matchFunc, result)

fun <O, N> wildcard(result: (O) -> N): Case<O, N> = Case({_ -> true}, result, true)

Well, that doesn't look so bad now does it? For the record, our Java implementation was close to 100 lines of code. This code clocks in at a clean 17! It also reads nicely in use:

val i = Person("Devin", 29)
	val res:String? = i.match(
			case({it.name == "Devin"}, {"This matches everyone named Devin and uses age (${it.age}) in the response"}),
			wildcard({"This matches everyone and uses their name (In this case ${it.name}) in it's response"})
	)
	println(res) //Prints "This matches everyone named Devin and uses age (29) in the response"

But what's actually going on in the code? If you're not familiar with Kotlin it's likely there are a number of syntax questions you have about the code so let's dive into it and see where we end up. As before, we'll ignore the match part for now and focus on case.

data class Case<in O, out N> (val matchFunc: (O) -> Boolean, val result: (O) -> N?, val isDefault: Boolean = false)

Let's start with the Case class. The first and most obvious thing you'll notice about the class is that there isn't much there. At all. It has no body, no obviously defined getters or setters, appears to use the Javascript style default parameter, and has an odd data flag at the start. This is a very neat feature of Kotlin called a "Data Class". A data class is, at it's core, a bit syntactic sugar for creating POKOs, or "Plain Old Kotlin Objects". These classes represent a collection of data much the same way a POJO would, but take care of a lot of the nitty-gritty code for you. They automatically generate a number of methods (including equals, hashcode and toString among others), and allow for the easy definition of variables (properties actually, but that's neither here nor there for the moment). Effectively, this class could be written in Java as follows:

public final class Case<O, N> {
	private final Predicate<O> matchFunc;
	private final Function<O, N> result;
	private final Boolean isDefault;

	public Case(Predicate<O> matchFunc, Function<O, N> result, Boolean isDefault) {
		this.matchFunc = Objects.requireNonNull(matchFunc);
		this.result = Objects.requiresNonNull(result);
		this.isDefault = Objects.requireNonNull(isDefault);
	}

	public Case(Predicate<O> matchFunc, Function<O, N> result) {
		this.matchFunc = Objects.requireNonNull(matchFunc);
		this.result = Objects.requireNonNull(result);
		isDefault = false;
	}

	public Predicate<O> getMatchFunc() {
		return matchFunc;
	}

	public Runction<O, N> getResult() {
		return result;
	}

	public Boolean getDefault() {
		return isDefault;
	}
	
	public Case<O, N> copy(Predicate<O> matchFunc, Function<O, N> result, Boolean isDefault) {
		return new Case<O, N>(matchFunc, result, isDefault);
	}

	@Override
	public boolean equals(Object o) {
		if (this == o) return true;
		if (o == null || getClass() != o.getClass()) return false;

		Case<?, ?> aCase = (Case<?, ?>) o;

		if (matchFunc != null ? !matchFunc.equals(aCase.matchFunc) : aCase.matchFunc != null) return false;
		if (result != null ? !result.equals(aCase.result) : aCase.result != null) return false;
		if (isDefault != null ? !isDefault.equals(aCase.isDefault) : aCase.isDefault != null) return false;

		return true;
	}

	@Override
	public int hashCode() {
		int result1 = matchFunc != null ? matchFunc.hashCode() : 0;
		result1 = 31 * result1 + (result != null ? result.hashCode() : 0);
		result1 = 31 * result1 + (isDefault != null ? isDefault.hashCode() : 0);
		return result1;
	}

	@Override
	public String toString() {
		return "Case{" +
				"matchFunc=" + matchFunc +
				", result=" + result +
				", isDefault=" + isDefault +
				'}';
	}
}

This doesn't even include the three componentN functions that Kotlin generates, but it doesn't need them to get across the point: Kotlin Data classes save a large amount of coding.

Less obvious benefits though (which are made more obvious in the Java version if you're unfamiliar with Kotlin) is that the properties of the Case class are all final (denoted in Kotlin by the val keyword) and none of them can be null (denoted by the lack of ? following the variable type). Additionally, we can clearly see that the result function in Kotlin returns a nullable field type ((O) -> N?), but it's left ambigious in Java. The null` checking in Kotlin is compile-time, a huge benefit over Java's run-time, which let's us write very nice, clean, and safe code because we know of every expected null case up-front thus the only time there should be a bug is when null was never an expected case to begin with! I should note that obviously we're using a boolean in place of a separate class for the wildcard, and that we could use a "Sealed Class" instead, but for now let's stick with what we've got.

Great, so we've covered the Case class, but what about those two function?

fun <O, N> case(matchFunc: (O) -> Boolean, result:(O) -> N?): Case<O, N> = Case(matchFunc, result)

fun <O, N> wildcard(result: (O) -> N): Case<O, N> = Case({_ -> true}, result, true)

Again, the syntax is confusing, but with a little work you can understand it. We'll break down the first function signature though to make it clear:

fun denotes a function
<O, N> are generics, same as Java
case is the function name. You'll notice it's not a keyword in Kotlin
match: (O) -> Boolean The name of a parameter, followed by the type (in this case, a function that takes O and returns a Boolean)
: Case<O, N> is the return type of the function
= Case(matchFunc, result) The actual body of the function! Because this is a simple function that returns an object we can use a simple = instead of the more verbose bracket syntax.

Great, so between these two functions and the one class we're good to go to create cases. Let's move on to the match function:

fun <T, N> T.match(vararg cases: Case<T, N>):N? {
	val matchingCase = cases
			.filter { (matchFunc) -> matchFunc(this) }
			.sortedBy { c -> c.isDefault }
			.firstOrNull()
	if (matchingCase == null) {
		throw RuntimeException("No match found")
	} else {
		return matchingCase.result(this)
	}
}

More crazy syntax! What in the world does T.match mean? Why does it say vararg? Where is all the new keywords?! Well slow down buster, we'll cover all this stuff, starting with the new keyword. In Kotlin there is no need to actually put new infront of a variable assignment (such as throw RuntimeException("No match found"), it's implied! This is just a minor time saving bonus. The second question, vararg, is as easy as you're expecting: it's the same as Java's ... vararg operator. While we could have done cases: Array<Case<T, N>> instead, we wouldn't have been able to cleanly pass in the case arguements, instead needing to wrap them in arrayOf() first. And finally the more complex question, what even does T.match do? Let's start by examining it as a it relates to the function declaration:

fun <T, N> T.match(vararg cases: Case<T, N>):N?

Starting off, we can see that the T is a generic, so it could very well be any kind of object (we could make it more verbose by saying <T: Any, N> but the : Any is implied because everything extends Any). Next up is the actual T.match declaration, which is called an "Extension Function" and effectively adds the match function to any class type. It could be an Int, a String, or even a Person POKO! It should be noted that it doesn't actually add a method to the type, it resolves the method statically, but I'll let the Kotlin docs explain all the details. The last thing we should note is that since we're not using a separate class type for the wildcard case we're using the Array.sortedBy method instead.

What this looks like in practice might be something like this:

data class Person(val name: String, val age: Int)
fun main(args: Array<String>) {
	println(getAgeString(Person("", 0)))
	println(getAgeString(Person("", 2)))
	println(getAgeString(Person("", 10)))
	println(getAgeString(Person("", 17)))
	println(getAgeString(Person("", 29)))
}

fun getAgeString(person: Person): String {
	return person.match(
			case({it.age < 2}, {"Baby"}),
			case({it.age in 2 .. 5}, {"Toddler"}), //"it" refers to the passed in variable in a single parameter lambda.
			case({it.age in 5 .. 13}, {"Child"}), //The {} tell Kotlin that the code should be a lambda!
			case({it.age in 13 .. 18}, {"Teenager"}), //the in 13 .. 18 is the same as [13, 18)
			wildcard { "Adult" }
	)!! //This tells Kotlin that while person.match returns a String?, it shouldn't ever be null so coerce it into a String.  It will throw an exception if you're wrong.
}

And there we go, a Kotlin version of the Java pattern matching we wrote in the last post! I would be amiss not to mention that Kotlin provides a fairly verbose when function, which is a less powerful version of Pattern Matching, but they have no intention of providing full Pattern Matching support, at least at the time of writing this blog.

About the author
Photo by Ashim D’Silva on Unsplash