35

Kotlin: fun with “in” - ProAndroidDev

 4 years ago
source link: https://proandroiddev.com/kotlin-fun-with-in-8a425704b635?gi=54392ae4bf6b
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Image for post
Image for post

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 inoperator.

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 with in
  • You just need to implement the contains() operator
  • The in keyword passes the parameter to your type’s contains operator
  • Your contains() operator can include any logic that returns a Boolean
  • 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 your when 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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK