본문 바로가기
ETC

Arrow Core: Tutorials - Functional Error Handling (작성 중)

by devson 2021. 12. 31.

Functional Error Handling
함수형 에러 핸들링

https://arrow-kt.io/docs/patterns/error_handling/

 

Λrrow

Functional companion to Kotlin's Standard Library

arrow-kt.io

 

When dealing with errors in a purely functional way, we try as much as we can to avoid exceptions.

순수한 함수형 방식으로 에러를 처리할 때, 예외들을 피할 수 있게 최대한 노력한다.

Exceptions break referential transparency and lead to bugs when callers are unaware that they may happen until it’s too late at runtime.

예외들은 호출자가 런타임에 너무 늦게까지 그 예외들을 알지 못할 때 참조 투명성(referential transparency)을 깨뜨리고 버그를 발생시킨다.

In the following example, we are going to model a basic program and go over the different options we have for dealing with errors in Arrow.

다음 예제를 통해, 우린 간단한 프로그램을 모델링하고 Arrow를 통해 에러를 다룰 수 있는 다른 옵션들을 살펴볼것이다.

The program simulates the typical lunch scenario where we have to get the ingredient, and a series of preconditions needs to be met in order to actually prepare and eat it.

프로그램은 전형적인 점심 시간 시나리오(재료들을 어디서 가져와야하고, 재료들을 준비하고 먹기 충족시켜야할 일련의 전제 조건들)를 시뮬레이션한다.

 

Requirements
준비물

  • Take food out of the refrigerator
    음식을 냉장고에서 꺼낸다
  • Get your cutting tool
    썰 도구들을 준비한다
  • Cut up the lettuce to make lunch
    상추를 점심을 만들기 위해 잘라낸다

 

Requirements
준비물

object Lettuce
object Knife
object Salad

fun takeFoodFromRefrigerator(): Lettuce = TODO()
fun getKnife(): Knife = TODO()
fun prepare(tool: Knife, ingredient: Lettuce): Salad = TODO()

 

Exceptions

A naive implementation that uses exceptions may look like this.

예외들을 사용한 순수한 구현체들은 이렇게 될 것이다.

fun takeFoodFromRefrigerator(): Lettuce = throw RuntimeException("You need to go to the store and buy some ingredients")
fun getKnife(): Knife = throw RuntimeException("Your knife needs to be sharpened")
fun prepare(tool: Knife, ingredient: Lettuce): Salad = Salad

As you may have noticed, the function signatures include no clue that, when asking for takeFoodFromRefrigerator() or getKnife(), an exception may be thrown.

알아차릴 수도 있지만, 함수의 시그니처는, takeFoodFromRefreigerator() 또는 getKinife()를 호출했을 때, 던져질 예외에 대한 아무런 단서를 포함하지 않는다.

 

The issues with exceptions
예외의 이슈들

Exceptions can be seen as GOTO statement, given they interrupt the program flow by jumping back to the caller.

예외는 호출자로 다시 돌아옴으로써 프로그램 흐름을 중단(인터럽트)하기 때문에 예외는 GOTO문 처럼 보일 수 있다, 

Exceptions are not consistent, as throwing an exception may not survive async boundaries;

that is to say that one can’t rely on exceptions for error handling in async code, since invoking a function that is async inside a try/catch may not capture the exception potentially thrown in a different thread.

예외를 던짐으로써 비동기 경계들에서 살아남지 못할 수 있기 때문에 예외는 일관적이지 않다;

즉, 비동기 코드에서 에러 처리를 위해 예외에 의존할 수 없다는 것이다.

왜냐하면 try/catch 내부에서 비동기인 함수의 호출은 예외가 잠재적으로 다른 쓰레드에서 던져질 수 있기 때문에 잡지(capture) 못할 수 있기 때문이다.

 

Because of this extreme power of stopping computation and jumping to other areas, Exceptions have been abused even in core libraries to signal events.

계산을 중지시키고 다른 곳으로 뛰어가는 이런 극단적인 힘 때문에, 예외는 이벤트 신호를 보내는 코어 라이브러리에서도 오용되고 있다.

at java.lang.Throwable.fillInStackTrace(Throwable.java:-1)
at java.lang.Throwable.fillInStackTrace(Throwable.java:782)
- locked <0x6c> (a sun.misc.CEStreamExhausted)
at java.lang.Throwable.<init>(Throwable.java:250)
at java.lang.Exception.<init>(Exception.java:54)
at java.io.IOException.<init>(IOException.java:47)
at sun.misc.CEStreamExhausted.<init>(CEStreamExhausted.java:30)
at sun.misc.BASE64Decoder.decodeAtom(BASE64Decoder.java:117)
at sun.misc.CharacterDecoder.decodeBuffer(CharacterDecoder.java:163)
at sun.misc.CharacterDecoder.decodeBuffer(CharacterDecoder.java:194)

 

They often lead to incorrect and dangerous code because Throwable is an open hierarchy where you may catch more than you originally intended to.

Throwable은 여러분이 원래 의도했던 것 보다 더 많은 것을 잡는(catch) 개방형 계층 구조(open hierachy)이기 때문에 예외는 자주 정확하지 않고 위험한 코드를 발생시킨다.

try {
  doExceptionalStuff() //throws IllegalArgumentException
} catch (e: Throwable) { 
    // too broad, `Throwable` matches a set of fatal exceptions and errors a 
   // a user may be unable to recover from:
    // 너무 넓다, `Throwable`은 사용자가 다음으로부터 복구할 수 없는 치명적인 예외들과 에러들의 세트와 연결된다
    /*
    VirtualMachineError
    OutOfMemoryError
    ThreadDeath
    LinkageError
    InterruptedException
    ControlThrowable
    NotImplementedError
    */
}

Furthermore, exceptions are costly to create.

게다가, 예외는 생성하는 것이 비싸다.

Throwable#fillInStackTrace attempts to gather all stack information to present you with a meaningful stacktrace.

Throwable#fillInStackTrace 는 의미있는 stacktrace를 여러분에게 제공하기 위해 모든 스택 정보를 모으려고 시도한다.

public class Throwable {
    /**
    * Fills in the execution stack trace.
    * This method records within this Throwable object information
    * about the current state of the stack frames for the current thread.
    * 실행 스택 트레이스를 채운다.
    * 이 메서드는 Throwable 객체 정보 내에 현재 쓰레드의 스택 프레임들의 현재 상태에 대해 기록한다 
    */
    Throwable fillInStackTrace();
}

Constructing an exception may be as costly as your current Thread stack size, and it’s also platform dependent since fillInStackTrace calls into native code.

예외를 만드는 것은 현재 쓰레드 스택 크기 만큼 비싸질 것이고, 또한 fillInStackTrace는 네이티브 코드를 호출하기 때문에 플랫폼 의존적이다.

More info on the cost of instantiating Throwables, and throwing exceptions in general, can be found in the links below.

일반적으로 Throwable을 객체화하고, 예외를 던지는 비용에 대한 더 많은 정보는 아래 링크에서 확인할 수 있다.

The Hidden Performance costs of instantiating Throwables
Throwable을 객체화의 숨겨진 성능 비용

Exceptions may be considered generally a poor choice in Functional Programming when:

예외는 다음과 같은 경우에 함수형 프로그래밍에서 일반적으로 좋지 않은 선택지로 여겨집니다.

  • Modeling absence
    모델링 부재
  • Modeling known business cases that result in alternate paths
    대체 경로들로 이끄는 비지니스 케이스로 알려진 모델링
  • Used in async boundaries over APIs based callbacks that lack some form of structured concurrency.
    구조화된 동시성의 형태가 결여된 콜백 기반의 API을 통한 비동기 경계에서의 사용
  • In general, when people have no access to your source code.
    일반적으로, 사람들이 여러분의 소스 코드에 접근이 불가능할 때

 

 

---

 

---

 

 

Alternative validation strategies : Failing fast vs accumulating errors
유효성 검사 전략의 대안: 빨리 실패하기 vs 에러 축적하기

In this different validation example, we demonstrate how we can use Validated to perform validation with error accumulation or short-circuit strategies.

여기 또 다른 유효성 검사 예제에서, 어떻게 우리가 Validated에러 축적 또는 short-circuit 전략으로 유효성 검사를 수행하기 위해 사용할 수 있는지에 대해 설명한다.

import arrow.core.Nel
import arrow.core.ValidatedNel
import arrow.core.computations.either
import arrow.core.handleErrorWith
import arrow.core.invalidNel
import arrow.core.traverseEither
import arrow.core.traverseValidated
import arrow.core.validNel
import arrow.typeclasses.Semigroup
import arrow.core.zip

 

Model

모델

sealed class ValidationError(val msg: String) {
    data class DoesNotContain(val value: String) : ValidationError("Did not contain $value")
    data class MaxLength(val value: Int) : ValidationError("Exceeded length of $value")
    data class NotAnEmail(val reasons: Nel<ValidationError>) : ValidationError("Not a valid email")
}

data class FormField(val label: String, val value: String)
data class Email(val value: String)

 

Strategies

전략

/** strategies **/
/** 전략 **/
sealed class Strategy {
    object FailFast : Strategy()
    object ErrorAccumulation : Strategy()
}

/** Abstracts away invoke strategy **/
/** 호출 전략을 추상화한다 **/
object Rules {

    private fun FormField.contains(needle: String): ValidatedNel<ValidationError, FormField> =
        if (value.contains(needle, false)) validNel()
        else ValidationError.DoesNotContain(needle).invalidNel()

    private fun FormField.maxLength(maxLength: Int): ValidatedNel<ValidationError, FormField> =
        if (value.length <= maxLength) validNel()
        else ValidationError.MaxLength(maxLength).invalidNel()

    private fun FormField.validateErrorAccumulate(): ValidatedNel<ValidationError, Email> =
        contains("@").zip(
            Semigroup.nonEmptyList(), // accumulates errors in a non empty list, can be omited for NonEmptyList - 비지 않은 리스트에 에러를 축적한다, NonEmptyList에 대해서 생략 가능
            maxLength(250)
        ) { _, _ -> Email(value) }.handleErrorWith { ValidationError.NotAnEmail(it).invalidNel() }

    private fun FormField.validateFailFast(): Either<Nel<ValidationError>, Email> =
        either.eager {
            contains("@").bind() // fails fast on first error found - 첫 에러 발견 시 바로 실패한다
            maxLength(250).bind()
            Email(value)
        }

    operator fun invoke(strategy: Strategy, fields: List<FormField>): Either<Nel<ValidationError>, List<Email>> =
        when (strategy) {
            Strategy.FailFast ->
                fields.traverseEither { it.validateFailFast() }

            Strategy.ErrorAccumulation ->
                fields.traverseValidated(Semigroup.nonEmptyList()) {
                    it.validateErrorAccumulate()
                }.toEither()
        }
}

Program

프로그램

val fields = listOf(
    FormField("Invalid Email Domain Label", "nowhere.com"),
    FormField("Too Long Email Label", "nowheretoolong${(0..251).map { "g" }}"), //this fails - 실패한다
    FormField("Valid Email Label", "getlost@nowhere.com")
)

Fail Fast

빠른 실패

Rules(Strategy.FailFast, fields)

Error Accumulation

에러 축적

Rules(Strategy.ErrorAccumulation, fields)

 

Credits
크레딧

Tutorial adapted from the 47 Degrees blog Functional Error Handling

튜토리얼은 47 Degrees 블로그 함수형 에러 처리하기를 각색함

댓글