FRAN - Functional Reactive ANimation (작성 중)

Posted on March 7, 2024

Functional Reactive Animation - Conal Elliott and Paul Hudak, 1997 논문을 본 분들을 대상으로 삼는 글입니다. 무려 27년 전 글이지만, 아직도 많이 읽힌다고 하는데, 검색에는 딱히 걸려드는 한글 자료는 없습니다. 수많은 선행자들이 이해하고 넘어 갔을텐데 자료가 없는 게 아쉽습니다.

생각 스트레칭

b1 untilB (lbp t0) ==> λe.b2 untilB e -=> b3

절차형에서는 여러 단계로 되어 있는 if문들이 함수형에선 모두 사라진 느낌의 표현입니다. 이게 익숙하게 보이지 않아, 람다 산법에서 if문을 어떻게 처리하는지 다시 상기해 봤습니다.

if문은
“외부에서 들어온 값”을
“값을 받아 true 또는 false로 판단하는 구문”을 거치고,
if구문”으로 true일 때 실행할 코드, false일 때 실행할 코드로 진행합니다. –(가)

람다 산법에서 trueselect_first로, falseselect_second로 정합니다. ※ 참고 람다 대수 기본 용어 - 분기문
실행할 코드 두 가지를 튜플로 가지고 있고, 외부에서 “튜플 중 하나를 선택하는 함수”를 받는 고차 함수를 정의합니다. 고차 함수에 select_first 또는 select_second를 넣어줘서 진행할 길을 결정합니다. –(나)

좀 더 추상적으로 정리하면, 함수형에서는 가능한 경우의 수에 따른 코드를 튜플로 준비해 놓고, 튜플의 위치를 고르는 함수를 받아, 그 함수를 튜플에 적용해서 분기를 구현하고 있습니다. 값으로 표현하던 true가 함수가 됐으니, 고차 함수는 이 함수를 적용할 값을 가지고 있어야 합니다.

처음 볼 때는, 값과 함수의 역할이 뒤바뀌는 식으로 생각해야 하나 싶었는데, 잘 따져 보면, 두 개가 그리 달라 보이지 않습니다.

true를 받았다면, 컴비네이터 안에는 값에 적용할 함수가 있고,(if구문을 별도의 함수로 생각)
함수 true 즉, select_first를 받았다면, 컴비네이터 안에는 이 함수를 적용할 값을 가지고 있습니다.

분기란 건, “조건”과 “조건에 따라서 실행될 코드”, 그리고 둘을 매핑해주는 역할이 필요합니다. (이 역할을 런타임이라 보겠습니다.) 매핑해주는 역할을 true에 심어 놓든가(함수형의 select_first), 별도 함수(절차형의 if)로 두든가의 차이입니다.

프로그래밍은 매핑한 결과를 또다른 실행 코드와 매핑하고, 그래서 나온 결과를 또 다른 실행 코드와 매핑하는, 매핑의 반복으로 이루어져 있습니다. 매핑할 때 분기를 하든가, 갈 길이 하나뿐이 없는 시퀀싱으로 매핑하든가 합니다. 절차형이 됐든, 함수형이 됐든 이 동작으로, 현실을 모델링하는데는 차이가 없습니다. 매핑 결과를 다음 매핑에 어떻게 넘겨 주냐의 차이만 있을 뿐입니다.

절차형에선 프로시저가 작업이 끝나면, 작업 결과를 메모리에 넣어두고, 항상 다음 프로시저로 진행합니다. 다음 프로시저를 정하는 건 런타임이 합니다. (내부 구현은 함수형과 같이 CPS로 되어 있을 수도 있겠습니다.) 함수형에선 함수가 작업이 끝나면, 작업 결과를 바로 다음 함수에 넣어주며 진행합니다. 다음 함수를 정하는 건 함수 자체가가 정합니다. (CPS를 알고 있다면, 함수형은 CPS로 이루어져 있다고 말할 수 있습니다.)

Event와 Behavior 컴비네이션

2.3 Semantics of Events 섹션에 External events 부분에 아래 예시가 나옵니다.

b1 untilB (lbp t0) ==> λe.b2 untilB e -=> b3
untilB :: Behavior α -> Event (Behavior α) -> Behavior α
-=> :: Evant α -> β -> Event β
==> :: Evnet α -> (α -> β) -> Event β

lbp, rbp :: Time -> Event (Event ()) -- 마우스 클릭

결합 순서를 뜻하는 괄호는 없지만, 타입을 고려하면, 아래처럼 해석해야 말이 됩니다.

untilB

마우스 버튼이 눌리기 전에, 위 체인의 결과는 b1
마우스 버튼이 떼지기 전에, 위 체인의 결과는 b2
마우스 버튼이 떼어진 후에, 위 체인의 결과는 b3

예를 들면 상태가 b1인데, 마우스 버튼을 누르면 b2가 되고, 그 후 버튼을 떼면 b3가 되는 동작을 모델링한 체인입니다. 어떻게 이렇게 해석되는지 하나 하나 뜯어 봤습니다.

-=>Event α와 값β를 받아 Event β를 돌려주고, (Event가 가지고 있는 값을 바꿔치는 작업을 합니다.)
==>Event α와 함수(α -> β)를 받아 Event β를 돌려 줍니다. (Event가 가지고 있는 값을 바뀌주는 함수를 받아 값을 변환하는 작업을 합니다. 폴리모픽으로 타입을 규정하고 있진 않지만, 위 사용례를 보면 αEvent타입을 받고, 이 Event가 발생하면, 조건에 따라 Behavior를 결정해서 β로 내뱉는 함수가 주로 올 것 같습니다.)

-=>==>Event 컴비네이션을 만들어 내는 컴비네이터들로 최종 결과는 Event를 만들어 내는 함수들입니다.

매직같은, 번뜩이는 아이디어는 λe.b2 untilB e -=> b3 부분입니다. lbp t0Event ()가 아니라 Event (Event ()) 값을 만들어 냅니다. e -=> b3는 안에 있는 Event ()의 값을 바꿔 놓는 역할을 합니다.

순서를 가진 Event들은 Event(Event(Event ...)) 모양으로 표현할 수 있습니다. 버튼 떼기는, 버튼 누르기 후 일어납니다. Event(Event ())로 표현하고 있습니다.

아무도 일도 일어나지 않으면 b1,
바깥 Event까지만 일어났다면 b2,
안 쪽 Event까지 일어났다면 b3입니다.

  1. 마우스 버튼 클릭이 없었다면 b1
  2. 마우슨 버튼 클릭이 일어났다면, Event(Event ())중 안에 있는 Event ()λe.b2 untilB e -=> b3에 넘겨 줍니다.
  3. e -=> b3로 이벤트e가 가지고 있는 ()b3로 바꿉니다.
  4. 그 후 Event b3가 아직 일어나지 않았다면, 첫 번째 untilB의 결과(전체 체인의 결과)가 b1대신 b2, 이벤트가 일어났다면 b1대신 b3가 됩니다.

마치 if문이 없는 것처럼 보이지만, untilB에서 이뤄지고 있습니다.

절차형으로 위 동작을 표현하려면 어찌할지 생각해 봤습니다.

if (마우스 버튼 클릭) then
  if (마우스 버튼 릴리즈) then return b3
  else return b2
else
  return b1

절차형에 비해 생각이 걸리적 거리는 이유는 버튼 클릭 -> 버튼 릴리즈 처럼 순서대로 가지 않고, 생각 흐름과 일치하지 않는 것처럼 보일 수도 있습니다. 하지만, ifuntilB에 숨은 것을 생각하고 보면 절차형과 그리 달라 보이지 않습니다. 잘 보면 ==>는 두 번째 인자로 α -> β 함수를 받습니다. 여기서는 Event () -> b2 혹은 b3이 됩니다. 의사 코드로 쓰면

b1 until 클릭 이벤트 
         b2 until 릴리즈 이벤트 
                  b3 

이런 느낌입니다. 이렇게 봐도 아직 편하진 않습니다.

Push or Pull

어느 한 쪽에서 무슨 일이 일어났는지 아는 방법은

두 가지 방법뿐이 없습니다.

일등급 시민으로 타입(Event,Behavior)을 설계하고 그냥 함수로 일반 값 다루듯이 하는 게 언뜻 이해가지 않았습니다. 아마도 옵저버블 패턴(리스너 패턴) 자체가 어떻게 함수형으로 구현되는지 너무 일찍 궁금해 해서 그런 듯 합니다. 위 untilB 체인은 프레임마다 계속 실행하는 걸로 생각하고 읽으면 됩니다. 리스너를 쓰지 않는다는 말이 아닙니다. 나중 외부 이벤트와 붙이게 되면, 리스너를 써야만 합니다. 논문에서 구현 전에 나오는 내용은 리스너와 붙이는 것을 대체하는 게 아닌, 리스너로 통지를 받은 후에 전파되는, 일종의 이벤트 네트워크를 표현하는 방식에 관한 얘깁니다.

Event 타입은 특별한 동작이 있습니다. Event가 발생하기 전엔 코드가 진행되지 않게 막을, 블록할 방법이 필요합니다. FRAN에선 바로 언급하지 않고 있지만, 저는 구현을 생각하며 쫓아가는 게 이해하기 편했습니다. 구현에선

첫 번째 방법을 pull, 두 번째 방법을 push로 부르는 것 같습니다. push 방법은 바로 전체 체인의 결과가 바로 나오는 게 아니니, 체인 중간 중간 결과가 의미있게 드러난다면 가능할 것으로 보입니다. 어차피 MVarIORef를 써야하니, 이펙트에 의미를 잘 심으면 될 것으로 보입니다.

시간 연속 Continuous

FRAN 논문의 2.1 Semantic Domains

아래는 추측입니다.

pull로 구현한 상황을 보겠습니다. Event3.5초에 일어났다고 보겠습니다. 0.5초 단위로 샘플링하고 있었다면, 이 이벤트는 정상적으로 캡처됐겠지만, 만일 1초 단위로 샘플링하면 3초에 샘플링할 때도 이 값은 없고, 4초에 샘플링할 때도 이 값은 없습니다. 이 Event를 놓치지 않는 방법을 생각해 봤습니다.

이렇게 정의하면 샘플링 단위 시간에 상관없이 이벤트를 놓치지 않을 수 있습니다. 벡터 그래픽을 비트맵으로 만드는 것과 비슷한데, 너무 일찍 비트맵으로 만들어 버리면 데이터를 잃어버리는 양이 늘어 나듯이, 최대한 시각이 아닌, 시간, 즉 범위로 정보를 끌고 다녀야 합니다.

샘플링에 상관없이 결과를 내려면, 확정된 하나의 값이 아닌, 최대한 범위로 정보를 들고 다녀야 합니다. 이렇게 하기 위해 논문에선, 시간을 집합론의 Order Theory로 정의하고 있습니다.

1 ⊑ 2 ⊑ 3.5이상 ⊑ 4

3.5 이상이면 4일지 더 큰 수일지 알지 못하지 않나 싶은데, 위 처럼 순서가 나오도록 다음과 같이 정의해 놨습니다.

x ≤ y이상 if x ≤ y
x이상 ≤ y 는 undefined
x이상 ≤ y이상 undefined

처음 볼 때 금방 이해가 안갔습니다. x이상,y이상을 하나의 수처럼 바라보긴 하는데, x이상y보다 작을지 클지는 모른다는 직관 그대로 undeinfed라 정의해놨습니다. denotational한 의미를 정의하는데 중요한 역할을 하는 정의라고 하는데, 천재적인 것 같기도 하고, 말장난 같기도 합니다. 어쨌든 시간의 크기보단 순서가 의미가 있다 정도로 넘어갔습니다.

크기가 작다는 뜻이 아닙니다.(이름은 Square Image of or Equal To 라 부르는 기호입니다.) 순서상으로 먼저와 나중을 표현하는 기호입니다. 3.54보다 작지 않을지 몰라도 순서상으론 앞에 오도록 약속, 규칙을 정의합니다. 만일 샘플링을 0.2초 단위로 한다면 3.53.6보다는 (작은 게 아니라) 먼저라는 뜻입니다. “어떤 수 이상”이란 원소들은 자신이 속한 샘플링 구간을 넘지 않는다는 약속이니, 어떤 샘플링 단위에서도 정보를 잃어버리지 않는다로 이해하고 있습니다.

현재 구현된 FRP 프레임워크들 중에 퍼포먼스도 잘 나오고, Continuous 시간도 이론대로 구현한, 두 마리 토끼를 다 잡은 프레임워크는 없다는 것 같습니다. 코널 엘리엇 교수의 여기 저기 글을 보면, FRP는 denotational하고, Continuous 시간을 표현, 구현해야 제대로 FRP라 할 수 있는데, 그런 의미에선 아직은 제대로 실용 가능한 퍼포먼스를 보이는 FRP프레임워크는 없다는 뜻입니다.

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