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

Lazysoul
8 min readSep 11, 2017

몇 달 전 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을 적용 할 수 있습니다. 다음은mapSomeNone을 정의한 코드입니다.

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 을 사용해 함수 합성을 할 수 있습니다.

Functor에 대해 간단하게 소개를 했고 도움이 되길 바랍니다. 원본의 설명은 더 길지만, 조금 씩 나눠서 설명 하겠습니다. 다음은 Applicatives에 대해 설명하겠습니다.

--

--