
Kotlin: fun with “in”
I’m a big fan of determinism. It’s part of why I love Kotlin as a programming language. I like pushing problems to the compiler. I like using types for expressiveness and safety. And I really like sealed classes for representing state. One of the tools in Kotlin that allows us to achieve greater determinism is the language’s when
expression.
Most programmers working in Kotlin are pretty familiar with the when
expression. As an analog to Java’s switch
statement, the when
expression is a familiar way of branching logic with multiple conditions. Quickly, however, folks tend to realize it can do a lot more. when
can be returned, assigned, exhaustive, check types, smart cast, and much more. One of the less-discussed features, however, is the ability for when
to use the in
operator.
Let’s start by looking at a basic example using ranges:
var x = 5when (x) {
in 0..9 -> println("single digit")
in 10..99 -> println("double digit")
}
By using the in
keyword on the left side of the when
expression, Kotlin will check if x
is contained by the range.
By using ranges 0..9
and 10..99
in the code above, we’re using the convenient “lower-bound dot-dot upper-bound” Kotlin syntax in order to check if the target number is single-digit or double-digit.
This is a pretty arbitrary example but it gives us a starting point for our exploration. Alternatively, we could have defined these ranges using the IntRange
type.
when (x) {
in IntRange(0, 9) -> println("single digit")
in IntRange(10, 99) -> println("double digit")
}
In this case we haven’t really bought ourselves anything over the previous example (except maybe an unnecessary object allocation). But, since we know that a range is actually a type, our next question can be: what types work with in
? Interfaces? Concrete classes?
Operator overloading
It turns out the Kotlin in
keyword is shorthand for the operator contains
. It’s not an interface or a type, just the operator. If we wanted to make a custom type to check if a value is in
our type, all we need to do is add the operator contains()
.
Put plainly, a in b
is just shorthand for b.contains(a)
Let’s look at an example. Let’s say we’re big fans of kittens and want to find out if a sound variable we have equals “mew”. To do this, let’s create a PossibleKitten
object.
object PossibleKitten {
operator fun contains(value: CharSequence): Boolean {
return value == "mew"
}
}
Now, let’s use it in a when
expression:
val sound1 = "woof"
val sound2 = "mew"when (sound) {
in PossibleKitten -> println("possible kitten found!")
else -> println("doesn't seem like a kitten")
}
If we check sound1
we get the result “doesn’t seem like a kitten.” But if we check sound2
, we get “possible kitten found!” This works because the value of sound
is implicitly passed to our contains
function and expects a Boolean
in response.
So in PossibleKitten
is just shorthand for PossibleKitten.contains(sound)
. The when
expression is passing the sound
parameter to in
and in
is acting as an infix
operator.
As delightful as kittens are, it’s important to realize that we’ve just created a custom contains
overload. And, with that overload, we can now return anything we want. We’ve created a kind of custom predicate. As long as you return a Boolean
, you can put any logic you want in your contains
operator.
Dynamic predicate
Taking this a bit further, let’s create a type that takes an input. This allows us to check for “good doggos” too. To do this, let’s create a new class that can take a constructor parameter, and we’ll name it something more generic.
class Contains(val text: String) {
operator fun contains(value: CharSequence) = value.contains(text)
}
Now we can use it in our when
expression to look for both kittens and good doggos:
when (sound) {
in Contains("mew") -> println("possible kitten found!")
in Contains("woof") -> println("13/10 good doggo")
}
Alternatively, we could have created a custom class just to check for good doggos, just like our kitten class. Then we would have ended up with a when
expression like this:
when (sound) {
in PossibleKitten -> println("possible kitten found!")
in PossibleDoggo -> println("13/10 good doggo")
}
To me, both of these approaches are pretty good. They’re both easy to read and give us a powerful way of representing a predicate for a when
expression condition. Which of these you use is up to you and depends on your specific use case.
Let’s look at a few more examples though — you can do a few more interesting things with in
.
First, we created a custom type Contains
, but we can also invert the logic and create a class that checks if a string is contained by another. This is akin to checking if a string has a particular substring. All we need to do is swap value.contains(text)
from the previous example with text.contains(value)
class ContainedBy(val text: String) {
operator fun contains(value: CharSequence) = text.contains(value)
}
And the when
expression
when (sound) {
in ContainedBy("meow,mew") -> println("possible kitten found!")
in ContainedBy("woof,awoo") -> println("13/10 good doggo")
}
Of course, checking for “meow, mew” is a bit weird (shouldn’t it be a collection?) but it’s just for demonstration purposes. Nevertheless, it reminds us about a feature of Collections
. Because Collections
implement the operator contains
, they can also be used with an in
operator. Consequently, we could have also written the when
expression this way:
when (sound) {
in listOf("meow", "mew") -> println("possible kitten found!")
in setOf("woof", "awoo") -> println("13/10 good doggo")
}
Again, both of these approaches are perfectly valid. There are some slight differences in what a String
can match vs. a List
, but mostly this again comes down to your use case. It depends on whether you need to reuse the custom predicate, your preference for which is more readable, and how complex the logic is that you want to put in your contains
operator.
Regular Expressions
One last cool trick with contains()
is that you can use it to allow for regular expressions in your when
expression. It’s pretty handy for things like validating user input or checking URLs.
A neat way to do do this is by overriding the contains
operator on Kotlin’s built-in Regex
class using an extension function.
operator fun Regex.contains(text: CharSequence) : Boolean {
return this.containsMatchIn(text)
}
As a bonus, because we used Kotlin’s built-in Regex
class, we get some nice syntax highlighting in the pattern string.
This allows us to write a when
expression like the following:
when (sound) {
in Regex("[0–9]") -> println("contains a number")
in Regex("[a-zA-Z]") -> println("contains a letter")
}
Why “in”?
At this point, you may be wondering if we couldn’t just return a Boolean
for each case of our when
expression. Why do we need in
at all? in Regex
isn’t exactly the most fluent code. The answer is that you can, but only if you omit the parameter used by the when
expression.
For example, we could have written the examples above as something more like:
when {
sound.contains("mew") -> println("possible kitten found!")
"woof".contains(sound) -> println("13/10 good doggo")
"[0–9]".toRegex().containsMatchIn(str) -> println("number!")
funcReturnsBool() -> println("funcReturnsBool was true!")
isWednesday() -> println("Happy almost Friday!")
}
In our previous examples, the in
keyword was passing the parameter sound
from the when
expression to our custom type. In this style, you can use anything that returns a Boolean
on the left. This is pretty powerful but could be an opportunity for mistakes. You can’t actually be sure the sound
parameter is used at all here. We could have accidentally used sound2
in one case and sound
in another. In fact, the last branch — isWednesday()
— doesn’t really have anything to do with kittens or doggos at all.
I don’t mean to say this is a bad approach though. It’s just important to know that without a parameter you have no guarantee that all the conditions are operating on the same thing. Remember, our original goal was determinism. I want my code to help me do the right thing.
All in all, the in
+ contains
combo becomes a nice little trick to improve readability and can be slightly safer if you are operating on a particular parameter because it gets automatically passed to your contains
operator.
What we learned
Let’s go over what we learned in our exploration:
- You can create a custom type to use in a
when
expression within
- You just need to implement the
contains()
operator - The
in
keyword passes the parameter to your type’scontains
operator - Your
contains()
operator can include any logic that returns aBoolean
- Custom
contains
types can be dynamic and take a constructor parameter - If you’re using a parameter with a
when
expression, then you can be sure it’s passed to each branch - If you aren’t using a parameter, you can use any expression that returns a
Boolean
for conditions of yourwhen
expression
So what do you think? What would you use a custom contains
type for?
Credit to Mike Burns from Thoughtbot for showing me this trick. And, huge thanks to cketti, Parth Padgaonkar, and Adam McNeilly for reviewing this article.
If you enjoy cats and Kotlin you can find me on the twitters: https://twitter.com/alostpacket