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 라는 더미 예외 클래스를 던지는 것으로 바뀌었음을 알 수 있었다.