얌파 Yampa (작성 예정)

Posted on October 29, 2023

Tue Nov 14 08:08:18 PM KST 2023 스케치 중…

ivanperez-keera / Yampa
The Yampa Arcade
ivanperez-keera / SpaceInvaders
Yampa Book

Arrowized FRP

Arrow 인터페이스를 이용해서 FRP 프레임워크 구현

Signal, SF (Signal Function)

얌파는 두 가지 핵심 개념을 기반으로 만들었다.

-- 시그널 
Signal α ≃ Time -> α

-- 시그널 함수 
SF α β ≃ Signal α -> Signal β

Reactive-Banana에서는 Conal Elliot 논문과 같은 Behavior (t -> a)와 Event ((t,a))에서 시작했다. 얌파는 “특정 시간에만 존재하는 Event” 즉, (Time, Event) 모양이 없다. Behavior와 Event를 최대한 비슷하게 표현(모델링)한다면,

Behavior = [b, b, b, b, b, ...] -- [ Just b,  Just b, Just b,  Just b, ...]
Event    = [_, _, e, _, e, ...] -- [Nothing, Nothing, Just e, Nothing, ...]

이런 모양으로 표현할 수도 있다. 아마도 얌파에선 이런 방식으로, Signal 하나로 둘 모두를 표현하지 않았을까?
( ※ 연속 시간을 음이 아닌 실수로 표현한다. )
SF 해석을 어떻게 하면 좋을까? Signal α란 걸 주면 Signal β로 바꿔놓는 함수다. Time을 주면 α를 내어 줄 준비를 하고 있었는데, 이를 변환 β를 내어주게 변환한다. 어떻게 변환할지는 이를 받아가는 모피즘, 즉 런타임의 동작을 봐야 한다.

(Signal α, Signal β)

위와 같이 쓴다면, (,)는 이를 만나는 모피즘들이 모두 별 일을 하지 않는다. 그저 단순히 두 개의 정보를 “함께” 가지고 있다는 뜻이다. SF(,)자리에 (->)를 넣어 정의했다. (->) (Signal α) (Signal β), 나중 SF와 만나는 Runner들이 이 걸 실행하든, 다른 것과 합성하든 할 것이다. 뭘 할지는 그들에게 달렸다.
프로그램 플로우 차트로 비유하자면, 마치 라인이 Signal이고, 박스 요소들이 SF로 볼 수 있다고 한다. SF는 어찌 됐든 함수를 래핑해 놨으니, SF만을 위한 컴비네이터들을 만들어 쓸 거라고 예측할 수 있다.

arr

SF(t -> a) -> (t -> b)타입의 함수를 래핑한 타입이다. (a -> b) 함수를 SF로 만들어 보자. 서명 모양이 딱 요네다 임베딩이다. > Nat(Hom(-,a), Hom(-,b)) ~= (Hom(-,b)) a
> 모피즘 모양으로 다시 쓰면 (x -> a) -> (x -> b) ~= a -> b

f :: a -> b 함수를 받아서, 어떻게 SF{ (s :: t -> a) -> (h :: t -> b) } 로 바꿀까? 요네다를 안다면 둘은 같은 정보를 가지고 있다는 건 바로 보이긴 하는데, 실제 구현은 어떻게 될까?

함수를 변환하는 연습을 해보자.

  1. 일단 a를 결정하려면 st를 넣어줘야 한다.
    a = s t
  2. a를 결정했으니, f를 적용할 수 있다.
    b = f (s t)
  3. b가 나왔다. 최종 결과는 t를 받아서 b를 돌려주는 함수다.
    \t -> f (s t)
  4. 이제 마지막으로 s를 받도록 만들면 된다.
    \s -> \t -> f (s t)

The Yampa Arcade - 3.2 Composing Signal Functions 참고

arr :: (a -> b) -> SF a b
arr f = \s -> \t -> f (s t)

이제 일반 함수를 arr을 써서 SF세상으로 리프팅할 수 있다. 요네다하고도 맞아 떨어지고, a -> b라는 함수의 의미 - a가 먼저 일어나면, 그 것에 의존해서 b가 일어난다는 직관하고도 맞아 떨어진다. 요네다 식 자체가 가지고 있는 뜻이, 원래부터 이 의미로 보인다. 마치 시간의 개념이 없던 수학식에 시간 개념을 불어 넣는 느낌이다. 이제 막 요네다를 공부해서 어설프게 다 적용하고 있는지도 모르겠다. 직관적으로 해석하면, 시간에 따라 a가 일어나는 함수를 받아서 b가 일어나는 함수로 바꿔 놓았다.

함수가 들어있는 데이터 타입의 펑크터 만들기 참고


FRP in Yampa: Part1 - Reasonably Polymorphic

controller :: SF () Controller

첫번 째 인자 ()는, 컨트롤러 값을 얻기 위해서 아무 것도 넣어 줄 필요가 없다는 뜻.

얌파 프리미티브 중에 하나 (delay)
(>>>) SF 합성 컴비네이터

aState :: SF () Bool
aStateTwoSecondsAgo :: SF () Bool

integral :: (Fractional s, VectorSpace a s) => SF a a

integral: rectangle 규칙을 써서 통합

transformer 우리말로 번역하면 변환기 정도 되겠다. SF는 연속 시그널 변환기다. 머릿속이 모호한 이유는 바로 “연속” 때문이다. 연속이란 개념을 어떻게 처리했을까? 무한 재귀 정의를 봤을 때와 마찬가지다. 무한하게 정의했다고, 무한하게 “동작”을 매 순간, CPU 틱을 소진하며 계속 동작 하고 있는 것이 아니다. 모든 대상들은 모피즘을 만나야 의미가 생긴다. 무한 정의들은 무한히 동작하지만, 어느 순간에, 혹은 어느 순간 까지만 필요하다는 모피즘을 반드시 파트너로 가지고 있다.
아마도 이 연속이란 개념도 그러할 것이다. 시그널 Time -> α는, 함수로 되어있다. 이 상태론 α를 알 수 없다. 원하는 시간을 넣어주는 파트너와 만날 것이다. 그 파트너가 언제, 어떤 Time값으로 찾아 올지 모르니, 특정 시간에만 존재하는 값이 아니라, 모든 시간에 대응하는 값이 있다라는 개념을 표현한다면, 함수가 제격이다.

data Event a
  = Event a
  | NoEvent
after :: Time -> b -> SF a (Event b)

시간과 b를 받아 시그널 함수를 돌려준다. 이 시그널 함수는 언젠가 t->a함수를 받아서, t->Event b로 변환한다. Event는 마치 Maybe와 비슷하다. Event가 있거나 없거나다. 5초 후에 알람을 맞춘다면, after 5 () :: SF a (Event ())로 두면 된다. Runner가 5가 되는지 검사하는 함수를 넣어 줄거라 추측할 수 있다.

edge :: SF Bool (Event ())

after 5 ()는 5초 후에 시그널 함수가 동작한다면, edge는 입력 Bool값이 바뀌면 시그널 함수를 동작시킨다.

hold :: a -> SF (Event a) a

SF 함수들을 >>>로 모두 연결해 놓으면서 프로세스가 완성된다. Runner가 이들을 받아서 어떤 식으로 정리해서 동작시킬지는 아직 설명하지 않는다. 그러니, SF 함수가 머릿속에서 모호하게 남아 있다. 실제 구현을 보여주진 않더라도, 어떤 아이디어가 동작할지는 얘기해줘야 하지 않을까?

혼자 상상해 보자. Runner는 어떤 일을 하게 될까? (Time -> α) -> (Time -> β) 뭉치를 받아서 뭘 하면 될까? 오른쪽 화살표가 눌리면, 그 것으로 끝이 아니다. 누군가는 그 걸 가져가 좌표값을 오른 쪽으로 바꿔 놓으면, 누군가는 그 걸 받아 화면을 다시 그려야 한다.

이 건 그동안 리스너를 콜백으로 넘겨주던 패턴과 다르지 않다. 어떤 식으로든 누가 누구를 리스닝할지는 지정해야 한다. 지정하는 방식에 분명 차이가 있을 것이다.

개개의 개체들이 리스너를 관리했다면, 총감독을 두어 관리 할 수도 있을 것이다. 이 때, Free 모나드와 비슷하게 체인을 엮은 구조로 만들어 두고 이를 총감독이 관리한다는 것 아닐까? 파이프에 이벤트 흐름이 흘러가게 두고, 누가 리스닝할지 모르지만, 이벤트에 이름을 붙여 파이프에 넣는 것이다. 그런데, 파이프에 흐름이 하나만 있는 건 아니다. 몇 개의 흐름이 있던 상관 없다. 총감독은 이 흐름들을 모두 지켜보고 있다. 내가 어떤 이벤트를 리스닝할지는 파이프에게만 알려주면 된다. 파이프가 총감독이다. 이 걸 의미하는 것일까? SF는 이 파이프 안에서 “단위”처럼 동작하는 것 같다. 이벤트를 리스닝 한 후의 결과도 SF다. A가 이런 이벤트를 발생 시키면, B가 받아서 어떤 작업을 하는 것으로 끝나는 게 아니라, B도 이어지는 이벤트를 발생 시키는 것이다. 이 걸 이벤트가 “변환”된 것으로 표현한다. 마치 모든 흐름은 파이프 안에서 이루어지고, 그저 잠깐 잠깐 상태를 바깥에서 지켜 볼 뿐이다.

어떤 것이 어떤 것의 영향을 받는다면, “동시”라는 개념은 존재하지 않는다. 의존성이 있다면 순서는 반드시 정해진다.

엘리엇의 FRP의 핵심 키워드는 “연속”이다. 연속을 어떻게 모델링할 것인가다. 가장 프리미티브한 모양을 t->α로, 함수로 시작한다. 생각해보면, 연속을 표현하는데 이 거만한 게 있을까 싶다. SF는 이벤트가 흘러갈 경로를 만드는 프리미티브다. 파이프 안에는

SF ----> SF ----> SF ---->...
            SF ----> SF ----> SF ---->...
    SF ----> SF -+--> SF ---->...
                 |
                 +----> SF ----> ...

이 존재한다.

엘리엇과 후닥이 Functional 반응 애니메이션을 내놓은지 20년이 지났다.

reactimate :: Monad m => m a -- 초기 액션
                      -> (Bool -> m (DTime, Maybe a))  -- 입력 sensing 액션
                      -> (Bool -> b -> m Bool) -- Actuation (출력 처리) 액션
                      -> SF a b
                      -> m ()

reactimate init sense actuate (SF {sfTF = tf0}) = do
    a0 <- init
    let (sf, b0) = tf0 a0
    loop sf a0 b0
  where
    loop sf a b = do
      done <- actuate True b
      unless (a `seq` b `seq` done) $ do
        (dt, ma') <- sense False
        let a'        = fromMaybe a ma'
            (sf', b') = (sfTF' sf) dt a'
        loop sf' a' b'

IO 액션을 써서 새로운 입력을 받고, 출력을 내보내는 SF 함수를 무한히 실행하는 함수입니다. SF 코드 덩어리의 runner쯤 되겠습니다.

컴비네이터 Time Lifting Integration Choice Behavior Switching Snapshot

Learning Yampa and Functional Reactive Programming

FRP.Yampa.embed :: SF a b -> (a, [(DTime, Maybe a)]) -> [b]
--                   |        |           |
--       run할 시그널 함수    |           |
--                          초기값        |
--                          (time, Nothing | Just nextValue)

입력 시그널을 위해 초기 입력 샘플과 샘플링 리스트, 그리고 시그널 함수를 인자로 받아 출력 샘플을 만들어 냅니다. reactimate의 순수 함수 버전입니다.

main :: IO ()
main = do
    putStrLn $ show $ embed time (Nothing, [(1.0, Nothing), (0.2, Nothing), (0.03, Nothing)])
    putStrLn $ show $ embed time (123, [(1.0, Just 234), (0.2, Just 345), (0.03, Just 456)])

    putStrLn $ show $ embed identity (123, [(1.0, Just 234), (0.2, Just 345), (0.03, Just 456)])
    putStrLn $ show $ embed identity (537, [(1.0, Nothing), (0.2, Nothing), (0.03, Just 123)])

    putStrLn $ show $ embed (constant 537) (Nothing, [(1.0, Nothing), (0.2, Nothing), (0.03, Nothing)])
    putStrLn $ show $ embed (constant 537) (123, [(1.0, Just 234), (0.2, Just 345), (0.03, Just 456)])
Github 계정이 없는 분은 메일로 보내주세요. lionhairdino at gmail.com