본문 바로가기

카테고리 없음

sealed class의 sugar syntax 처리

Kotlin Koans의 샘플에서 따온 코드이다.

Smart casts에서는 when 을 사용한 스마트 캐스트 예제를 보여주고

fun eval(expr: Expr): Int =
        when (expr) {
            is Num -> expr.value
            is Sum -> eval(expr.left) + eval(expr.right)
            else -> throw IllegalArgumentException("Unknown expression")
        }

interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr

Sealed classes에서는 interface를 sealed class 로 바꾸면서 when의 else 를 제거한다.

fun eval(expr: Expr): Int =
        when (expr) {
            is Num -> expr.value
            is Sum -> eval(expr.left) + eval(expr.right)
        }

sealed class Expr
class Num(val value: Int) : Expr()
class Sum(val left: Expr, val right: Expr) : Expr()

default 제거의 기억

위의 실습을 하면서 실무에서 enum 을 사용해서 상태를 구현할 때 swtch ~ case 의 default를 없애기 위해 했던 방법이 생각났다.

default 때문에 sonarqube에서 존재하지도 않는 상태 때문에 커버리지가 100%가 안되는 것이 있었기 때문에 여러가지 궁여책을 생각했기 때문이다. 통신사 때에는 예외 처리부분 테스트를 위해 사용하지 않는 LG_UPLUS를 추가해야 했고, 현금영수증에서는 람다 함수를 이용하기도 했다.

 

드로이드 나이츠 2018에 보면 우명인 님이 Kotlin Sealed Class를 이용한 뷰상태 관리라는 발표에서 단계별로 개선을 하는 과정을 볼 수 있으니 참고하면 좋을 것 같다.

 

Kotlin 코드를 JVM에서 돌아가는 코드로 구현을 한다면 결국은 default 에 해당하는 부분을 다른 stub으로 대체할 것 같은 느낌이 들었다.

그래서 아래와 같이 코드로 작성하고 디컴파일을 해보았다.

Smart casts

interface ExpressionWithInterface
class Num(val value: Int) : ExpressionWithInterface
class Sum(val left: ExpressionWithInterface, val right: ExpressionWithInterface) : ExpressionWithInterface

fun eval(expression: ExpressionWithInterface): Int =
    when (expression) {
        is Num -> expression.value
        is Sum -> eval(expression.left) + eval(expression.right)
        else -> throw IllegalArgumentException("Unknown expression")
    }

fun main() {
    println(eval(Sum(Num(10), Num(23))))
}

Sealed classes

sealed class ExpressionWithSealedClass
class Number(val value: Int) : ExpressionWithSealedClass()
class Add(val left: ExpressionWithSealedClass, val right: ExpressionWithSealedClass) : ExpressionWithSealedClass()

fun eval(expression: ExpressionWithSealedClass): Int =
    when (expression) {
        is Number -> expression.value
        is Add -> eval(expression.left) + eval(expression.right)
    }

fun main() {
    println(eval(Add(Number(10), Number(23))))
}

eval 함수의 컴파일 된 코드는 각각 아래와 같다. 

  public final static eval(Lsandbox/ExpressionWithInterface;)I
    // annotable parameter count: 1 (invisible)
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
   L0
    ALOAD 0
    LDC "expression"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
   L1
    LINENUMBER 8 L1
    ALOAD 0
    ASTORE 1
   L2
    LINENUMBER 9 L2
    ALOAD 1
    INSTANCEOF sandbox/Num
    IFEQ L3
    ALOAD 0
    CHECKCAST sandbox/Num
    INVOKEVIRTUAL sandbox/Num.getValue ()I
    GOTO L4
   L3
    LINENUMBER 10 L3
   FRAME APPEND [sandbox/ExpressionWithInterface]
    ALOAD 1
    INSTANCEOF sandbox/Sum
    IFEQ L5
    ALOAD 0
    CHECKCAST sandbox/Sum
    INVOKEVIRTUAL sandbox/Sum.getLeft ()Lsandbox/ExpressionWithInterface;
    INVOKESTATIC sandbox/ExpressionWithInterfaceKt.eval (Lsandbox/ExpressionWithInterface;)I
    ALOAD 0
    CHECKCAST sandbox/Sum
    INVOKEVIRTUAL sandbox/Sum.getRight ()Lsandbox/ExpressionWithInterface;
    INVOKESTATIC sandbox/ExpressionWithInterfaceKt.eval (Lsandbox/ExpressionWithInterface;)I
    IADD
    GOTO L4
   L5
    LINENUMBER 11 L5
   FRAME SAME
    NEW java/lang/IllegalArgumentException
    DUP
    LDC "Unknown expression"
    INVOKESPECIAL java/lang/IllegalArgumentException.<init> (Ljava/lang/String;)V
    CHECKCAST java/lang/Throwable
    ATHROW
   L4
    LINENUMBER 8 L4
    LINENUMBER 12 L4
   FRAME SAME1 I
    IRETURN
   L6
    LOCALVARIABLE expression Lsandbox/ExpressionWithInterface; L0 L6 0
    MAXSTACK = 3
    MAXLOCALS = 2

vs

  public final static eval(Lsandbox/ExpressionWithSealedClass;)I
    // annotable parameter count: 1 (invisible)
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
   L0
    ALOAD 0
    LDC "expression"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
   L1
    LINENUMBER 8 L1
    ALOAD 0
    ASTORE 1
   L2
    LINENUMBER 9 L2
    ALOAD 1
    INSTANCEOF sandbox/Number
    IFEQ L3
    ALOAD 0
    CHECKCAST sandbox/Number
    INVOKEVIRTUAL sandbox/Number.getValue ()I
    GOTO L4
   L3
    LINENUMBER 10 L3
   FRAME APPEND [sandbox/ExpressionWithSealedClass]
    ALOAD 1
    INSTANCEOF sandbox/Add
    IFEQ L5
    ALOAD 0
    CHECKCAST sandbox/Add
    INVOKEVIRTUAL sandbox/Add.getLeft ()Lsandbox/ExpressionWithSealedClass;
    INVOKESTATIC sandbox/ExpressionWithSealedClassKt.eval (Lsandbox/ExpressionWithSealedClass;)I
    ALOAD 0
    CHECKCAST sandbox/Add
    INVOKEVIRTUAL sandbox/Add.getRight ()Lsandbox/ExpressionWithSealedClass;
    INVOKESTATIC sandbox/ExpressionWithSealedClassKt.eval (Lsandbox/ExpressionWithSealedClass;)I
    IADD
    GOTO L4
   L5
   FRAME SAME
    NEW kotlin/NoWhenBranchMatchedException
    DUP
    INVOKESPECIAL kotlin/NoWhenBranchMatchedException.<init> ()V
    ATHROW
   L4
    LINENUMBER 8 L4
    LINENUMBER 11 L4
   FRAME SAME1 I
    IRETURN
   L6
    LOCALVARIABLE expression Lsandbox/ExpressionWithSealedClass; L0 L6 0
    MAXSTACK = 2
    MAXLOCALS = 2

 

보면 else의 예외 던지는 부분은 NoWhenBranchMatchedException 라는 더미 예외 클래스를 던지는 것으로 바뀌었음을 알 수 있었다.