아마도 띄어 쓰기가 Monadic Stream1, Function 이 아니라 Monadic, Stream Function 처럼 보입니다.
주로 Fault Tolerant Functional Reactive Programming (Functional Pearl) - Ivan Perez 텍스트를 보며 메모를 추가했습니다.
언제든 꺼내 올 수 있는, 무한 함수 제공 상자가 있습니다. 함수를 하나 꺼내서 써먹으면, 그 결과를 반영해서 다음 번에 꺼내 올 함수를 상자 안에 준비 합니다. 예를 들면, 누적값등을 계속 다음 준비되는 함수에 넘기는 겁니다. 이 상자에서 꺼내 쓰는 함수들만의 컨텍스트가 생길거라 추측할 수 있습니다.
하나의 함수를, 하나의 값에 적용하는 보통의 상황과 달리 스트림 함수라 부를 때는,
하나의 함수를, 스트림 원소 마다 계속 적용하는 게 아니라
하나의 함수를, 스트림에서 꺼내온 한 원소에 적용하고 나면, 함수를 변형해서 그 다음 원소에 적용할 준비를 합니다. 그리고, 다음 원소가 존재한다면 계속 적용, 변형해서 준비를 반복합니다.
@todo - 지금 지식으론 스트림에 적용하는 용도로 쓰는 “스트림 함수”도 맞지만, 위에 얘기한 함수 상자를 “함수 스트림”이라 부를 수도 있지 않을까?
MSF
는 스트림을 다루면서, 프로그래머에게 드러나지 않게 상태, 혹은 컨텍스트, 혹은 side-effect를 어떻게 잘 유지, 전달할 것인가에 대한 해결책 중 하나입니다.
자기 자신과 같은 타입을 가지고 있는 재귀 타입입니다.
data MSF m a b = MSF { unMSF :: a -> m (b, MSF m a b) }
MSF(Monadic Stream Function)는
- 폴리모픽 타입
- 입력값에 MSF를 적용하고,
모나딕 컨텍스트 안에 결과값과 Continuation을 넣어 반환하는 함수
두 가지로 이루어져 있습니다.
순환 타입이 불편한 분은 Arrow 예시 - Circuit의 Circuit
타입을 참고해 보세요. 거의 같은 아이디어입니다. 아래 step
함수로 MSF
가 가진 함수를 a
에 한 번 적용하게 되면, 결과와 함께 조금 바뀌었을 수도 있는, 다음에 쓸 MSF
를 준비합니다.
위 타입을 읽을 때 불편한 이유는, 파트너 Runner 언급이 없는 것도 한 몫합니다. MSF는 함수 하나를 가지고 있을 뿐입니다. 어디선가 이 함수를 꺼내서 적용을 하면 결과로 m (b, MSF m a b)
를 받을 겁니다. 타입만으로 알 수 있는 건 여기까지입니다. 이 후 매직은 Runner가 어찌 할 거라 믿고 넘어가야 합니다. 예를 들면, Runner가 어떤 조건에 맞으면 이 결과를 이용하게 될텐데
m
컨텍스트를 동작 시키기 위해 패턴 매칭을 할테고,(b, Next:MSF m a b)
에서 b
가 필요한 곳이 있으면 넘겨 주고, 스트림의 다음 요소에 Next
를 적용하겠거니 예측할 수 있습니다.Runner의 언급이 있었다면 좀 더 편하게 보이지 않았을까요?
예를 들면, 시간을 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
가 나옵니다. 그런데, 스트림 함수라면 어떤 동작이 추가될까요?
-> m ( b , MSF ... )
a -- ^^^^^^^
-- continuation
스트림에서 한 원소를 가져와 함수를 적용하고 나면, 결과와 다음 원소에 적용할 함수를 준비합니다. 잊어버리지 말아야 할 정보들은 이 함수에 넣어 준비하면 됩니다. 그럼 누군가 가져다 쓸 겁니다. (항상 이 조건이 맞아야 범용적인 용어로서의 스트림 함수라 부르는지는 잘 모르겠습니다. 일단, 여기선 스트림 함수라 하는 것은, 모두 다음에 쓸 Continuation을 준비합니다.) step by step
또는 sample by sample
로 동작하는 step
을 유심히 보면, 출력 스트림의 n
번 째 요소를 보려면, 그 전에 입력 스트림 n
개를 거쳐서 오는게 보입니다.
물론 m
컨텍스트가 있으니 패턴 매칭으로 이를 처리하는 코드도 있을 겁니다.
타입과 step
만으론 스트림 causal(인과관계)을 표현하지 못 합니다. 스트림 입력에 함수를 연이어 적용하고, 매 번 나오는 effect도 잘 join해서 끌고 가는 함수가 필요합니다.
Data.MonadicStreamFunction에서 발췌
embed :: Monad m => MSF m a b -> [a] -> m [b]
= return []
embed _ [] :as) = do -- 스트림에 있는 모나드 m의 do
embed sf (a<- unMSF sf a
(b, sf') <- embed sf' as -- 다음 원소에 접근 할 땐 sf가 아니라 변형된 sf'입니다.
bs return (b:bs)
당연히 스트림에 다음 원소가 아직 안들어 오면, bs
는 준비되지 않습니다. 하지만 b
는 준비 됐으니, Lazy한 스트림을 다루는 함수들은 b
만 먼저 가져가게 될 겁니다. 모나드 컨텍스트 안이니 매 번 바인드가 동작하면서 이펙트에 관한 처리(패턴 매칭)를 하고 있을 겁니다.
= do
main -> 1 + n)) [1,2,3] embed (arr (\n
λ> [2,3,4]
time t
에서 출력은, 시작 시간 부터 t
까지, 즉 [0,t]
동안의 입력에 의존합니다.
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)
위에서 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
= MSF $ \future_a ->
feedback past sf -- m 모나드의 do
do
<- unMSF sf (future_a, past)
((result', past'), sf') -- m의 바인드가 돌며 effect에 필요한 동작을 합니다.
return (result', feedback past' sf')
-- 후임 MSF를 바로 써먹지 않고, 또 다시 feedback을 먹입니다.
※ 누적된 값은 과거의 값이어서 past
, 나중에 embed
가 넣어 주게 될 값은 futuer_a
라 이름 붙였습니다.
세세한 걸 보기전에 윤곽만 먼저 짚으면, 초기값
과 MSF
를 넣어주면, MSF
를 돌려주는 컴비네이터입니다. 누적의 초기값
으로 쓰일 값을 넘겨서 MSF
를 변형한다고 볼 수 있습니다. MSF
에 들어 있는 함수 타입이 아무거나 되는 건 아니고, ( , ) -> m ( , )
이어야 합니다. 2튜플을 입력으로 받는데, 2튜플 중 하나는 지금 넣어주는 초기값이고, 나머지 하나는 나중에 받을 값입니다.
embed
가 feedback
을 먹인 MSF
를 받는다면, 첫 번째 원소에 적용하고, 그 다음에 쓸 MSF
는 feedback past' sf'
즉, sf'
만 알고 있는 게 아니라, 누적값 past'
을 같이 가지고 있는 작업입니다. 다음 작업으로 sf'
을 쓰는데, State
모나드처럼 추가적인 정보 past'
를 쓸 수 있습니다.
※ 처음 feedback
만 봐서는 딱히, 무슨 일을 하는지, 왜 이름이 feedback
인지 눈에 잘 안들어 왔는데, embed
와 붙여 읽으니 조금 눈에 들어옵니다.
아래 코드를 말로 읽어 보겠습니다.
sumFrom :: (Num n, Monad m) => n -> MSF m n n
= feedback n0 (arr add2)
sumFrom n0 where
= let n' = n + acc in (n', n') add2 (n, acc)
해석하면, 처음 embed
가 sumFrom
을 한 번 실행하면, 초기값 n0
와 리스트에서 뽑은 원소 하나를 더하고, 두 개를 더한 값을 튜플에 넣어서 다음 번 sumFrom
을 실행할 준비를 합니다.
count :: (Num n, Monad m) => MSF m () n
= arr (const 1) >>> sumFrom 0 count
여기서부터 살짝 MSF
Arrow의 장점이 보이기 시작합니다. const 1
은 embed
가 무슨 값을 넣어 주든 1
로 바꿉니다. 그리고 이 sumFrom
에 초기값 0
을 넣어 돌리니, 1
을 원소 수만큼 더하게 되어 최종 값은 전체 개수를 뜻하게 됩니다.
= do
main "foo", "bar", "baz"] embed count [
λ> [1,2,3]
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
<- arrM (\_ -> asks windowWidth) -< ()
winW <- arrM (\_ -> asks windowHeight) -< ()
winH -< (winW - x, winH - y) returnA
run~
2으로 시작하는 MSF running 함수들을 통해서 모나딕 effect를 날리고 “flatten”할 수 있습니다.
첫 번째 MSF
가 Nothing
이면, 거기서 부턴 두 번 째 MSF
를 실행합니다. 스트림 함수니 “거기서 부터”란 말이 들어갑니다.
…작성 중 (이 후 텍스트는 Faults in Reactive Systems으로 넘어 갑니다. MSF
를 충분히 보고 넘어 가야겠습니다. 한 동안은 계속 작성 중 상태일 듯 합니다.)
모나딕 스트림
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 은 여기서 말하는 모나딕 스트림과는 상관 없는 듯 보입니다. 특정 논문에서만 붙인 이름들인지, 통용되는 이름인지 조차 모르겠습니다. 아직 단편적인 지식만 있어, 실제 프로젝트에 사용해 보면서 보완해야 할 것 같습니다.↩︎
상상입니다. - 함수의 적용을 나타내는 ($)
도 일종의 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
를 위해 초기화된 평가 함수 step
은 setp :: 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
가 더 합리적인 이름이 아닐까요?↩︎