Kotlin Functors, Applicatives, And Monads in Pictures. Part 1/3
몇 달 전 Functors, Applicatives, Monads 를 Kotlin으로 설명한 포스팅을 읽었습니다. 그러다 본격적으로 Monad에 관심이 생겨 다시 공부를 하던 와중 이 포스팅이 생각나 저자에게 허락을 구하고 번역을 해보기로 했습니다. 실제 원문과 표현이 다를수도 있으니 원문이 궁금하신분은 아래 링크를 확인해 주시기 바랍니다.
그럼 본격적으로 Functors, Applicatives, Monads개념을 Kotlin 으로 살펴보겠습니다.
값에 에 함수를 적용하는 법을 알고 있습니다.
값에 함수를 적용하는 법은 매우 간단합니다. 값에 함수를 적용하는 과정을 하나의 context 로 보고, context는 값을 넣을 수 있는 박스라고 가정 해보겠습니다.
값에 함수를 적용하면 context에 따라 다른 결과를 얻을 수 있습니다. 이 개념이 Functors, Applicatives, Monads, Arrows 등 여러 개념의 기본이 됩니다.
Option
이라는 data를 살펴보겠습니다. Option
은 두가지 data type 으로 정의 되어 있습니다.
Note: 그림은 Haskell에서 사용하는 Maybe (Just | None) 이지만, Kotlin에서 커스텀으로 작성한 Option (Some | None) 에 해당합니다.
sealed class Option<out A> {
object None : Option<Nothing>()
data class Some<out A>(val value: A) : Option<A>()
}
Some(T)
과 None
에 함수를 적용 했을때, 어떻게 다른지 살펴 보겠습니다. 먼저 Functors에 대해 알아 보겠습니다.
Functors
값이 박스안에 있다면, 일반 함수를 적용 할 수 없습니다.(래핑을 하면 내부 값을 알지 못하기 때문에)
map
을 사용하면 래핑 된 값에 함수 적용 이 가능합니다. map
을 사용해Some(2)
에 3을 더하는 함수를 적용해보겠습니다.
fun sumThree(n: Int) = n + 3Option.Some(2).map(::sumThree)
// => Some(5)
익명함수를 사용해 다음과 같이 표현 할 수도 있습니다.
Option.Some(2).map { it + 3 }
// => Some(5)
map
은 어떻게 함수를 적용 했을 까요?
Functor는 실제로 무엇일까요?
Functor란map
이 적용 가능한 모든 타입을 말합니다. map
의 적용 방식은 다음과 같습니다.
다음과 같이 적용 할 수 있습니다.
Option.Some(2).map { it + 3 }
// => Some(5)
Option
은 Functor 이기 때문에 map
을 적용 할 수 있습니다. 다음은map
에 Some
과 None
을 정의한 코드입니다.
inline fun <B> map(f: (A) -> B): Option<B> = when (this) {
is None -> this
is Some -> Some(f(value))
}
Option.Some(2).map { it + 3 }
: 코드는 실제로 다음과 같은 로직이 발생합니다.
위 그림이 Functor의 핵심입니다. 래핑된 값에서 값을 빼고 함수를 적용하고 다시 래핑합니다. 이게 바로 Functor 입니다.
None
에도 map
을 사용해 { it + 3 }
을 적용 해보겠습니다.
Option.None.map { it + 3 }
// => None
위 코드를 보면 함수가 적용 되지 않았습니다. 왜 그럴까요? None은 타입이 없기 때문에, 덧셈을 할 수 없습니다. 일반적으로는 None에 함수를 적용하지 않습니다. (어차피 None이기 때문에)
그렇다면 어떤 경우에 사용될까요?
val option: Option<Int> = someCallThatMightReturnNone()
option.map { it + 3 }
// => None
map
을 사용해 None
에 함수를 적용 한다면 결과는 항상None
입니다. 반환 하는 값이 실제로 있을지 없을지 모르는 상황에서 Option
을 주로 사용합니다. Option
을 사용하지 않고 코드를 작성한다면 다음과 같습니다.
let post = Post.findByID(1)
if post != nil {
return post.title
} else {
return nil
}
Kotlin 으로 작성한 Option
functor 사용해보겠습니다.
findPost(1).map(::getPostTitle)
findPost(1)
가 post를 반환 한다면, getPostTitle
을 이용해 title을 반환 합니다. 만약None
을 반환하다면 결과적으로 None
을 반환 합니다. 입력값이 None or Some처럼 있을 수도 없을 수도 있는 경우에 주로 사용합니다.
infix 를 사용하면 map
을 중위 연산으로 표현 할 수도 있습니다.
inline infix fun <B> map(f: (A) -> B): Option<B> { ... }findPost(1) map ::getPostTitle
다른 예제도 있습니다. 함수를 배열에 적용하면 어떻게 될까요?
Array 역시 functor 입니다!
- Kotlin에서 Iterable은 기본적으로 map 함수를 제공합니다.
inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {..}
마지막으로 예제를 하나 더 살펴보겠습니다. 함수에 함수를 적용하면 어떻게 될까요?
{ a: Int -> a + 2 } map { a: Int -> a + 3 }
// => ???
다음과 같은 함수가 있습니다.
함수에 다른 함수를 적용 합니다.
typealias IntFunction = (Int) -> Intinfix fun IntFunction.map(g: IntFunction): IntFunction {
return { x -> this(g(x)) }
}val foo = { a: Int -> a + 2 } map { a: Int -> a + 3 }
foo(10)
// => 15
함수도 Functors 일 수 있습니다. 함수에서 map
을 사용해 함수 합성을 할 수 있습니다.