메이비를 사용하면 실패 여부를 정확하게 반환 할 수 있으나, 실패 원인까지 파악 할 수 없다. 실패 여부까지 포함 할 수 있는 컨텍스트인 이더를 모나드로 작성하여 예외처리에 활용 할 수 있다.
이더 모나드 구현
sealedclassEither<outL, outR> : Monad<R> {companionobject {fun <V> pure(value: V) =Right(0).pure(value) }overridefun <V> pure(value: V): Either<L, V> =Right(value)overridefun <R2> fmap(f: (R) -> R2): Either<L, R2> =when (this) {is Left ->Left(value)is Right ->Right(f(value)) }overridefun <B> flatMap(f: (R) -> Monad<B>): Monad<B> =when (this) {is Left ->Left(value)is Right ->f(value) }// override fun <R2> flatMap(f: (R) -> Monad<R2>): Either<L, R2> = when (this) {// is Left -> Left(value)// is Right -> try {// f(value) as Right<R2>// } catch (e: TypeCastException) {// Left(e.message) as Left<L>// }// }}dataclassLeft<outL>(valvalue: L) : Either<L, Nothing>() {overridefuntoString(): String="Left($value)"}dataclassRight<outR>(valvalue: R) : Either<Nothing, R>() {overridefuntoString(): String="Right($value)"}infixfun <L, A, B> Either<L, (A) -> B>.apply(f: Either<L, A>): Either<L, B> =when (this) {is Left ->Left(value)is Right -> f.fmap(value)}
이더 모나드를 활용한 예외처리 예제
funmain() {when(val result =divSubTenBy(5)) {is Left ->println("divSubTenBy(5) error by ${result.value}")is Right ->println("divSubTenBy(5) returns ${result.value}") } // divSubTenBy(5) returns 8when(val result =divSubTenBy(0)) {is Left ->println("divSubTenBy(0) error by ${result.value}")is Right ->println("divSubTenBy(0) returns ${result.value}") } // divSubTenBy(0) error by divide by zero exception}privatefundivideTenBy(value: Int): Either<String, Int> =try {Right(10/value)} catch (e: ArithmeticException) {Left("divide by zero exception")}privatefunsubtractTenBy(value: Int) =10-valueprivatefundivSubTenBy(value: Int) =divideTenBy(value).fmap { subtractTenBy(it) }
3. 트라이 모나드 활용
이더 모나드는 try catch 문으로 예외를 받고, Left에 직접 메세지를 넣어 반환하였다. 트라이 모나드는 예외 자체를 컨텍스트에 담아서, 별도의 예외처리 없이 체이닝을 가능하게 한다.
funmain() {when(val result =divSubTenBy(5)) {is Failure ->println("divSubTenBy(5) error by ${result.e}")is Success ->println("divSubTenBy(5) returns ${result.value}") } // divSubTenBy(5) returns 5when(val result =divSubTenBy(0)) {is Failure ->println("divSubTenBy(0) error by ${result.e}")is Success ->println("divSubTenBy(0) returns ${result.value}") } // divSubTenBy(0) error by java.lang.ArithmeticException: / by zero}privatefundivideTenBy(value: Int): Try<Int> = Try.pure(10).fmap { it /value }privatefunsubtractTenBy(value: Int) =10/valueprivatefundivSubTenBy(value: Int) =divideTenBy(value).fmap { subtractTenBy(it) }
함수형 프로그래밍에서 테스팅하기
함수형 프로그래밍은 일반적인 프로그래밍 방식보다 테스트하기 좋은 코드가 만들어진다.
테스트하기 좋은 코드만들기
첫째, 불변 객체를 사용하라
순수한 함수형 언어에서 한번 생성된 객체는 변경할 수 없다. 불변 객체를 사용한 함수는 예측이 가능하다.
둘째, 동일한 입력에 동일한 출력을 보장하는 순수한 함수를 만들라
셋째, 파일 / 네트워크 IO와 같은 부수효과를 발생하거나 상태를 가진 영역은 순수한 영역과 최대한 분리하라
넷째, 널값의 사용을 피하라
다섯째, 메이비와 이더를 적극적으로 활용하라
위 다섯까지 명제의 방향성은 한가지이며 다음과 같다 : 부수효과를 최대한 결리하고 프로그램을 순수한 함수들로만 구성하라
객체지향 프로그래밍에서는 부수효과를(DB 연결 등) 가진 모듈을 효과적으로 교체하기 위해 의존성 주입(DI) 패턴을 사용하며, 함수형 프로그래밍에서도 태그리스 파이널(Tagless final)이라는 패턴을 사용한다.
태그리스 파이널은 메이비, 리스트, 이더처럼 구체적인 타입을 사용하지 않은 타입 클래스에 행위들을 선언하고 실행 흐름을 선언하는 것이다. 이렇게 선언된 실행 흐름에는 구체적인 타입이 정해져 있지 않다. 실행 흐름이 선언된 상태에서 타입 클래스의 행위들을 구현해서 넘길 수 있는데, 구체적인 타입도 이때 정해진다. 태그리스 파이널 패턴을 사용하면 외부 시스템에 접근하는 모듈을 테스트 목적의 모의 객체로 손쉽게 교체할 수 있다.
함수형 프로그래밍에서 디버깅하기
명령형 프로그래밍은 라인별로 코드를 자겅하기 때문에 주로 중단점 디버거를 사용하지만, 함수형 프로그래밍은 여러 가지 고차 함수의 체인을 한 라인에 선언하고, 심지어 게으른 평가로 인해서 코드의 실행이 특정 라인에서 이루어진다고 보장하기 어렵다. 따라서 함수형 프로그래밍에서는 다른 도구를 사용해야 함
인텔리제이 디버거를 코틀린에서 활용하는 팁을 알아보자
인텔리제이를 활용한 리스트 체인 디버깅
인텔리제이 디버거는 함수에 입력되는 람다 함수 단위로 중단점을 찍을 수 있다.
중단점을 찍은 후 Set Breakpoint를 통해 라인, 람다 혹은 전체 등 중단점을 세밀하게 설정 할 수 있다.
인텔리제이를 활용한 시퀀스 체인 디버깅
게으른 평가를 수행하는 시퀀스는 실제 수행되는 시점에 중단점이 걸린다.
함수 체인이 평가되는 시점이 실제 로직과 아주 멀리 있거나, 시퀀스에 값이 매우 많다면, 특정 값에서 발생하는 오류를 찾기 쉽지 않은데 이럴 때는 람다 중단점에 조건을 서정 할 수 있다(해당 브레이크 포인트에 컨트롤을 누른상태로 클릭)