모나드 체인이 목표하는 코드 모양과 실행 순서

Posted on August 6, 2020

수학적인 배경 지식없이 순수하게 코드 모양만 보고, 모나드와 친해지기 위해 생각해 본 내용입니다. 이게 모나드를 이해하는데 도움이 될지 어떨지는 아직 잘 모르겠습니다. 일단 올려두겠습니다.

클로저는 살아 있다

하스켈 전체 코드가 다음과 같은 모양을 만드는 게 목표인데, 모나드는 특별히 a -> m b 형태의 함수를 바인드에 넣어서 바인드들이 아래 모양으로 되게 할 때 쓰입니다.

funcs = (..(..(..(..))))
-- $ 연산자로 표현하면 
funcs = .. $ .. $ .. $ ..

함수형에서 작업을 순차적으로 실행하려면 이 구조로 만드는 수 밖에 없습니다. 그런데 모양은 같은데 실행 순서가 이랬다 저랬다 합니다.

품고 품은 상태의 실행 순서는?

f1 = \_ -> putStrLn "f1"
g2 = \_ -> putStrLn "g2"
h3 = \_ -> putStrLn "h3"

glue :: (() -> IO ()) -> IO () -> IO () 
glue f x = do 
    unwrapx <- x
    f unwrapx 

> glue f (glue g (glue h (putStrLn "begin or end")))
begin or end
h3
g2
f1

괄호 안부터 실행됩니다. 그럼 다음 코드도 h가 가장 먼저 실행될까요?

glue2 :: (() -> IO ()) -> (() -> IO ()) -> IO () 
glue2 f1 f2 = do 
    f1 ()
    f2 ()

> glue2 f (\_ -> glue2 g (\_ -> glue2 h (\_ -> putStrLn "begin or end")))
f1
g2
h3
begin or end

품고 품었다고 다 같지 않습니다. glue의 두 매개 변수 fx는 독립적이지 않습니다. x를 알아야만 f를 알 수 있습니다. 하지만 glue2의 두 매개 변수 f1f2는 독립적입니다. 그래서 실행 순서가 서로 반대가 됩니다. 눈에 보이는 구조만으론 구별할 수 없습니다. 구조를 만드는 접착제로 쓰인 glue, glue2의 정의에 따라 달라집니다.

2021.5.1 추가
glueglue2는 내부 구현이 IO컨텍스트에 있으므로 인자로 들어오는 액션의 실행 순서를 이미 지정해 놨다고 보면 됩니다.
glue두 번째 인자 >>= \_ -> 첫 번째 인자
glue2첫 번째 인자 >>= \_ -> 두 번째 인자

품을 것인가, 품 속으로 들어갈 것인가

새로 들어오는 작업을 가장 바깥 쪽에 둘 수도 있고, 가장 안 쪽에 품어지도록 할 수도 있습니다. Free, Cont의 경우는 가장 안 쪽에 붙이고, Reader는 바깥 쪽에 둡니다. Reader는 안 쪽부터 실행되고 FreeCont는 바깥 쪽부터 실행됩니다. 결국 모나드는 do구문 안에 써있는 순서대로 동작합니다.

정말 여러 모나드들이 모두 이 구조를 만들기 위한 것인지 한 번 살펴보겠습니다.

Free 모나드의 바인드

(Free x) >>= g = Free (fmap (>>= g) x)

몇 겹의 Free로 쌓여 있든, 가장 안 쪽으로 들어가 g를 적용합니다. g의 결과값은 Free 타입이니 가장 안 쪽에 Free를 연결하는 효과가 생깁니다. Free ..(Free ..(Free ..))

Cont 모나드의 바인드

s >>= f = cont $ \c -> runCont s $ \x -> runCont (f x) c

runContCont를 벗겨내는 역할을 하는데, 여기선 읽기 편하게 runCont s를 그냥 s로 표기하고, cont는 다시 Cont로 만드는 생성자로 잠시 빼고 읽어 보겠습니다.

\c -> s $      \x -> (f x) c

s 후 다음 작업이 f 이고, 그 다음 작업이 c 입니다. 코드 모양대로 표현하면 s .. (f .. (c ..))

Reader 모나드의 바인드

m >>= k = Reader $ \r -> runReader (k (runReader m r)) r 

핵심만 보기 위해 몇가지 바꿔서 보겠습니다. runReaderReader를 벗겨내는 동작과 Reader는 생성자로 잠시 빼고 읽으면

\r -> k (m r) r 

r을 받아 m을 적용 후 결과를 k에 넘겨주는데, r도 또 넘겨 줍니다. k (m …) …

Github 계정이 없는 분은 메일로 보내주세요. lionhairdino at gmail.com