수학적인 배경 지식없이 순수하게 코드 모양만 보고, 모나드와 친해지기 위해 생각해 본 내용입니다. 이게 모나드를 이해하는데 도움이 될지 어떨지는 아직 잘 모르겠습니다. 일단 올려두겠습니다.
하스켈 전체 코드가 다음과 같은 모양을 만드는 게 목표인데, 모나드는 특별히 a -> m b
형태의 함수를 바인드에 넣어서 바인드들이 아래 모양으로 되게 할 때 쓰입니다.
= (..(..(..(..))))
funcs -- $ 연산자로 표현하면
= .. $ .. $ .. $ .. funcs
함수형에서 작업을 순차적으로 실행하려면 이 구조로 만드는 수 밖에 없습니다. 그런데 모양은 같은데 실행 순서가 이랬다 저랬다 합니다.
= \_ -> putStrLn "f1"
f1 = \_ -> putStrLn "g2"
g2 = \_ -> putStrLn "h3"
h3
glue :: (() -> IO ()) -> IO () -> IO ()
= do
glue f x <- x
unwrapx
f unwrapx
> glue f (glue g (glue h (putStrLn "begin or end")))
or end
begin
h3
g2 f1
괄호 안부터 실행됩니다. 그럼 다음 코드도 h
가 가장 먼저 실행될까요?
glue2 :: (() -> IO ()) -> (() -> IO ()) -> IO ()
= do
glue2 f1 f2
f1 ()
f2 ()
> glue2 f (\_ -> glue2 g (\_ -> glue2 h (\_ -> putStrLn "begin or end")))
f1
g2
h3or end begin
품고 품었다고 다 같지 않습니다. glue
의 두 매개 변수 f
와 x
는 독립적이지 않습니다. x
를 알아야만 f
를 알 수 있습니다. 하지만 glue2
의 두 매개 변수 f1
과 f2
는 독립적입니다. 그래서 실행 순서가 서로 반대가 됩니다. 눈에 보이는 구조만으론 구별할 수 없습니다. 구조를 만드는 접착제로 쓰인 glue
, glue2
의 정의에 따라 달라집니다.
2021.5.1 추가
glue
와 glue2
는 내부 구현이 IO
컨텍스트에 있으므로 인자로 들어오는 액션의 실행 순서를 이미 지정해 놨다고 보면 됩니다.
glue
는 두 번째 인자 >>= \_ -> 첫 번째 인자
glue2
는 첫 번째 인자 >>= \_ -> 두 번째 인자
새로 들어오는 작업을 가장 바깥 쪽에 둘 수도 있고, 가장 안 쪽에 품어지도록 할 수도 있습니다. Free
, Cont
의 경우는 가장 안 쪽에 붙이고, Reader
는 바깥 쪽에 둡니다. Reader
는 안 쪽부터 실행되고 Free
와 Cont
는 바깥 쪽부터 실행됩니다. 결국 모나드는 do
구문 안에 써있는 순서대로 동작합니다.
정말 여러 모나드들이 모두 이 구조를 만들기 위한 것인지 한 번 살펴보겠습니다.
Free x) >>= g = Free (fmap (>>= g) x) (
몇 겹의 Free
로 쌓여 있든, 가장 안 쪽으로 들어가 g
를 적용합니다. g
의 결과값은 Free
타입이니 가장 안 쪽에 Free
를 연결하는 효과가 생깁니다. Free ..(Free ..(Free ..))
>>= f = cont $ \c -> runCont s $ \x -> runCont (f x) c s
runCont
는 Cont
를 벗겨내는 역할을 하는데, 여기선 읽기 편하게 runCont s
를 그냥 s
로 표기하고, cont
는 다시 Cont
로 만드는 생성자로 잠시 빼고 읽어 보겠습니다.
-> s $ \x -> (f x) c \c
s
후 다음 작업이 f
이고, 그 다음 작업이 c
입니다. 코드 모양대로 표현하면 s .. (f .. (c ..))
>>= k = Reader $ \r -> runReader (k (runReader m r)) r m
핵심만 보기 위해 몇가지 바꿔서 보겠습니다. runReader
는 Reader
를 벗겨내는 동작과 Reader
는 생성자로 잠시 빼고 읽으면
-> k (m r) r \r
r
을 받아 m
을 적용 후 결과를 k
에 넘겨주는데, r
도 또 넘겨 줍니다. k (m …) …