Sealed classes and 'when' expression

‘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