Monadic Stream Function (작성 중)

Posted on October 17, 2023

아마도 띄어 쓰기가 Monadic Stream1, Function 이 아니라 Monadic, Stream Function 처럼 보입니다.

주로 Fault Tolerant Functional Reactive Programming (Functional Pearl) - Ivan Perez 텍스트를 보며 메모를 추가했습니다.

생각 스트레칭

언제든 꺼내 올 수 있는, 무한 함수 제공 상자가 있습니다. 함수를 하나 꺼내서 써먹으면, 그 결과를 반영해서 다음 번에 꺼내 올 함수를 상자 안에 준비 합니다. 예를 들면, 누적값등을 계속 다음 준비되는 함수에 넘기는 겁니다. 이 상자에서 꺼내 쓰는 함수들만의 컨텍스트가 생길거라 추측할 수 있습니다.

하나의 함수를, 하나의 값에 적용하는 보통의 상황과 달리 스트림 함수라 부를 때는,
하나의 함수를, 스트림 원소 마다 계속 적용하는 게 아니라
하나의 함수를, 스트림에서 꺼내온 한 원소에 적용하고 나면, 함수를 변형해서 그 다음 원소에 적용할 준비를 합니다. 그리고, 다음 원소가 존재한다면 계속 적용, 변형해서 준비를 반복합니다.

@todo - 지금 지식으론 스트림에 적용하는 용도로 쓰는 “스트림 함수”도 맞지만, 위에 얘기한 함수 상자를 “함수 스트림”이라 부를 수도 있지 않을까?

MSF는 스트림을 다루면서, 프로그래머에게 드러나지 않게 상태, 혹은 컨텍스트, 혹은 side-effect를 어떻게 잘 유지, 전달할 것인가에 대한 해결책 중 하나입니다.

모나딕 스트림 함수 - MSF 타입

자기 자신과 같은 타입을 가지고 있는 재귀 타입입니다.

data MSF m a b = MSF { unMSF :: a -> m (b, MSF m a b) }

MSF(Monadic Stream Function)는 - 폴리모픽 타입 - 입력값에 MSF를 적용하고,
모나딕 컨텍스트 안에 결과값과 Continuation을 넣어 반환하는 함수

두 가지로 이루어져 있습니다.

순환 타입이 불편한 분은 Arrow 예시 - CircuitCircuit 타입을 참고해 보세요. 거의 같은 아이디어입니다. 아래 step함수로 MSF가 가진 함수를 a에 한 번 적용하게 되면, 결과와 함께 조금 바뀌었을 수도 있는, 다음에 쓸 MSF를 준비합니다.

위 타입을 읽을 때 불편한 이유는, 파트너 Runner 언급이 없는 것도 한 몫합니다. MSF는 함수 하나를 가지고 있을 뿐입니다. 어디선가 이 함수를 꺼내서 적용을 하면 결과로 m (b, MSF m a b)를 받을 겁니다. 타입만으로 알 수 있는 건 여기까지입니다. 이 후 매직은 Runner가 어찌 할 거라 믿고 넘어가야 합니다. 예를 들면, Runner가 어떤 조건에 맞으면 이 결과를 이용하게 될텐데

Runner의 언급이 있었다면 좀 더 편하게 보이지 않았을까요?

step

예를 들면, 시간을 Discrete하게 모델링(샘플링)해서 단위 시간마다 잘라서 바라 볼때, 다음 단위 시간으로 넘어가는 함수를 step이라고 부릅니다.

step :: Monad m => MSF m a b -> a -> m (b, MSF m a b)

스트림용이 아닌 보통의 함수라면 a -> b 또는 a -> m b 등의 함수를 a에 적용해서 결과값 b 혹은 m b가 나옵니다. 그런데, 스트림 함수라면 어떤 동작이 추가될까요?

a -> m (  b  ,  MSF ... )
--              ^^^^^^^
--            continuation

스트림에서 한 원소를 가져와 함수를 적용하고 나면, 결과와 다음 원소에 적용할 함수를 준비합니다. 잊어버리지 말아야 할 정보들은 이 함수에 넣어 준비하면 됩니다. 그럼 누군가 가져다 쓸 겁니다. (항상 이 조건이 맞아야 범용적인 용어로서의 스트림 함수라 부르는지는 잘 모르겠습니다. 일단, 여기선 스트림 함수라 하는 것은, 모두 다음에 쓸 Continuation을 준비합니다.) step by step 또는 sample by sample로 동작하는 step을 유심히 보면, 출력 스트림의 n번 째 요소를 보려면, 그 전에 입력 스트림 n개를 거쳐서 오는게 보입니다.

물론 m컨텍스트가 있으니 패턴 매칭으로 이를 처리하는 코드도 있을 겁니다.

embed

타입과 step만으론 스트림 causal(인과관계)을 표현하지 못 합니다. 스트림 입력에 함수를 연이어 적용하고, 매 번 나오는 effect도 잘 join해서 끌고 가는 함수가 필요합니다.

Data.MonadicStreamFunction에서 발췌

embed :: Monad m => MSF m a b -> [a] -> m [b]
embed _  []     = return []
embed sf (a:as) = do -- 스트림에 있는 모나드 m의 do
  (b, sf') <- unMSF sf a
  bs       <- embed sf' as -- 다음 원소에 접근 할 땐 sf가 아니라 변형된 sf'입니다.
  return (b:bs)

당연히 스트림에 다음 원소가 아직 안들어 오면, bs는 준비되지 않습니다. 하지만 b는 준비 됐으니, Lazy한 스트림을 다루는 함수들은 b만 먼저 가져가게 될 겁니다. 모나드 컨텍스트 안이니 매 번 바인드가 동작하면서 이펙트에 관한 처리(패턴 매칭)를 하고 있을 겁니다.

main = do
  embed (arr (\n -> 1 + n)) [1,2,3]
λ> [2,3,4]

time t에서 출력은, 시작 시간 부터 t까지, 즉 [0,t]동안의 입력에 의존합니다.

Arrow 예시 - runCircuit 참고

Monadic Stream Function 합성

arr :: (a → b) → MSF m a b
(≫) :: MSF m a b → MSF m b c → MSF m a c
(&&&) :: MSF m a b → MSF m a c → MSF m a (b, c)

Arrow는 모나드의 일반화 참고

feedback

위에서 bs를 구하는 과정 embed sf' as를 보면, 이전 작업 결과 b를 참조하거나 하지 않습니다. 이 전 작업의 결과를 참조하려면 어떻게 할까요? (텍스트에선 과거에 의존하기라 부르고 있습니다.)embed는 후임MSF를 받아 그냥 적용할 뿐입니다. 한 가지 방법은 embed에 넘기기 전에 후임 MSF에 이전 결과를 심어 놓으면 됩니다. (추측-MSF로 만들어 낸 결과를 다시 MSF에 넣는다 해서 feed-back 아닐까요?)

※ 이전 작업의 결과를 다음 작업에 넣어주는, embed 비슷한 별도의 runner를 만들어도 될 것 같은데, 여기선 스트림 함수 자체에 필요한 정보를 실어 놓는 컴비네이터를 소개하고 있습니다. 모로 가도 서울로만 가면 되니, 일단 따라가 보겠습니다. 텍스트의 과거 의존이란 설명보다 소스 동작을 보는 게 더 이해하기 편합니다.

feedback :: Monad m => past -> MSF m (future_a, past) (result, past) -> MSF m future_a result 
feedback past sf = MSF $ \future_a -> 
        -- m 모나드의 do
        do 
          ((result', past'), sf') <- unMSF sf (future_a, past)
--        m의 바인드가 돌며 effect에 필요한 동작을 합니다.    
          return (result', feedback past' sf') 
--                           후임 MSF를 바로 써먹지 않고, 또 다시 feedback을 먹입니다.

※ 누적된 값은 과거의 값이어서 past, 나중에 embed가 넣어 주게 될 값은 futuer_a라 이름 붙였습니다.

세세한 걸 보기전에 윤곽만 먼저 짚으면, 초기값MSF를 넣어주면, MSF를 돌려주는 컴비네이터입니다. 누적의 초기값으로 쓰일 값을 넘겨서 MSF를 변형한다고 볼 수 있습니다. MSF에 들어 있는 함수 타입이 아무거나 되는 건 아니고, ( , ) -> m ( , )이어야 합니다. 2튜플을 입력으로 받는데, 2튜플 중 하나는 지금 넣어주는 초기값이고, 나머지 하나는 나중에 받을 값입니다.

embedfeedback을 먹인 MSF를 받는다면, 첫 번째 원소에 적용하고, 그 다음에 쓸 MSFfeedback past' sf' 즉, sf'만 알고 있는 게 아니라, 누적값 past'을 같이 가지고 있는 작업입니다. 다음 작업으로 sf'을 쓰는데, State모나드처럼 추가적인 정보 past'를 쓸 수 있습니다.

※ 처음 feedback만 봐서는 딱히, 무슨 일을 하는지, 왜 이름이 feedback인지 눈에 잘 안들어 왔는데, embed와 붙여 읽으니 조금 눈에 들어옵니다.

아래 코드를 말로 읽어 보겠습니다.

sumFrom :: (Num n, Monad m) => n -> MSF m n n
sumFrom n0 = feedback n0 (arr add2)
  where
    add2 (n, acc) = let n' = n + acc in (n', n') 

해석하면, 처음 embedsumFrom을 한 번 실행하면, 초기값 n0와 리스트에서 뽑은 원소 하나를 더하고, 두 개를 더한 값을 튜플에 넣어서 다음 번 sumFrom을 실행할 준비를 합니다.

count :: (Num n, Monad m) => MSF m () n
count = arr (const 1) >>> sumFrom 0

여기서부터 살짝 MSF Arrow의 장점이 보이기 시작합니다. const 1embed가 무슨 값을 넣어 주든 1로 바꿉니다. 그리고 이 sumFrom에 초기값 0을 넣어 돌리니, 1을 원소 수만큼 더하게 되어 최종 값은 전체 개수를 뜻하게 됩니다.

main = do
  embed count ["foo", "bar", "baz"]
λ> [1,2,3]

Arrow 인터페이스

Arrow는 함수를 래핑해 둔 것인데, Monadic Stream Function, 즉 함수를 MSF로 래핑해뒀으니 Arrow가 생각나는게 자연스럽습니다.

arrM :: Monad m => (a -> m b) -> MSF m a b

Fault Tolerant Functional Reactive Programming (Functional Pearl) - Ivan Perez에서 MSF 부분만 가져왔습니다.
원문에 나온 코드는 GHC 9.2.8에서 컴파일되지 않아, 조금 수정했습니다.

{-# LANGUAGE Arrows #-}
import Control.Arrow
import Control.Monad.Trans.MSF
import Control.Monad.Trans.MSF.Reader
--import Control.Monad.Trans.Reader
import Data.MonadicStreamFunction
import Data.Functor.Identity

data Env = Env { windowWidth :: Int
               , windowHeight :: Int
               }
rotateMousePos180 :: MSF (Reader Env) (Int, Int) (Int, Int)
rotateMousePos180 :: proc (x, y) -> do
  winW <- arrM (\_ -> asks windowWidth) -< ()
  winH <- arrM (\_ -> asks windowHeight) -< ()
  returnA -< (winW - x, winH - y)

run~2으로 시작하는 MSF running 함수들을 통해서 모나딕 effect를 날리고 “flatten”할 수 있습니다.

첫 번째 MSFNothing이면, 거기서 부턴 두 번 째 MSF를 실행합니다. 스트림 함수니 “거기서 부터”란 말이 들어갑니다.

…작성 중 (이 후 텍스트는 Faults in Reactive Systems으로 넘어 갑니다. MSF를 충분히 보고 넘어 가야겠습니다. 한 동안은 계속 작성 중 상태일 듯 합니다.)


  1. 모나딕 스트림
    Monsters:programming and reasoning - A study and implrementation of monadic streams 참고
    스트림은 원소들이 순차적으로 (보통은 무한하게) 들어옵니다.

    0, 1, 2, 3, 4, 5, ...

    그냥 순차적으로 들어오는 정보로만 생각했는데, 모나딕 스트림에서 얘기하는 스트림에는 다음 뜻이 들어가 있습니다.

    n번 째 원소(값)를 얻기 위해선, 이전 값들을 모두 traverse해야 합니다.”

    모나딕 스트림은 원소를 보려면, Effect를 평가해야 하는 스트림입니다.

    +--------+    +--------+
    | Effect | 0, | Effect | 1, ...
    +--------+    +--------+
    
               0,            1, ... pure 스트림

    모나딕 스트림에서 n번 째 원소를 얻으려면, 모든 이전 원소와 Effect를 거쳐 와야합니다. pure 스트림은 Effect가 없는, 혹은 Effect가 아무 일도 안하는 모나딕 스트림으로 볼 수 있습니다.
    Just 1 -- Just 2 -- Nothing -- Just 3 -- .. 이런 걸 모나딕 스트림이라 하는가 싶었는데, 이건 그냥 [Maybe a] 입니다. 모나딕 스트림은 다음을 의미합니다. Just (1, Just (2, Just (3, Just (4, Nothing)))) -검증 필요-

    @todo - Monadic Stream Function 은 여기서 말하는 모나딕 스트림과는 상관 없는 듯 보입니다. 특정 논문에서만 붙인 이름들인지, 통용되는 이름인지 조차 모르겠습니다. 아직 단편적인 지식만 있어, 실제 프로젝트에 사용해 보면서 보완해야 할 것 같습니다.↩︎

  2. 상상입니다. - 함수의 적용을 나타내는 ($)도 일종의 runner로 볼 수 있겠습니다.

    runReaderS_ :: Monad m => MSF (ReaderT env m) a b -> env -> MSF m a b

    복잡한 설명보다 서명이 이해하는 출발점입니다. ReaderT를 가지고 있는 MSF를 받아서 ReaderT를 없애고 m만 남아 있는 MSF를 만듭니다. 구현이 자세하게 이해가지 않더라도 ReaderT를 없애기 위해 환경값을 넣어주면 되겠다 정도는 보입니다.

    ghci> embed (runReaderS_ (rotateMousePos180) (Env 1024 768)) [(10, 10), (100, 100)]
    Identity [(1014,758),(924,668)]

    Reader 모나드 안에서 위와 비슷하게 Maybe가 들어가면,

    runMaybeS :: (Functor m, Monad m) => MSF (MaybeT m) a b -> MSF m a (Maybe b)

    여기처럼 특정 모나드 MaybeT를 위해 초기화된 평가 함수 stepsetp :: MSF Maybe a b -> a -> Mabye (b, MSF Maybe a b) 타입으로, 결과 타입이 Maybe이니 continuation이 없는Nothing 경우도 있다는 얘기가 됩니다. runMaybeS는 내부에 있는 MSF가 no result 결과가 한 번 나오면, 계속 Nothing을 출력합니다. 실패failure에서 “복구Recovering”를 하려면 추가 continuation이 필요합니다.

    catchM :: Monad m => MSF (MaybeT m) a b -> MSF m a b -> MSF m a b

    Control.Monad.Trans.MSF.Maybe에 있는 catchMaybe가 같은 함수로 보입니다. catchM이면 모든 모나드에 범용으로 대응할 것 같으니, catchMaybe가 더 합리적인 이름이 아닐까요?↩︎

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