‘when’ expression as pattern-matching in Kotlin
Let’s consider the following case. You need to return 3 different return values from a function. Each value is associated with information. Say we do authentication and authorization in one shot. The results are:
- not authenticated + error message
- not authorized + userId + error message
- authenticated and authorized + userId
Algebraic data types looks the best fit here, but we have no such in Java.
A possible approach is to use enum for that. But enum does not allow us to pass additional information with each call. To fix that we may return a value object with all fields, but it will add a level of mess to the callee code.
Yet another apporach is to make the method return a base class or interface and to have an
implementation per return case. This would make code cleaner, but with a cost of instenceof
or visitor pattern impementation.
A nice thing in Kotlin is we are able to use
when expression
to make this checking code read better. In a recent post
I covered when
expression benefits.
We may also make sure we check all possible branchs in when
expression. For this we only need to
use sealed classes for
return objects hierarchy.
This is example implementation code with sealed class and when
expression:
class UserId { /* ... */ }
class MagicToken { /* ... */ }
sealed class AuthResult {
class NotAuthenticated(val message:String) : AuthResult()
class NotAuthorized(val id : UserId, val message: String) : AuthResult()
class Success(val id: UserId, val token : MagicToken) : AuthResult()
}
fun proceed(r : AuthResult) = when(r) {
is NotAuthenticated -> "NotAuthenticated ${r.message}"
is NotAuthorized -> "NotAuthorized ${r.id}, ${r.message}"
is Success -> "Success ${r.token}"
}
In this example we also need not specify else
case for
when expression.
Kotlin compiler is able to prove we listed all types if this sealed class.
Thanks to smart casts
in each when
branch we use exactly matched type, so for example, r.message
in the first branch
is NotAuthenticated#message
and so on.
Generated bytecode
Let’s traditionally take a look into bytecode, that was generated from this code snippet. Note. I use IntelliJ IDEA 2017.1 EAP with Kotlin 1.0.6 plugin. The generated bytecode may change with a future version of tools.
// access flags 0x19
public final static proceed(LAuthResult;)Ljava/lang/String;
@Lorg/jetbrains/annotations/NotNull;() // invisible
@Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
L0
ALOAD 0
LDC "r"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
L1
LINENUMBER 25 L1
ALOAD 0
ASTORE 1
L2
LINENUMBER 26 L2
ALOAD 1
INSTANCEOF AuthResult$NotAuthenticated
IFEQ L3
L4
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "NotAuthenticated "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 0
CHECKCAST AuthResult$NotAuthenticated
INVOKEVIRTUAL AuthResult$NotAuthenticated.getMessage ()Ljava/lang/String;
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
GOTO L5
L3
LINENUMBER 27 L3
ALOAD 1
INSTANCEOF AuthResult$NotAuthorized
IFEQ L6
L7
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "NotAuthorized "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 0
CHECKCAST AuthResult$NotAuthorized
INVOKEVIRTUAL AuthResult$NotAuthorized.getId ()LUserId;
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/Object;)Ljava/lang/StringBuilder;
LDC ", "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 0
CHECKCAST AuthResult$NotAuthorized
INVOKEVIRTUAL AuthResult$NotAuthorized.getMessage ()Ljava/lang/String;
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
GOTO L5
L6
LINENUMBER 28 L6
ALOAD 1
INSTANCEOF AuthResult$Success
IFEQ L8
L9
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "Success "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 0
CHECKCAST AuthResult$Success
INVOKEVIRTUAL AuthResult$Success.getToken ()LMagicToken;
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/Object;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
GOTO L5
L8
NEW kotlin/NoWhenBranchMatchedException
DUP
INVOKESPECIAL kotlin/NoWhenBranchMatchedException.<init> ()V
ATHROW
L10
LINENUMBER 25 L10
L5
LINENUMBER 29 L5
ARETURN
L11
LOCALVARIABLE r LAuthResult; L0 L11 0
MAXSTACK = 2
MAXLOCALS = 2
Kotlin compiler generated an if-else
chain with instanceof
checks. First it checks
if the value is AuthResult$NotAuthenticated
, next AuthResult$NotAuthorized
and finally
AuthResult$Success
. In a case something went terribly wrong, a kotlin.NoWhenBranchMatchedException
exception is thrown. And this can be achieved if older version of our snippet is executed with a
newer version of AuthResult
class. A full re-compile will fail with error so we were able to
fix the problem easily.
Conclusion
In this post, we looked how when
expression is working with sealed classes
, which are really nice
to use for cases, where one needs to return several different unrelated values.
Related work
You may also like to read a related blog post Algebraic Data Types In Kotlin
comments powered by Disqus