Kotlin Functors, Applicatives, And Monads in Pictures. Part 2/3

Lazysoul
6 min readOct 8, 2017

--

이전 글(Functor)을 못보신 분들은 아래 링크를 확인해 주시기 바랍니다.

Applicatives

Applicative를 사용하면, Functor 예제처럼 값을 박스(context)에 래핑 할 수 있습니다.

또한 일반 값이 아닌 함수도 박스에 래핑 할 수 있습니다.

하스켈과 달리 Kotlin 은 명시적으로 Applicative를 지원하지 않지만, 구현하기 어렵지 않습니다. 일반적으로 Functor를 지원 하는 함수는 map이 있듯이, Applicative를 지원하는 함수들은 apply함수를 가지고있습니다. 제너릭타입으로 래핑된 함수에 래핑된 값을 적용 해보겠습니다.

infix fun <A, B> Option<(A) -> B>.apply(f: Option<A>): Option<B> =
when (this) {
is Option.None -> Option.None
is Option.Some -> f.map(this.value)
}
infix inline fun <A, reified B> Array<(A) -> B>.apply(a: Array<A>) =
Array(this.size * a.size) {
this[it / a.size](a[it % a.size])
}

this 와 함수 f가 모두 Some이면 언래핑된 옵션에 함수가 적용 됩니다. 그렇지 않은 경우에는 None 을 반환합니다.

Array 의 경우 배열을 생성하기 위해 생성자 매개변수를 사용하고 있습니다.

Some({ a: Int -> a + 3 }) apply Some(2)
// => Some(5)

Option(function) apply Option(value) 의 경우에만(function에 value를 전달) 동작합니다.

fun <A, B> Option<A>.apply(o: Option<(A) -> B>) = {...}
fun <A, B> Option<(A) -> B>.apply(o: Option<A>) = {...}

Kotlin은 Java 코드와 같이 사용 되기 때문에 type erasure 이슈를 가지고 있습니다. 그렇기 때문에 두 함수는 컴파일 될때 제네릭 타입정보를 잃어버리게 되 컴파일 에러가 발생합니다. 회피하는 방법으로는dummyImplicit 방법이 있습니다.

fun <A, B> Option<A>.apply(f: Option<(A) -> B>, 
dummyImplicit: Any? = null): Option<B> =
when (this) {
is Option.None -> Option.None
is Option.Some -> f.map { it(value) }
}

다음과 같이 활용 할 수 있습니다.

Some(2).apply(Some({ a: Int -> a + 3 }))
// => Some(5)

infix형식은 인자가 하나만 있어야 하기 때문에 위에 선언한 apply 함수는 infix 함수로 만들 수 없습니다.

이전설명들 처럼 apply를 사용하면 흥미로운 상황이 발생할 수 있습니다.

arrayOf<(Int) -> Int>({ it * 2 }, { it + 3}) apply arrayOf(1, 2, 3)
// => [ 2, 4, 6, 4, 5, 6 ]

래핑 된 2 값에 함수를 어떻게 적용할까요?

Functor:

fun curriedAddition(a: Int) = { b: Int ->
a + b
}
Some(3) map ::curriedAddition map Some(2)
// => COMPILER ERROR

Applicative:

Some(3) map ::curriedAddition apply Some(2)
// => Some(5)

하나씩 하나씩 좀 더 자세히 살펴 보겠습니다.

Some(3) map ::curriedAddition
// => Some({ 3 + b })
Some({ 3 + b }) apply Some(2)
// => Some(5)

map과 apply 함수를 사용해서 래핑된 커링을 사용 할 수도 있습니다.

fun curriedTimes(a: Int) = { b: Int ->
a * b
}
Some(3) map ::curriedTimes apply Some(5)
// => Some(15)

예제를 통해 더 살펴보겠습니다.

3개의 인자 입력받고 값을 모두 곱하는 함수를 예로 들겠습니다.

fun tripleProduct(a: Int, b: Int, c: Int) = a * b * c

이전과 같은 방법으로 문제를 해결하겠습니다. 하지만 이전처럼 커링을 직접 구현하지 않겠습니다.

fun <A, B, C, D> curry(f: (A, B, C) -> D): (A) -> (B) -> (C) -> D = { a -> { b -> { c -> f(a, b, c) } } }

실제 적용한다면 다음과 같습니다.

Some(3) map curry(::tripleProduct) apply Some(5) apply Some(4)
// => Some(60)

Applicative를 사용하면 래핑된 함수에 래핑된 값을 적용 할 수 있습니다.

--

--