내가 이해하는 모나드에 대한 생각을 잠깐 정리해보려고 한다
1. Monoid
2. Functor
3. Applicative Functor
4. Monad
이 순서를 알아야 한다.
1. Monoid
모노이드는 아주 단순하다.
- 항등원을 가진다
- 결합법칙
- 이항연산
이 셋이 뭔지 모른다면 덧셈을 예로 들겠다. 덧셈은 Monoid다.
1. 항등원을 가진다. (0은 항등원이다)
2. 결합법칙을 가진다 (1 + (2 + 3)) == ((1 + 2) + 3)
3. 이항연산 (덧셈은 두개의 항이 필요하다)
모노이드는 함수형 프로그래밍에서 꽤나 중요한 역할을 한다.
바로 병렬로 fold(fold가 뭔가 모른다면 reduce라고 생각하면 됨)를 할 수 있다는 것이다.
순서가 필요없이 이항연산이 가능하기 때문에, 왼쪽부터 덧셈을 하거나, 오른쪽부터 덧셈을 하거나, 식을 반으로 쪼개서 각자 계산후에 머지를 하더라도 값이 동일하다는 것을 의미한다.
그러므로 모노이드는 꽤나 중요하다.
모노이드면 일단 '이항연산을 안전하게 할 수 있구나' 라는 생각을 하면 된다.
2. Functor
a Functor is a mapping between categories.
두 가지의 카테고리 사이를 이어주는(map) 사상을 말하는 것 같다. (사실 영어는 잘 모르겠다)
하스켈에서 functor는 `fmap` 을 구현한 녀석을 말한다.
`fmap`이란 뭘까?
이건 List 에서 map을 사용하는 것을 생각하면 쉽다.
List에서 map을 사용하면 어떻게 될까? 값이 변경되어도 그것이 List라는 것은 변함이 없다. 즉, Functor는 map이다 라고 생각하는 것이 좀 더 이해하기 쉬울 것 같다.
하스켈 Functor의 fmap의 구조는 아래와 같다.
class Functor f where
fmap :: (a -> b) -> f a -> f b
(<$) :: a -> f b -> f a
3. Applicative Functor
중요한 리소스가 있다.
하스켈에서 Functor와 Applicative Functor의 타입차이를 한번 봐보자.
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
그러니까 functor는 (a -> b) 이고 Applicative functor는 f (a -> b) 임을 보자.
결국 f (a -> b)에서 이 앞에 붙은 f가 무엇이냐는 것이다. 즉. Applicative Functor는 Functor 안에 함수도 들어갈 수 있는 경우를 말한다.
만약에 즉, 위에서 예를 들었던 List안에 [+1, +2] 이런식의 리스트가 존재하여, 합칠 수도 있다.
즉, 이런 타입 안에 함수를 넣을 수 있고, 이 함수는 그 타입에서 가지는 컨텍스트가 적용된다.
(여기서 컨텍스트란 그 타입 안에 내포되어 있는 의미를 말한다. 예를 들어 List는 여러개를 가진다를 말할 수도 있을 것 같다)
4. Monad
모나드는 좀 특이한데, 내 생각에 이것은 좀 필요에 의해서 만들어진 것 같다.
최근에 회사 내에서 스칼라 빨간책 스터디를 하면서 monad에 대한 이야기를 했다.
일반적으로 monad에 대한 가장 간단한 설명은 `flatMap`을 말한다고 할 수 있다. 그런데 flatMap이 왜 중요할까?
class Applicative m => Monad m where
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
return :: a -> m a
여기서 중요한 것은 >>= 이다
m a -> (a -> m b) -> m b 이 형태를 보면 flatMap임을 알 수 있다. (이것이 이상하다고 생각된다면, 자바나 다른 언어에서 map과 flatMap의 차이를 보면 좋을 것 같다)
flatMap이 왜 map이랑 그렇게 다른 것일까?
왜
-- fmap or map (Functor)
(a -> b) -> f a -> f b
-- flatMap (Monad)
m a -> (a -> m b) -> m b
이 둘이 뭐가 그렇게 다른 것일까?
아주 조금 다르지만 아주 크게 다르다. 여기서 중요한 것은
(a -> m b) 이다. 이것이 아주 큰 일을 한다.
일단 Monad는 Functor랑 아주 큰 차이는 Type을 생성하는데 주도권이 서로 다르다는 것이다.
당신은 List [1,2,3,4] 에 더하기 1을 하는 함수를 호출했다고 해보자.
var a = [1,2,3]
function addOne(x) {
return x + 1;
}
a.map(addOne); // [2,3,4]
여기서 당신은 이런걸 원할 수도 있다.
리스트를 두배로 만드는 거다!
var a = [1,2,3]
function double(x) { return [x,x] }
var b = a.map(double); // [[2,2],[3,3],[4,4]]
b.map(double); // [[[2,2],[2,2]],[[3,3],[3,3]],[[4,4],[4,4]]]
이래가지고, 함수를 함성할 수 있을까?
누군가는 이렇게 말할 수 있다. 애초에 왜! 리스트를 리턴하는 함수를 만든거야? 그냥 그 타입에 맞는 것만 리턴하지?!
말은 쉽지만 그렇지 않다. List를 다루는 곳에는 List를 리턴하는 함수를 많이 사용할 수 밖에 없다.
만약에 리턴하는 것이 IO라면? 그리고 IO를 리턴하는 객체가 계속 겹친다면? 제대로 작동할 수 없다.
자바의 Optional도 마찬가지이다. Optional을 실행했는데 안에 Optional이 있다면 어떻게 할 것인가? 재귀로 호출할 것인가?
이런 함수의 합성에서 예기치 않은 타입의 덮어씌움이 있기에 우리는 flatMap을 쓴다. 여기서 한가지 뇌피셜을 더해서 설명하면
monad는 두 개의 타입을 어떻게 concat을 하냐가 관건이다.
즉 파이썬의 키워드를 빌려서 직관적으로 설명을 시도해보겠다.
[1,2,3] ++ [4,5,6] # [1,2,3,4,5,6]
List를 concat한다면 이렇게 될 것이다. 만약에 다른 타입들에도 이 concat이라는 개념을 녹일 수 있다면(concat을 해서 두 타입 합치는 방식을 약속한다면)
flatMap안에 해당 타입에 어울리는 구현으로 만들어서 flatMap을 만들면 타입을 겹겹이 쌓이지 않을 수 있다. 그때그때 concat으로 합치면 되니까 말이다.
우리가 concat이라는 개념을 생각해보는 것이 중요하다. concat은 1번에서 지칭한 Monoid와 같다. (왼쪽부터 수행하건, 오른쪽부터 수행하건 결과값은 동일해야 한다.)
이상 여기까지가 내가 이해한 Monad의 정리다.
모나딕 뭐 이런 말은 안쓰려고 노력했다. (사실 용어에 대해서 완벽하게 알지 못하면 안쓰는게 나을 것 같아서 쓰지 않았다)
사실 더 디테일한 내용들이 있다. 특히 모나드의 경우 위에 설명한 타입만 맞으면 되는 것은 아니고, 몇가지 조건을 만족해야만 한다.