pull 기반 모나딕 FRP
reactive-banana가 FRP를 어떻게 구현했는지는 보지 않습니다.
reactive-banana 사용법을 따라가며, FRP가 무엇인가만 보려합니다.
※ Hackage에서 FRP로 검색하면 많은 수가 나옵니다. 이 중 예시 코드가 잘 되어 있어, 읽기 편할 것 같은 라이브러리를 골랐을 뿐, 현재(2023.10) 가장 권장되는 라이브러리는 아닌 걸로 보입니다. Original FRP 방식은 가베지 컬렉션에 문제가 있어 Yampa가 해결했다는데, 다른 라이브러리는 잘 모르겠습니다. (퍼포먼스 문제를 가지고 있어, 프로덕트에는 reflex를 더 많이 쓰는 추세라 합니다. - @jhhuh님)
(검증 필요)
나무를 썰지 않고 있다가, 필요할 때 톱을 주면서 “나무를 썰어”라고 시퀀셜하게 할 수도 있고,
톱을 주지 않고 “나무를 썰어”라고 먼저 말해 놓고, 필요할 때 톱을 넘겨도fire
동일한 효과가 납니다. 어차피 톱이 없으면, 나무를 써는 일은 일어나지 않으니, 신경쓰지 않고, “나무를 썰어”라고 말해도 됩니다. 그리고, 중요한 비유가 빠졌는데, “언제든지, 톱이 들어오면 나무를 썬다”까지 얘기해야 됩니다. - Lazy한 스트림
또 다시, 톱으로 썰어 목재가 준비되면, 망치를 넘겨 “가구를 만들어”라고 할 수도 있지만, 역시나 목재나 망치를 주지 않고 “가구를 만들어”라고 먼저 해 놓아도 됩니다. 어차피 목재, 망치가 준비되지 않으면 “가구를 만들어”는 일어나지 않습니다. - Reactive Style
프로그래밍의 흐름이 어떤 이벤트 스트림에 의존하는 다른 이벤트 스트림을 만드는 식의 모양이 됩니다. 이렇게 이벤트에 (연쇄적으로) 반응react하는 흐름을 함수형으로 작성하는 걸 Functional Reactive Programming이라 합니다. Lazy한 특징들과 잘 맞아 떨어질 것 같은데, 아직 라이브러리 코드를 깊게 보지 못해 추측일 뿐입니다.
(아래 핵심 타입으로 Event
와 Behavior
가 나오는데 먼저 비유에 붙여 얘기하면, 스트림에 톱이나 망치가 들어 온 횟수를 센다거나 하는 사람이 있다면, 이 사람은 Behavior
로 볼 수 있습니다. 딱히 이벤트에 반응하는 핸들러 타입은 아닌데, 이벤트에 반응해서 바뀔 수 있는 값을 위한, 더 추상적으론 시간에 따라 바뀔 수 있는 값 개념으로 보입니다. 결과적으로 Event
스트림이나 시간이 raw한 값이라면, Behavior
는 이 raw한 값들을 가공해서 기억하고 있는 값처럼 보이기도 합니다.)
(Reflex에선 Event
, Behavior
, Dynamic
세 개를 핵심 타입으로 다루고 있습니다. @todo Daynamic
의미를 이해하게 내용 되면 추가할 것)
reactive-banana 프레임워크의 가장 기본적인 사용법은 아래와 같습니다.
do
<- newAddHandler -- 말만 먼저 할 수 있는 구조를 만들고,
(addHandler, fire) putStrLn -- 나무를 썰어라고 말 해놓고
register addHandler "Hello!" -- 톱을 나중에 넘깁니다. fire
프레임워크에 있는 구조 안에 함수를 넣어(register
) 두면, 이 함수에 필요한 인자를 구조의 인터페이스(fire
)로 전달해 비동기로 실행하고 있습니다.
실제 사용 예시를 보겠습니다.
HeinrichApfelmus/reactive-banana - SlotMachine.hs 소스를 가져와서 뜯어봤습니다.
그림 세 개가 일치하면, 돈이 쏟아지는 그 슬롯 머신 게임입니다.
main :: IO ()
= do
main
displayHelpMessage<- makeSources
sources <- compile $ networkDescription sources
network
actuate network eventLoop sources
reactive-banana는 이벤트 네트워크란 이름을 붙인 이벤트 그래프를 만들고networkDescription
, 컴파일compile
이라는 작업을 통해 리액티브로 돌아갈 구조를 준비합니다. 그리고 나서, 이 네트워크를 시작actuate
시키면 이벤트 관련 동작들이 시작됩니다. eventLoop
는 특별히 프레임워크 내부에 준비된 절차는 아니고, 외부의 일반적인 루프로 fire
핸들러를 통해 프레임워크와 소통합니다. 여기선, 이 정도까지만 보고 넘어가겠습니다. 핵심적인 아이디어 구현 코드를 보려고 라이브러리 코드 자체를 조금 뜯어 봤는데, 역시나 금방 눈에 들어오진 않습니다. 여기서는 우선 FRP가 뭔지를 알아내는 게 목표입니다.
displayHelpMessage :: IO ()
=
displayHelpMessage mapM_ putStrLn $
"-----------------------------"
: "- THE REACTIVE SLOT MACHINE -"
: "------ WIN A BANANA ---------"
: ""
: "Commands are:"
: " coin - inser a coin"
: " play - play one game"
: " quit - quit the program"
: ""
: []
makeSources = (,) <$> newAddHandler <*> newAddHandler
newAddHandler
는 나중에 함수(작업)를 받아, 이벤트 형태로 동작시키기 위한 틀을 만듭니다.
newAddHandler :: IO (AddHandler a, Handler a)
a
는 특별한 말이 없으니 forall a
입니다. a
와 무관하게, a
를 담을 구조만 만든다는 얘기입니다.
fire
를 담당할 핸들러를 만들어냅니다yield.(통지)type Handler a = a -> IO ()
핸들러는 이벤트로 발생한 값 a
를 받아 Computation을 하는 함수입니다.
AddHandler
는 핸들러를 등록, 통지 할 수 있는 타입입니다.
newtype AddHandler a = AddHandler { register :: Handler a -> IO (IO ()) }
Q. 안에 있는 함수는 핸들러를 받아
IO (IO ())
를 반환한다.IO ()
가 아니라IO (IO ())
다. 왜 그럴까? .
A. @todoMomentIO
와 연관 있는 듯 한데, 라이브러리 코드를 더 뜯어봐야 알겠습니다.
이제 서명의 뜻을 알았으니, 그대로 읽으면 newAddHandler
는 (AddHandler a, fire함수)
두 개의 정보를 반환합니다.
아래는 프레임워크의 일부가 아닌, 보통의 하스켈 루프입니다.
eventLoop :: (EventSource (), EventSource ()) -> IO ()
= loop
eventLoop (escoin, esplay) where
= do
loop putStr "> "
hFlush stdout<- getLine
s case s of
"coin" -> fire escoin () -- coin 이벤트 소스를 fire한다.
"play" -> fire esplay ()
"quit" -> return ()
-> putStrLn $ s ++ " - unknown command"
_ /= "quit") loop when (s
Event 소스라면 뭘 할 줄 알아야 할까요?
fire
시킬 수 있고,EventSource
는 핸들러 등록/통지 작업을 하는 AddHandler
와 fire
역할을 하는 함수를 가지고 있습니다.
type EventSource a = (AddHandler a, Handler a) -- 원 코드는 a -> IO () 로 되어 있다.
-- 두 번째 Handler는 이벤트를 `fire`를 하는 함수로, newAddHandler로 만들어진다.
-- fire도 핸들러로 볼 수 있긴 한데, Fire라는 synonym을 만들어 써도 좋겠다.
addHandler :: EventSource a -> AddHandler a
= fst
addHandler -- 2튜플의 첫 번째에 있는 "핸들러를 등록하는 함수"를 가진 AddHandler 가져오기
-- 정의만 보면, 마치 getHandler 작명이 어울릴 것 같은데,
-- 남은 코드를 더 읽어보면 add가 맞는 작명으로 보인다.
fire :: EventSource a -> Handler a -- 원 코드는 a -> IO () 로 되어 있다.
= snd fire
newAddHandler
가 반환한 값을 쓰기 편하게 이름 붙여 놓는 역할정도 하는 함수들입니다.
type Money = Int
type Reels = (Int, Int, Int) -- 릴테입할 때 릴
data Win = Double | Triple
networkDescription :: (EventSource (), EventSource ()) -> MomentIO ()
= mdo -- MomentIO의 mdo (RecursiveDo)
networkDescription (escoin, esplay) <- liftIO newStdGen
initialStdGen <- fromAddHandler (addHandler escoin)
ecoin ----------------------------------
-- MomentIO (Event a)
<- fromAddHandler (addHandler esplay) eplay
핸들러 등록 구조를 가진 AddHandler
타입에서 준비해 둔 이벤트 스트림을 가져 옵니다.
escoin
, esplay
는 이벤트 구조를 만들어 둔 것들로, 인자 타입 서명을 보면 EventSource ()
이므로 ()
를 담을 구조입니다. 이 구조 안의 스트림인 Event
타입을 fromAddHandler
로 가져옵니다.
fromAddHandler :: AddHandler a -> MomentIO (Event a)
이벤트 네트워크가 시작actuate되면, 호출될 때마다 이벤트를 발생시킬 콜백 함수를 이 함수가 등록합니다.
ecoin <- fromAddHandler (addHandler escoin)
를 말로 읽어 보면, escoin
이벤트 소스에서, 이벤트 등록/통지를 담당하는 AddHandler
를 꺼내 와서 MomentIO
로 감싼 값을 만듭니다. 이름대로, 특정 순간에 일어날 MomentIO
Computation을 뜻합니다. Computation이라 했으니, IO
모나드처럼 어떤 조건이 만족되면 값을 만들거라 예상할 수 있습니다. ecoin
의 실제 값은 언제 생길까요? 언젠가, 어디선가 fire
시키면 그 때 만들어질 값입니다.
data MomentIO a = MIO { unMIO :: Prim.Moment a}
이벤트 네트워크에 입,출력할 때 쓰입니다.
ecredits :: Event Money, bcredits :: Behavior Money) <-
(0 . fmap (\f x -> (f x, f x)) $
mapAccum
unions<$ ecoin, -- 언젠가 알게 될 coin값
[ addCredit <$ edoesplay,
removeCredit <$> ewin
addWin ]
unions
의 결과는 [ (+1)을 가진 이벤트, (+5)를 가진 이벤트, ...]
모양의 함수 스트림입니다. 게임 진행은 사용자 인터랙션에 따라 (+1)
,(+5)
,(-1)
… 등이 쌓여나갈 겁니다. 함수 스트림 값을 하나씩 credits
에 적용하며 0
이 되면 게임을 끝내야 합니다. mapAccum
이 받는 이벤트는 Event (acc -> (x, acc))
로, 결과 튜플의 두 번째 자리에 계속 값을 누적하며 기억할 수 있습니다. 이 값은 Behavior
로 기억합니다.
@todo 아직 제대로 mapAccum
을 해석하지 못했습니다. Event
, Behavior
개념을 더 자세히 추가하는 중입니다. - Sat Oct 7 03:29:47 PM KST 2023
Q. 이벤트 구조로 만들어 둔, 즉
Event
타입의ecoin
은()
를 담고 있다고 했는데,addCredit = (+1)
을 보면Number
여야 할 것럼 보인다?
A.(<$) :: a -> Event b -> Event a
안에 들어 있는 걸 바꾸는 컴비네이터입니다.'a' <$ Just 2 Just 'a' ghci> (+1) <$ [(),()] <*> [1,2] [2,3,2,3]
ecoin
이 가진 ()
를 (+ 1)
함수로 바꿔 놓고,
edoesplay
가 가진 ()
를 (- 1)
함수로 바꿔 놓고
ewin
이 가진 Double | Triple
에 따라 (+ 5)
, (+ 20)
로 바꿔 놓습니다.
의미만 보면(실제는 다름), [(발생시각,(+ 1)), (발생시각, (+ 5)), (발생시각, (- 1)) ...]
무한 리스트입니다.
예시 소스를 따라가며 기록하다 보니, 제일 먼저 나와야 하는 기본 개념이 지금 나왔습니다.
※ FRP의 Event와 Behavior 글에서 좀더 자세하게 정리 중입니다.
data Event a
newtype Event a = E { unE :: Prim.Event a }
Prim.Event
는 type Event a = Cached Moment (Pulse a)
로 정의되어 있는데, Moment
구현에 보면 ReaderT EventNetwork
가 보입니다. 뜻은 type Event a = [(Time, a)]
쯤으로 보면 된다하니, 라이브러리 소스 자체를 더 뜯어보기 전엔 이렇게만 알고 넘어 가야겠습니다. 발생 시간을 같이 가지고 있는 이벤트 튜플 스트림(무한 리스트)입니다. 이벤트 발생 시간순 시퀀스로 볼 수 있습니다.
@todo 단일 이벤트가 아닌, 이벤트 스트림이라 하여, 무한 리스트같은 재귀 모양이 나오고, 핸들러를 등록/해제 하는 함수가 불어 있을 거라 예상하며 코드를 뒤졌는데, 딱히 재귀 모양이 잘 드러나지 않습니다. (의심가는 부분을 한 곳 찾긴 했습니다.1)
IORef
들을 여기 저기 쓴 게 보이는데,MVar
는 안보입니다. 결국 블로킹을 통해 비동기 동작을 하려면MVar
가 보일 것만 같은데요. 코드를 이해하게 되면, 내용을 추가하도록 하겠습니다.
data Behavior a
type Behavior a = Cached Moment (Latch a, Pulse ())
로 정의되어 있고, 시간에 따라 변하는 값을 나타냅니다. 뜻은 type Behavior a = Time -> a
쯤으로 보면 됩니다. 가장 기본이 되는 시간에 따라 변하는 값은 time :: Behavior Time
입니다. (FRAN 논문의 주요 아이디어는, 시간에 따라 그림이 변하는, 즉 애니메이션을 Behavior Picture
라고 표현하고 있습니다.)
※ Event
와 Behavior
의 차이 2
Event - streams of times values : discrete 이벤트를 표현합니다. ex) 마우스 클릭은 어떤 순간에 일어나지만, 항상 일어나는 건(혹은 값이 있는 건) 아닙니다. (Behavior
가 연속적인 시간으로 표현되는 것과 대비해 보면 이산discrete 뜻을 알 수 있습니다.)
Behavior - time-varying values : ex) 마우스 좌표는 시간에 따라 변하고, 어떤 순간에도 값을 가집니다.
reactive-banana
에선 stepper
함수를 써서, Event
로 바뀐 값을 Behavior
가 의존해서, 가진 값을 바꿀 수 있지만, 그 반대로는 할 수 없습니다.
stackoverflow - FRP Event streams and Signals…에 흥미로운 설명이 있습니다. (아래는 답변 글에서 발췌)
Behavior a, Behavior b) ~ Behavior (a, b)
(Event a, Event b) ~ Event (EitherOrBoth a b) (
만일 Event
와 Behavior
를 하나로 합친다면, 위 상황이 깨진다는 설명입니다. Behavior
는 둘 다 모두 기억해야 하지만, Event
는 둘 모두를 살릴 수도 있지만, 둘 중 하나만 살려야 할 수도 있다는 말입니다. 무슨 말일까요?
Behavior
에는 change
개념이 없습니다. 그러니, 적어도 FRP 안에서는 Behavior
에는 반응react할 수 없습니다.
Event
, Behavior
두 타입과 MonadMoment
클래스로 여러 컴비네이터를 구현해 놨습니다.
※ 원래 안에 가지고 있는 값에 적용(의존)하면서 값을 바꾸면, 계속 바꿔 나갈테니 누적Accumulate이란 용어를 선택한 것 같습니다.
mapAccum:: MonadMoment m => acc -> Event (acc -> (x, acc)) -> m (Event x, Behavior acc)
AccumE
와 AccumB
를 합친 컴비네이션입니다.
accumE:: MonadMoment m => a -> Event (a -> a) -> m (Event a)
이벤트가 가진 값을 누적합니다. strict로 left scan하는 scanl'
와 비슷합니다. 초기값으로 시작해서, 언제든 이벤트가 발생하면 새로운 값을 emit합니다. 새 값은 이벤트에 들어있는 함수를 이전 값에 적용해서 계산합니다.
accumB:: MonadMoment m => a -> Event (a -> a) -> m (Behavior a)
Event
에 들어 있는 함수를 이벤트 발생하면 Behavior
가 가진 값에 적용합니다.
stepper:: MonadMoment m => a -> Event a -> m (Behavior a)
초기값 a
를 가지고 있고, Event a
스트림에 의존하는 Behavior
를 만듭니다. Event
가 fire
되면 Behavior
에 들어 있는 값은 Event
에 들어 있는 값으로 바뀝니다.
※ 값은 accumE
로 누적하고, stepper
를 써서 시간 의존time-varying값으로 변환할 수 있습니다.
unions:: [Event (a -> a)] -> Event (a -> a)
값이 함수인 이벤트 스트림 리스트를 받아 모두 붙여 하나의 스트림으로 만듭니다.
let addCredit = (+ 1)
= subtract 1
removeCredit Double = (+ 5)
addWin Triple = (+ 20) addWin
emayplay :: Event Bool
= (\credits _ -> credits > 0) <$> bcredits <@> eplay
emayplay
edoesplay :: Event ()
= () <$ filterE id emayplay
edoesplay
edenied :: Event ()
= () <$ filterE not emayplay edenied
eroll :: Event Reels, bstdgen :: Behavior StdGen) <-
($ roll <$> edoesplay
mapAccum initialStdGen let roll :: () -> StdGen -> (Reels, StdGen)
= ((z1, z2, z3), gen3)
roll () gen0 where
= randomR (1, 4)
random = random gen0
(z1, gen1) = random gen1
(z2, gen2) = random gen2 (z3, gen3)
랜덤값 3개를 만들어 3튜플 Reels
에 넣고, gen
값을 나중에 써야하니 결과로 같이 내보냅니다.
ewin :: Event Win
= fmap fromJust $ filterE isJust $ fmap checkWin eroll ewin
ewin
은 eroll
값이 정해지면 그 때 알 수 있습니다.
checkWin (z1, z2, z3)| length (nub [z1, z2, z3]) == 1 = Just Triple
| length (nub [z1, z2, z3]) == 2 = Just Double
| otherwise = Nothing
nub
은 중복 원소를 제거합니다. 세 개가 모두 같으면 Triple
, 두 개가 같으면 Double
입니다.
전체 동작을 요약해 보면,
coin
입력을 하면 ecoin
Event
가 fire
되고 스트림에는 (+1)
이 담기고,play
입력을 하면 eplay
Event
가 fire
되고,credits
가 0
이 아니라면, eplay
가 fire
되고, emayplay
를 거쳐 edoesplay
가 fire
되어 (-1)
이 담기고,credits
가 0
이라면, edenied
Event
가 fire
되고, Not enough
를 출력합니다.edoesplay
가 fire
되면 (랜덤, 랜덤, 랜덤)
값을 가질 eroll
Event
가 fire
되고,eroll
이 fire
되면 Just Triple | Just Double | Nothing
을 가진 ewin
이 fire
됩니다.$ putStrLn . showCredit <$> ecredits
reactimate -- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-- Event (IO ())
-- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-- MomentIO ()
$ putStrLn . showRoll <$> eroll
reactimate $ putStrLn . showWin <$> ewin
reactimate $ putStrLn "Not enough credits!" <$ edenied reactimate
reactimate :: Event (IO ()) -> MomentIO ()
Event
가 fire
되면, 언제든 Event
가 가진 IO
액션을 실행합니다.
eroll
이라면, eroll
이 가진 (랜덤, 랜덤, 랜덤)
에 putStrLn . showRoll
을 적용(<$>
)한 액션으로 바꾸고,ewin
이라면, ewin
이 가진 Double | Triple
(Nothing
은 filterE
가 걸렀습니다.)에 putStrLn . showWin
을 적용(<$>
)한 액션으로 바꾸고edenied
면, edenied
가 가진 ()
를 putStrLn "Not.."
으로 바꿉니다.eroll
, ewin
, edenied
들이 모두 평범한 값을 쓰는 모양과 다르지 않게 나오는게, 우아해 보이지 않나요?
아래 나머지는 그저 출력에 관한 문자열을 만드는 함수들입니다.
= "Credits: " ++ show money
showCredit money = "You rolled " ++ show z1 ++ show z2 ++ show z3
showRoll (z1, z2, z3) Double = "Wow, a double!"
showWin Triple = "Wowwowwow! A Triple! So awesome!" showWin
비동기 함수를 다루는 모양이 옵저버 패턴과 그다지 달라 보이지 않습니다. 옵저버 패턴과 다른 점이 뭘까요?
단일 이벤트가 아닌, 이벤트 스트림을 다루는 게 가장 큰 차이점으로 보입니다. 이런 저런 절차에서 조건이 맞으면 스트림에 있는 이벤트 자체를 바꾸어 놓고, 이렇게 바뀐 이벤트들에 연계되어 있는 작업들이 비동기로 돌아가는 모양입니다.
리액티브 프로그래밍은, 이벤트를 스트림으로 만들고, async
한 신호를 잡는데는 옵저버 패턴을 씁니다.
Q. 리액티브에서 이벤트 스트림을 조작하는 작업들은, 옵저버 패턴에선 옵저버단에서 해결할 수도 있지 않나?
A. @todo 할 수는 있지만, 복잡하기 때문에 RP가 나왔습니다.
Q. 스트림으로 만드는 이유는?
A. @todo - 아직 정확히 언급하는 자료를 못 찾았습니다. 다음은 추측입니다.
- 이벤트들의 시간 순서를 알기 쉽다. 먼저 일이난 이벤트가 먼저 들어 있을테니
- 언어가 Functional에 Lazy하면, 무한 스트림을 써서 우아하게 짤 수 있다.
- 두 스트림을 붙이거나, 필터링 하거나, (시간 순서는 유지한 채로) 예를 들어 E Int를 E String으로 변환한다거나 하는 것도 가능하다.
- 여러 이벤트에 Batch 작업을 먹일 수 있다. (@재경)
참고 - The introduction to Reactive Programming you’ve been missing @andrestaltz
Reactive도 하스켈(함수형?)으로 작성하면 (익숙하지 않은 것 투성이라) 토 나올 만큼 우아합니다.
= (,) <$> newAddHandler <*> newAddHandler makeSources
딱 2개의 이벤트로 시작해서 비동기로 돌아갈 구조를 만들고, 사용자 인터랙션을 통해 이벤트가 시작되면, 이벤트가 가진 값 자체를 바꾸거나, 새로운 이벤트로 연계합니다. Lazy함도 적절히 쓰이는 것 같기도 하고, 모나드 패턴도 딱 들어 맞는 걸로 보여서, 라이브러리 코드를 더 자세히 뜯어 보고싶은 욕구가 생기긴 합니다. 여기 코드만 봐서는 Functional과 찰떡처럼 보이긴 한데, 다른 비 함수형 구현은 보지 않아 얼마나 찰떡인지는 모르겠습니다.
Functional 구현 아이디어를 볼 때, Computation이란 언젠가 계산될 값이란 걸 염두하고 보면 도움이 되지 않을까 합니다. Computation들을 엮어서combine 표현하는 스타일에 익숙해져야 합니다. 여기선, 언젠가 값이 될 Computation인 MomentIO
뭉치를 만들고 있습니다. IO
모나드가 Realworld
를 흘리면 돌아가기 시작해서 결과가 나온다고 이해하고 있다면, MomentIO
는 언젠가 fire
가 실행되면 돌아가게 될 Computation입니다.
※ Conal Elliot 교수의 강의를 보면, FRP의 가장 중요한 아이디어로 연속된 시간과, Denotational Semantics를 뽑습니다.
FRP explanation using reactive-banana에서 발췌
eNewval :: Event t Int
bSet :: Behavior t Int
= stepper 0 eNewval bSet
eNewval
이벤트가 발생하면, eNewval
이 가지고 있는 값으로 bSet
의 값을 바꿔 놓습니다.
eUpdater :: Event t (Int -> Int)
bUpdated :: Behavior t Int
= accumB 0 eUpdater bUpdated
eUpdater
이벤트가 발생하면, eUpdater
가 가지고 있는 함수를 bUpdated
가 가진 값에 적용합니다.
예시를 보면 bSet
이 eNewval
에 의존하고, bUpdated
가 eUpdater
에 의존하고 있습니다. b~
는 e~
가 fire
되면 바뀌는 값입니다. 그런데, 다음처럼 되어 있는 소스를 보니, Event
와 Behavior
의 차이가 좀 흐릿해졌습니다.
type Octave = Int
data Pitch = PA | PB | PC | PD | PE | PF | PG
data Note = Note Octave Pitch
ePitch :: Event t Pitch
= (PA <$ filterE (=='a') eKey) `union` -- eKey에 리액트
ePitch PB <$ filterE (=='b') eKey) `union`
(...
PG <$ filterE (=='g') eKey) `union`
(
eOctChange :: Char -> Maybe (Octave -> Octave)
= case c of
eOctChange c '+' -> Just (+1)
'-' -> Just (-1)
-> Nothing
_ bOctave :: Behavior t Octave
= accumB - $ filterJust (eOctChange <$> ekey) -- eKey에 리액트
bOctave
bPitch :: Behavior t Pitch
= stepper PC ePitch -- ePitch에 리액트
bPitch
bNote :: Behavior t Note
= Note <$> bOctave <*> bPitch -- bOctave, bPitch에 리액트? 의존?
bNote
-- 그냥 보통의 함수, 값 처럼 쓰고 있는데, 어떻게 이렇게 쓸 수 있는가가 궁금하다면,
-- filterE, union, stepper, accumB, <$>, <*> 들은
-- Event, Behavior를 다루기 위해 새로 정의된 함수들임을 생각해 봅시다.
bNote
가 bOcatave
와 bPitch
에 의존합니다. Behavior
는 Event
뿐만 아니라 Behavior
에 의존할 수도 있습니다. 그냥 값이니 그럴 수 있는 게 당연해 보이기도 합니다. Event
와 Behavior
가 섞여 체이닝 되는 모양을 보면, Behavior
는 fire
란 인터페이스가 없어 체이닝의 첫 부분(외부에서 들어오는 이벤트를 받아 낼 자리)에는 두지 못하지만, 그 후로는 Event
를 써도 Behavior
를 써도 될 것처럼 보이지만 둘은 다음과 같은 차이가 있습니다.
(실제 구현을 봐도 비슷합니다.)
type Behavior a = Cached Moment (Latch a, Pulse ())
type Event a = Cached Moment (Pulse a)
-- 시간에 따른 이벤트 스트림과 관계 있다.
data Pulse a = Pulse
_key :: Lazy.Key (Maybe a) -- 캐시에서 pulse값을 받아오는 키
{ _nodeP :: SomeNode -- Reference to its own node
,
}
type SomeNode = Ref.Ref SomeNodeD
data SomeNodeD
= forall a. P (PulseD a)
| L LatchWriteD
| O OutputD
data PulseD a = PulseD
_keyP :: Lazy.Key (Maybe a) -- 캐시에서 pulse를 받아 오는 키
{ _seenP :: !Time -- See note [Timestamp].
, _evalP :: EvalP (Maybe a) -- 현재 값 계산
, _nameP :: String -- 디버깅에 쓸 이름
,
}data LatchWriteD = forall a. LatchWriteD
_evalLW :: EvalP a -- 기록할 값 계산
{ _latchLW :: Weak (Latch a) -- Destination 'Latch' to write to.
, }
둘의 차이인 Latch a
(영단어 뜻: 걸쇠)를 보면,
-- Behavior의 초기값을 설정할 때 쓴다.
-- Latch값은 다른 이벤트가 바꿔 놓을 수 있다.
type Latch a = Ref.Ref (LatchD a) -- 타임스탬프와 함께 현재 값을 IORef에 저장
data LatchD a = Latch
_seenL :: !Time -- 현재 값의 타임스탬프
{ _valueL :: a -- 현재 값
, _evalL :: EvalL a -- 현재 latch값을 다시 계산
,
}
data Ref a = Ref
!Unique -- Unique associated to the 'Ref'
!(IORef a) -- 'a'를 'IORef'에 저장
!(WeakRef a) -- For convenience, a weak pointer to itself
Behavior
는 Latch
값을 가지고 있습니다. 이 게 뭘 뜻할까요?
Behavior
는 이벤트 네트워크 내에서 기억장소처럼 동작하고 있습니다.
※ 전기 회로에서 Pulse
는 짧은 시간 동안 들어가는 신호를 의미하고, Latch
는 입력 신호를 기반으로 상태를 유지하는 논리 게이트를 뜻합니다. (왜 기억하는 걸 Latch
라 부를까요? 정보를 고정한다 그런 걸까요?)
Pulse
를 대충 Event
로 보고,
Latch
를 대충 Behavior
로 볼 수 있다 합니다.
stackoverflow - what are latch and pulse in reactive banana
Behavior
는 Event
와는 다르게 값을 “기억latch”합니다.
※ @todo - 그런데, Event
가 스트림으로 구현된 걸 못찾겠습니다. 재귀 요소가 잘 안 보이는데, (아직 해석은 안되는데), 아래 경로에서 []
를 찾았습니다. 이 걸로 스트림이 표현되지 않을까 추측 중입니다.
type Event a = Cached Moment (Pulse a)
^^^^^^
type Moment = ReaderT EventNetwork Prim.Build
^^^^^^^^^^
type Build = ReaderWriterIOT BuildR BuildW IO
^^^^^^
newtype BuildW = BuildW (DependencyChanges, [Output], Action, Maybe (Build ()))
Moment
-> Prim.Build(ReaderWriterIOT)
-> BuildW
-> [Output]
핵심만 읽어보면 Moment
는 IORef
에 [Output]
를 넣어 놓는 타입입니다.↩︎
FRP explanation using reactive-banana에서 발췌
eNewval :: Event t Int
bSet :: Behavior t Int
= stepper 0 eNewval bSet
eNewval
이벤트가 발생하면, eNewval
이 가지고 있는 값으로 bSet
의 값을 바꿔 놓습니다.
eUpdater :: Event t (Int -> Int)
bUpdated :: Behavior t Int
= accumB 0 eUpdater bUpdated
eUpdater
이벤트가 발생하면, eUpdater
가 가지고 있는 함수를 bUpdated
가 가진 값에 적용합니다.
예시를 보면 bSet
이 eNewval
에 의존하고, bUpdated
가 eUpdater
에 의존하고 있습니다. b~
는 e~
가 fire
되면 바뀌는 값입니다. 그런데, 다음처럼 되어 있는 소스를 보니, Event
와 Behavior
의 차이가 좀 흐릿해졌습니다.
type Octave = Int
data Pitch = PA | PB | PC | PD | PE | PF | PG
data Note = Note Octave Pitch
ePitch :: Event t Pitch
= (PA <$ filterE (=='a') eKey) `union` -- eKey에 리액트
ePitch PB <$ filterE (=='b') eKey) `union`
(...
PG <$ filterE (=='g') eKey) `union`
(
eOctChange :: Char -> Maybe (Octave -> Octave)
= case c of
eOctChange c '+' -> Just (+1)
'-' -> Just (-1)
-> Nothing
_ bOctave :: Behavior t Octave
= accumB - $ filterJust (eOctChange <$> ekey) -- eKey에 리액트
bOctave
bPitch :: Behavior t Pitch
= stepper PC ePitch -- ePitch에 리액트
bPitch
bNote :: Behavior t Note
= Note <$> bOctave <*> bPitch -- bOctave, bPitch에 리액트? 의존?
bNote
-- 그냥 보통의 함수, 값 처럼 쓰고 있는데, 어떻게 이렇게 쓸 수 있는가가 궁금하다면,
-- filterE, union, stepper, accumB, <$>, <*> 들은
-- Event, Behavior를 다루기 위해 새로 정의된 함수들임을 생각해 봅시다.
bNote
가 bOcatave
와 bPitch
에 의존합니다. Behavior
는 Event
뿐만 아니라 Behavior
에 의존할 수도 있습니다. 그냥 값이니 그럴 수 있는 게 당연해 보이기도 합니다. Event
와 Behavior
가 섞여 체이닝 되는 모양을 보면, Behavior
는 fire
란 인터페이스가 없어 체이닝의 첫 부분(외부에서 들어오는 이벤트를 받아 낼 자리)에는 두지 못하지만, 그 후로는 Event
를 써도 Behavior
를 써도 될 것처럼 보이지만 둘은 다음과 같은 차이가 있습니다.
(실제 구현을 봐도 비슷합니다.)
type Behavior a = Cached Moment (Latch a, Pulse ())
type Event a = Cached Moment (Pulse a)
-- 시간에 따른 이벤트 스트림과 관계 있다.
data Pulse a = Pulse
_key :: Lazy.Key (Maybe a) -- 캐시에서 pulse값을 받아오는 키
{ _nodeP :: SomeNode -- Reference to its own node
,
}
type SomeNode = Ref.Ref SomeNodeD
data SomeNodeD
= forall a. P (PulseD a)
| L LatchWriteD
| O OutputD
data PulseD a = PulseD
_keyP :: Lazy.Key (Maybe a) -- 캐시에서 pulse를 받아 오는 키
{ _seenP :: !Time -- See note [Timestamp].
, _evalP :: EvalP (Maybe a) -- 현재 값 계산
, _nameP :: String -- 디버깅에 쓸 이름
,
}data LatchWriteD = forall a. LatchWriteD
_evalLW :: EvalP a -- 기록할 값 계산
{ _latchLW :: Weak (Latch a) -- Destination 'Latch' to write to.
, }
둘의 차이인 Latch a
(영단어 뜻: 걸쇠)를 보면,
-- Behavior의 초기값을 설정할 때 쓴다.
-- Latch값은 다른 이벤트가 바꿔 놓을 수 있다.
type Latch a = Ref.Ref (LatchD a) -- 타임스탬프와 함께 현재 값을 IORef에 저장
data LatchD a = Latch
_seenL :: !Time -- 현재 값의 타임스탬프
{ _valueL :: a -- 현재 값
, _evalL :: EvalL a -- 현재 latch값을 다시 계산
,
}
data Ref a = Ref
!Unique -- Unique associated to the 'Ref'
!(IORef a) -- 'a'를 'IORef'에 저장
!(WeakRef a) -- For convenience, a weak pointer to itself
Behavior
는 Latch
값을 가지고 있습니다. 이 게 뭘 뜻할까요?
Behavior
는 이벤트 네트워크 내에서 기억장소처럼 동작하고 있습니다.
※ 전기 회로에서 Pulse
는 짧은 시간 동안 들어가는 신호를 의미하고, Latch
는 입력 신호를 기반으로 상태를 유지하는 논리 게이트를 뜻합니다. (왜 기억하는 걸 Latch
라 부를까요? 정보를 고정한다 그런 걸까요?)
Pulse
를 대충 Event
로 보고,
Latch
를 대충 Behavior
로 볼 수 있다 합니다.
stackoverflow - what are latch and pulse in reactive banana
Behavior
는 Event
와는 다르게 값을 “기억latch”합니다.
※ @todo - 그런데, Event
가 스트림으로 구현된 걸 못찾겠습니다. 재귀 요소가 잘 안 보이는데, (아직 해석은 안되는데), 아래 경로에서 []
를 찾았습니다. 이 걸로 스트림이 표현되지 않을까 추측 중입니다.
type Event a = Cached Moment (Pulse a)
^^^^^^
type Moment = ReaderT EventNetwork Prim.Build
^^^^^^^^^^
type Build = ReaderWriterIOT BuildR BuildW IO
^^^^^^
newtype BuildW = BuildW (DependencyChanges, [Output], Action, Maybe (Build ()))
Moment
-> Prim.Build(ReaderWriterIOT)
-> BuildW
-> [Output]
핵심만 읽어보면 Moment
는 IORef
에 [Output]
를 넣어 놓는 타입입니다.↩︎