reactive-banana 예시로 FRP 동작 방식 이해해 보기 (작성 중)

Posted on October 7, 2023

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한 특징들과 잘 맞아 떨어질 것 같은데, 아직 라이브러리 코드를 깊게 보지 못해 추측일 뿐입니다.

(아래 핵심 타입으로 EventBehavior가 나오는데 먼저 비유에 붙여 얘기하면, 스트림에 톱이나 망치가 들어 온 횟수를 센다거나 하는 사람이 있다면, 이 사람은 Behavior로 볼 수 있습니다. 딱히 이벤트에 반응하는 핸들러 타입은 아닌데, 이벤트에 반응해서 바뀔 수 있는 값을 위한, 더 추상적으론 시간에 따라 바뀔 수 있는 값 개념으로 보입니다. 결과적으로 Event 스트림이나 시간이 raw한 값이라면, Behavior는 이 raw한 값들을 가공해서 기억하고 있는 값처럼 보이기도 합니다.)

(Reflex에선 Event, Behavior, Dynamic 세 개를 핵심 타입으로 다루고 있습니다. @todo Daynamic 의미를 이해하게 내용 되면 추가할 것)

register, fire!

reactive-banana 프레임워크의 가장 기본적인 사용법은 아래와 같습니다.

do
  (addHandler, fire) <- newAddHandler -- 말만 먼저 할 수 있는 구조를 만들고,
  register addHandler putStrLn -- 나무를 썰어라고 말 해놓고
  fire "Hello!" -- 톱을 나중에 넘깁니다.

프레임워크에 있는 구조 안에 함수를 넣어(register) 두면, 이 함수에 필요한 인자를 구조의 인터페이스(fire)로 전달해 비동기로 실행하고 있습니다.

실제 사용 예시를 보겠습니다.
HeinrichApfelmus/reactive-banana - SlotMachine.hs 소스를 가져와서 뜯어봤습니다.
그림 세 개가 일치하면, 돈이 쏟아지는 그 슬롯 머신 게임입니다.

main :: IO ()
main = do
  displayHelpMessage
  sources <- makeSources
  network <- compile $ networkDescription sources
  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"
      : ""
      : []

비동기로 실행될 구조 - AddHandler 타입

makeSources = (,) <$> newAddHandler <*> newAddHandler

newAddHandler는 나중에 함수(작업)를 받아, 이벤트 형태로 동작시키기 위한 틀을 만듭니다.
newAddHandler :: IO (AddHandler a, Handler a) a는 특별한 말이 없으니 forall a입니다. a와 무관하게, a를 담을 구조만 만든다는 얘기입니다.

type Handler a = a -> IO () 핸들러는 이벤트로 발생한 값 a를 받아 Computation을 하는 함수입니다.

AddHandler는 핸들러를 등록, 통지 할 수 있는 타입입니다.
newtype AddHandler a = AddHandler { register :: Handler a -> IO (IO ()) }

Q. 안에 있는 함수는 핸들러를 받아 IO (IO ())를 반환한다. IO ()가 아니라 IO (IO ())다. 왜 그럴까? .
A. @todo MomentIO와 연관 있는 듯 한데, 라이브러리 코드를 더 뜯어봐야 알겠습니다.

이제 서명의 뜻을 알았으니, 그대로 읽으면 newAddHandler(AddHandler a, fire함수) 두 개의 정보를 반환합니다.

아래는 프레임워크의 일부가 아닌, 보통의 하스켈 루프입니다.

eventLoop :: (EventSource (), EventSource ()) -> IO ()
eventLoop (escoin, esplay) = loop
  where
    loop = do
      putStr "> "
      hFlush stdout
      s <- getLine
      case s of
        "coin" -> fire escoin () -- coin 이벤트 소스를 fire한다.
        "play" -> fire esplay ()
        "quit" -> return ()
        _ -> putStrLn $ s ++ " - unknown command"
      when (s /= "quit") loop

이벤트 소스

Event 소스라면 뭘 할 줄 알아야 할까요?

EventSource는 핸들러 등록/통지 작업을 하는 AddHandlerfire 역할을 하는 함수를 가지고 있습니다.

type EventSource a = (AddHandler a, Handler a) -- 원 코드는 a -> IO () 로 되어 있다.
-- 두 번째 Handler는 이벤트를 `fire`를 하는 함수로, newAddHandler로 만들어진다.
-- fire도 핸들러로 볼 수 있긴 한데, Fire라는 synonym을 만들어 써도 좋겠다.

addHandler :: EventSource a -> AddHandler a
addHandler = fst 
-- 2튜플의 첫 번째에 있는 "핸들러를 등록하는 함수"를 가진 AddHandler 가져오기
-- 정의만 보면, 마치 getHandler 작명이 어울릴 것 같은데,
-- 남은 코드를 더 읽어보면 add가 맞는 작명으로 보인다.

fire :: EventSource a -> Handler a -- 원 코드는 a -> IO () 로 되어 있다.
fire = snd

newAddHandler가 반환한 값을 쓰기 편하게 이름 붙여 놓는 역할정도 하는 함수들입니다.

type Money = Int
type Reels = (Int, Int, Int) -- 릴테입할 때 릴
data Win = Double | Triple

networkDescription :: (EventSource (), EventSource ()) -> MomentIO ()
networkDescription (escoin, esplay) = mdo -- MomentIO의 mdo (RecursiveDo)
  initialStdGen <- liftIO newStdGen
  ecoin <- fromAddHandler (addHandler escoin)
           ----------------------------------
--                 MomentIO (Event a)
  eplay <- fromAddHandler (addHandler esplay)

핸들러 등록 구조를 가진 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}
이벤트 네트워크에 입,출력할 때 쓰입니다.

mapAccum

  (ecredits :: Event Money, bcredits :: Behavior Money) <-
    mapAccum 0 . fmap (\f x -> (f x, f x)) $ 
      unions
        [ addCredit <$ ecoin, -- 언젠가 알게 될 coin값
          removeCredit <$ edoesplay,
          addWin <$> ewin
        ]

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)) ...] 무한 리스트입니다. 

Event, Behavior

예시 소스를 따라가며 기록하다 보니, 제일 먼저 나와야 하는 기본 개념이 지금 나왔습니다.
FRP의 Event와 Behavior 글에서 좀더 자세하게 정리 중입니다.

data Event a
newtype Event a = E { unE :: Prim.Event a }
Prim.Eventtype 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라고 표현하고 있습니다.)

EventBehavior의 차이 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)

만일 EventBehavior를 하나로 합친다면, 위 상황이 깨진다는 설명입니다. Behavior는 둘 다 모두 기억해야 하지만, Event는 둘 모두를 살릴 수도 있지만, 둘 중 하나만 살려야 할 수도 있다는 말입니다. 무슨 말일까요?

Behavior에는 change 개념이 없습니다. 그러니, 적어도 FRP 안에서는 Behavior에는 반응react할 수 없습니다.

컴비네이터

Event, Behavior 두 타입과 MonadMoment 클래스로 여러 컴비네이터를 구현해 놨습니다.

※ 원래 안에 가지고 있는 값에 적용(의존)하면서 값을 바꾸면, 계속 바꿔 나갈테니 누적Accumulate이란 용어를 선택한 것 같습니다.

mapAccum:: MonadMoment m => acc -> Event (acc -> (x, acc)) -> m (Event x, Behavior acc) AccumEAccumB를 합친 컴비네이션입니다.

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를 만듭니다. Eventfire되면 Behavior에 들어 있는 값은 Event에 들어 있는 값으로 바뀝니다.
※ 값은 accumE로 누적하고, stepper를 써서 시간 의존time-varying값으로 변환할 수 있습니다.

unions:: [Event (a -> a)] -> Event (a -> a)
값이 함수인 이벤트 스트림 리스트를 받아 모두 붙여 하나의 스트림으로 만듭니다.

  let addCredit = (+ 1) 
      removeCredit = subtract 1
      addWin Double = (+ 5)
      addWin Triple = (+ 20)
      emayplay :: Event Bool
      emayplay = (\credits _ -> credits > 0) <$> bcredits <@> eplay

      edoesplay :: Event ()
      edoesplay = () <$ filterE id emayplay

      edenied :: Event ()
      edenied = () <$ filterE not emayplay
  (eroll :: Event Reels, bstdgen :: Behavior StdGen) <-
    mapAccum initialStdGen $ roll <$> edoesplay
  let roll :: () -> StdGen -> (Reels, StdGen)
      roll () gen0 = ((z1, z2, z3), gen3)
        where
          random = randomR (1, 4)
          (z1, gen1) = random gen0
          (z2, gen2) = random gen1
          (z3, gen3) = random gen2

랜덤값 3개를 만들어 3튜플 Reels에 넣고, gen값을 나중에 써야하니 결과로 같이 내보냅니다.

      ewin :: Event Win
      ewin = fmap fromJust $ filterE isJust $ fmap checkWin eroll

ewineroll값이 정해지면 그 때 알 수 있습니다.

      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입니다.

연쇄적인 이벤트 Reactive!

전체 동작을 요약해 보면,

  1. coin 입력을 하면 ecoin Eventfire되고 스트림에는 (+1)이 담기고,
  2. play 입력을 하면 eplay Eventfire되고,
  3. credits0이 아니라면, eplayfire되고, emayplay를 거쳐 edoesplayfire되어 (-1)이 담기고,
  4. credits0이라면, edenied Eventfire되고, Not enough를 출력합니다.
  5. edoesplayfire되면 (랜덤, 랜덤, 랜덤) 값을 가질 eroll Eventfire되고,
  6. erollfire되면 Just Triple | Just Double | Nothing을 가진 ewinfire됩니다.
  reactimate $ putStrLn . showCredit <$> ecredits
--             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
--                        Event (IO ())
-- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
--                   MomentIO ()
  reactimate $ putStrLn . showRoll <$> eroll
  reactimate $ putStrLn . showWin <$> ewin
  reactimate $ putStrLn "Not enough credits!" <$ edenied

reactimate :: Event (IO ()) -> MomentIO ()
Eventfire되면, 언제든 Event가 가진 IO 액션을 실행합니다.

eroll, ewin, edenied들이 모두 평범한 값을 쓰는 모양과 다르지 않게 나오는게, 우아해 보이지 않나요?

아래 나머지는 그저 출력에 관한 문자열을 만드는 함수들입니다.

showCredit money = "Credits: " ++ show money
showRoll (z1, z2, z3) = "You rolled " ++ show z1 ++ show z2 ++ show z3
showWin Double = "Wow, a double!"
showWin Triple = "Wowwowwow! A Triple! So awesome!"

RP (Reactive Programming)

비동기 함수를 다루는 모양이 옵저버 패턴과 그다지 달라 보이지 않습니다. 옵저버 패턴과 다른 점이 뭘까요?
단일 이벤트가 아닌, 이벤트 스트림을 다루는 게 가장 큰 차이점으로 보입니다. 이런 저런 절차에서 조건이 맞으면 스트림에 있는 이벤트 자체를 바꾸어 놓고, 이렇게 바뀐 이벤트들에 연계되어 있는 작업들이 비동기로 돌아가는 모양입니다.

리액티브 프로그래밍은, 이벤트를 스트림으로 만들고, async한 신호를 잡는데는 옵저버 패턴을 씁니다.

Q. 리액티브에서 이벤트 스트림을 조작하는 작업들은, 옵저버 패턴에선 옵저버단에서 해결할 수도 있지 않나?
A. @todo 할 수는 있지만, 복잡하기 때문에 RP가 나왔습니다.

Q. 스트림으로 만드는 이유는?
A. @todo - 아직 정확히 언급하는 자료를 못 찾았습니다. 다음은 추측입니다.

참고 - The introduction to Reactive Programming you’ve been missing @andrestaltz

FRP (Functional React Programming)

Reactive도 하스켈(함수형?)으로 작성하면 (익숙하지 않은 것 투성이라) 토 나올 만큼 우아합니다.

makeSources = (,) <$> newAddHandler <*> newAddHandler

딱 2개의 이벤트로 시작해서 비동기로 돌아갈 구조를 만들고, 사용자 인터랙션을 통해 이벤트가 시작되면, 이벤트가 가진 값 자체를 바꾸거나, 새로운 이벤트로 연계합니다. Lazy함도 적절히 쓰이는 것 같기도 하고, 모나드 패턴도 딱 들어 맞는 걸로 보여서, 라이브러리 코드를 더 자세히 뜯어 보고싶은 욕구가 생기긴 합니다. 여기 코드만 봐서는 Functional과 찰떡처럼 보이긴 한데, 다른 비 함수형 구현은 보지 않아 얼마나 찰떡인지는 모르겠습니다.

Functional 구현 아이디어를 볼 때, Computation이란 언젠가 계산될 값이란 걸 염두하고 보면 도움이 되지 않을까 합니다. Computation들을 엮어서combine 표현하는 스타일에 익숙해져야 합니다. 여기선, 언젠가 값이 될 Computation인 MomentIO 뭉치를 만들고 있습니다. IO모나드가 Realworld를 흘리면 돌아가기 시작해서 결과가 나온다고 이해하고 있다면, MomentIO는 언젠가 fire가 실행되면 돌아가게 될 Computation입니다.

Conal Elliot 교수의 강의를 보면, FRP의 가장 중요한 아이디어로 연속된 시간과, Denotational Semantics를 뽑습니다.


  1. FRP explanation using reactive-banana에서 발췌

    eNewval :: Event t Int
    bSet :: Behavior t Int
    bSet = stepper 0 eNewval 

    eNewval 이벤트가 발생하면, eNewval이 가지고 있는 값으로 bSet의 값을 바꿔 놓습니다.

    eUpdater :: Event t (Int -> Int)
    bUpdated :: Behavior t Int
    bUpdated = accumB 0 eUpdater

    eUpdater 이벤트가 발생하면, eUpdater가 가지고 있는 함수를 bUpdated가 가진 값에 적용합니다.

    예시를 보면 bSeteNewval에 의존하고, bUpdatedeUpdater에 의존하고 있습니다. b~e~fire되면 바뀌는 값입니다. 그런데, 다음처럼 되어 있는 소스를 보니, EventBehavior의 차이가 좀 흐릿해졌습니다.

    type Octave = Int
    data Pitch = PA | PB | PC | PD | PE | PF | PG
    data Note = Note Octave Pitch
    
    ePitch :: Event t Pitch
    ePitch = (PA <$ filterE (=='a') eKey) `union` -- eKey에 리액트
             (PB <$ filterE (=='b') eKey) `union`
             ...
             (PG <$ filterE (=='g') eKey) `union`
    
    eOctChange :: Char -> Maybe (Octave -> Octave)
    eOctChange c = case c of
                    '+' -> Just (+1)
                    '-' -> Just (-1)
                     _  -> Nothing
    bOctave :: Behavior t Octave 
    bOctave = accumB - $ filterJust (eOctChange <$> ekey) -- eKey에 리액트
    
    bPitch :: Behavior t Pitch
    bPitch = stepper PC ePitch -- ePitch에 리액트 
    
    bNote :: Behavior t Note
    bNote = Note <$> bOctave <*> bPitch -- bOctave, bPitch에 리액트? 의존?
    
    -- 그냥 보통의 함수, 값 처럼 쓰고 있는데, 어떻게 이렇게 쓸 수 있는가가 궁금하다면,
    -- filterE, union, stepper, accumB, <$>, <*> 들은 
    -- Event, Behavior를 다루기 위해 새로 정의된 함수들임을 생각해 봅시다.

    bNotebOcatavebPitch에 의존합니다. BehaviorEvent뿐만 아니라 Behavior에 의존할 수도 있습니다. 그냥 값이니 그럴 수 있는 게 당연해 보이기도 합니다. EventBehavior가 섞여 체이닝 되는 모양을 보면, Behaviorfire란 인터페이스가 없어 체이닝의 첫 부분(외부에서 들어오는 이벤트를 받아 낼 자리)에는 두지 못하지만, 그 후로는 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

    BehaviorLatch값을 가지고 있습니다. 이 게 뭘 뜻할까요?
    Behavior는 이벤트 네트워크 내에서 기억장소처럼 동작하고 있습니다.

    ※ 전기 회로에서 Pulse는 짧은 시간 동안 들어가는 신호를 의미하고, Latch는 입력 신호를 기반으로 상태를 유지하는 논리 게이트를 뜻합니다. (왜 기억하는 걸 Latch라 부를까요? 정보를 고정한다 그런 걸까요?)

    Pulse를 대충 Event로 보고,
    Latch를 대충 Behavior로 볼 수 있다 합니다.
    stackoverflow - what are latch and pulse in reactive banana

    BehaviorEvent와는 다르게 값을 “기억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]
    핵심만 읽어보면 MomentIORef[Output]를 넣어 놓는 타입입니다.↩︎

  2. FRP explanation using reactive-banana에서 발췌

    eNewval :: Event t Int
    bSet :: Behavior t Int
    bSet = stepper 0 eNewval 

    eNewval 이벤트가 발생하면, eNewval이 가지고 있는 값으로 bSet의 값을 바꿔 놓습니다.

    eUpdater :: Event t (Int -> Int)
    bUpdated :: Behavior t Int
    bUpdated = accumB 0 eUpdater

    eUpdater 이벤트가 발생하면, eUpdater가 가지고 있는 함수를 bUpdated가 가진 값에 적용합니다.

    예시를 보면 bSeteNewval에 의존하고, bUpdatedeUpdater에 의존하고 있습니다. b~e~fire되면 바뀌는 값입니다. 그런데, 다음처럼 되어 있는 소스를 보니, EventBehavior의 차이가 좀 흐릿해졌습니다.

    type Octave = Int
    data Pitch = PA | PB | PC | PD | PE | PF | PG
    data Note = Note Octave Pitch
    
    ePitch :: Event t Pitch
    ePitch = (PA <$ filterE (=='a') eKey) `union` -- eKey에 리액트
             (PB <$ filterE (=='b') eKey) `union`
             ...
             (PG <$ filterE (=='g') eKey) `union`
    
    eOctChange :: Char -> Maybe (Octave -> Octave)
    eOctChange c = case c of
                    '+' -> Just (+1)
                    '-' -> Just (-1)
                     _  -> Nothing
    bOctave :: Behavior t Octave 
    bOctave = accumB - $ filterJust (eOctChange <$> ekey) -- eKey에 리액트
    
    bPitch :: Behavior t Pitch
    bPitch = stepper PC ePitch -- ePitch에 리액트 
    
    bNote :: Behavior t Note
    bNote = Note <$> bOctave <*> bPitch -- bOctave, bPitch에 리액트? 의존?
    
    -- 그냥 보통의 함수, 값 처럼 쓰고 있는데, 어떻게 이렇게 쓸 수 있는가가 궁금하다면,
    -- filterE, union, stepper, accumB, <$>, <*> 들은 
    -- Event, Behavior를 다루기 위해 새로 정의된 함수들임을 생각해 봅시다.

    bNotebOcatavebPitch에 의존합니다. BehaviorEvent뿐만 아니라 Behavior에 의존할 수도 있습니다. 그냥 값이니 그럴 수 있는 게 당연해 보이기도 합니다. EventBehavior가 섞여 체이닝 되는 모양을 보면, Behaviorfire란 인터페이스가 없어 체이닝의 첫 부분(외부에서 들어오는 이벤트를 받아 낼 자리)에는 두지 못하지만, 그 후로는 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

    BehaviorLatch값을 가지고 있습니다. 이 게 뭘 뜻할까요?
    Behavior는 이벤트 네트워크 내에서 기억장소처럼 동작하고 있습니다.

    ※ 전기 회로에서 Pulse는 짧은 시간 동안 들어가는 신호를 의미하고, Latch는 입력 신호를 기반으로 상태를 유지하는 논리 게이트를 뜻합니다. (왜 기억하는 걸 Latch라 부를까요? 정보를 고정한다 그런 걸까요?)

    Pulse를 대충 Event로 보고,
    Latch를 대충 Behavior로 볼 수 있다 합니다.
    stackoverflow - what are latch and pulse in reactive banana

    BehaviorEvent와는 다르게 값을 “기억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]
    핵심만 읽어보면 MomentIORef[Output]를 넣어 놓는 타입입니다.↩︎

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