지연성(Lazyness)은 기술과 실행의 관심사를 분리함으로써 재사용성을 높이는데 훨씬 더 가치가 있다. IO뿐만 아니라, 비동기, 병렬처리에서도 같은 개념으로 활용된다.
지연성을 이용해서 부수효과를 분리하고, 참조투명성을 확보할 수 있다. 테스트 및 검증에도 용이하다.
코틀린에서 IO보다 성능이 우수하고, 참조투명성을 확보할 수 있는 suspend를 제공한다.
참고자료 :
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
처음에 얘기했다시피 함수형 프로그래밍은 제약이 없다. FP에서 외부효과와 입출력을 다루는 방식이 대해서 알아보자. 핵심은 부수효과를 분리해 내는 것이다.
함수는 엄격(strict)할 수 있고, 지연(Lazy)될 수 도 있다. 지연성(Lazyness)를 활용하면 프로그램의 기술과 평가를 분리할 수 있다.
fun <A> lazyIf(
cond: Boolean,
onTrue: () -> A, //<1>
onFalse: () -> A
): A = if (cond) onTrue() else onFalse()
val y = lazyIf((a < 22),
{ println("a") }, // <2>
{ println("b") }
)
간단한 IO의 구현을 알아보자. 지연성을 활용하면 부수효과를 분리할 수 있다. 효과(effects)를 기술하고 최종 실행은 프레임워크에 맡길 수 있다.
sealed interface IO {
val run: () -> Unit
}
class PrintLine(val message: String): IO {
override val run: () -> Unit = { println(message) }
}
fun main() {
val printLine = PrintLine("Hello, World!")
printLine.run()
}
위와 같은 형태라면 테스트 및 검증도 쉬워진다.
assert(printLine.message == PrintLine("Hello, World!").message)
다른 함수형 에코시스템 즉, 스칼라랑 하스켈에서는 사이드 이펙트의 모나딕 모델을 사용한다. 주요 컴포넌트는 IO라고 불리는 모델이다. 애로우는 다른 모델을 차용하는데 suspend와 탑레벨 확장인 suspend () → A를 기본으로 한다.
IO를 사용하는 이유는 사이드 이펙트 코드를 안전사혹 참조 투명하게 작성하는 것이다. 추가적으로 IO는 강력한 동시 수행과 취소를 제공한다. 이러한 점에서 IO와 suspend는 같은 이점을 가지지만, 컴파일러 자체에서 suspend는 지원한다는 점이 다르다.
IO 사용의 가장 기본적인 패턴은 flatMap을 활용하는것이다.
fun number(): IO<Int> = IO.pure(1)
fun triple(): IO<Triple<Int, Int, Int>> =
number().flatMap { a ->
number().flatMap { b ->
number().map { c ->
Triple(a, b, c)
}
}
}
코틀린에서는 suspend를 컴파일러에서 지원하니까 다음과 같이 작성할 수 있다.
suspend fun number(): Int = 1
suspend fun triple(): Triple<Int, Int, Int> =
Triple(number(), number(), number())
개념레벨에서 보면 IO는 항상 성공하거나 예외로 마무리된다. Cats 이펙트의 unsafeRunAsync의 결과를 Either<Thowable, A>이다. 이는 IO에서 발생하는 오류의 처리를 강제한다. 여기서 중요한 점은 다른 스레드에서 비동기로 실행되더라도, 예외는 캡처되며 복구를 수행하도록 한다.
suspend는 항상 Result<A>를 반환하는데, 이것은 Either<Thorwable,A>와 같다. 안전함을 보장함에 된다는 점에서 IO와 같다. startCoroutine의 IO의 unsafeAsync와 같다. f: (Either<Throwable,A> → Unit) 대신에 suspend () → A를 실행하기 위해 f: (Result<A>) → Unit을 제공받는다.
성능은 suspend과 IO보다 월등히 우수하다. 이유는 IO는 런타임에 추가적으로 리소스가 들지만, suspend는 컴파일 타임에 이루어진다.
일단 IO라는 타입을 쓰면 해당 타입을 할당하는데 추가적인 리소스가 사용된다. IO타입을 연산하고, 지연계산을 하는데 스택이나 힙을 추가적으로 사용하게 된다. 반면에 suspend는 코틀린 컴파일러가 각 서스펜드 포인트를 알아내서, 해체한 다음 더 효과적인 코드로 변환한다.
fun number(): IO<Int> = IO.pure(1)
fun triple(): IO<Triple<Int, Int, Int>> =
number().flatMap { a ->
number().flatMap { b ->
number().map { c ->
Triple(a, b, c)
}
}
}
위와 같은 코드를 사용하게 되면 실제로 6개의 IO 클래스를 할당하게 된다.
fun number(): IO<Int> = IO.Just(1)
fun triple(): IO<Triple<Int, Int, Int>> =
IO.FlatMap(IO.Pure(1)) { a ->
IO.FlatMap(IO.Pure(1)) { b ->
IO.Map(IO.Pure(1)) { c -> Triple(a, b, c) }
}
}
지연성(Lazyness)은 기술과 실행의 관심사를 분리함으로써 재사용성을 높이는데 훨씬 더 가치가 있다. IO뿐만 아니라, 비동기, 병렬처리에서도 같은 개념으로 활용된다.
지연성을 이용해서 부수효과를 분리하고, 참조투명성을 확보할 수 있다. 테스트 및 검증에도 용이하다.
코틀린에서 IO보다 성능이 우수하고, 참조투명성을 확보할 수 있는 suspend를 제공한다.
참고자료 :
불변 컬렉션/Immutable Collections (Kotlin 함수형 프로그래밍 #5) (4) | 2025.01.16 |
---|---|
Option을 사용해야하는 이유/Nested Nullability (Kotlin 함수형 프로그래밍 #4) (1) | 2025.01.15 |
타입 에러/Typed Errors (Kotlin 함수형 프로그래밍 #3) (0) | 2025.01.14 |
함수형 데이터구조/대수적 타입/Algebraic Data Type (Kotlin 함수형프로그래밍 #3) (0) | 2025.01.13 |
불변 데이터/Immutable Data/Optics(Kotlin 함수형 프로그래밍 #2) (0) | 2025.01.09 |