跳轉到

1997-FRP與響應式程式#

案件摘要#

動畫、遊戲、互動程式本質上是「時間驅動」的:值隨時間變化、事件在離散時刻發生。命令式的回呼(callback)模型把這種連續的時間性切成破碎的事件處理函數,程式邏輯散落各處。偵探 Conal Elliott 與 Paul Hudak 於 1997 年在 ICFP 發表 Fran(Functional Reactive Animation),證明 Haskell 的純函數與惰性可以完整表達「隨時間變化的值」:Behavior 是從時間到值的函數,Event 是離散事件串流,兩者都可組合、可積分、可宣告式變換。這樁案子開創了「函數式響應式程式設計」(FRP),並在二十年後內建進所有現代前端框架。

前因 -- 為什麼會有這個案子#

  • 動畫與互動程式的核心是「隨時間變化的值」(位置、速度、顏色),命令式模型要求程式設計師手動管理時間迴圈與狀態突變。
  • 回呼(callback)讓程式破碎:每個事件一個處理函數,狀態散落在多個閉包之間,難以推理「在某時刻整體長什麼樣」。
  • Haskell 的惰性與純函數帶來一個誘人的問題:能否把「隨時間變化的值」表達為一個純函數?
  • Elliott 在 Microsoft Research 想用函數式方法做動畫;Hudak 在 Yale 有 Haskell 的一致性設計經驗,兩人合作。
  • 同時期互動程式研究的既有方案(Tcl/Tk、事件迴圈)都是命令式的,缺少數學基礎——需要一個可組合的代數理論。

線索與推理 -- 數學式、程式、理論#

線索一:Behavior -- 隨時間變化的值是純函數#

Fran 的第一個關鍵線索:Behavior 就是從時間到值的函數:

\[ \text{Behavior}\; a = \text{Time} \to a \]

位置、顏色、旋轉角度都是 Behavior。因為 Behavior 是函數,所有函數組合子自動升級為 Behavior 組合子——lift2 (+) b1 b2 就是兩個 Behavior 的線性組合:

\[ (\text{lift2}\; f)\; b_1\; b_2 = \lambda t.\, f\, (b_1\, t)\, (b_2\, t) \]

線索二:Event -- 離散事件串流#

Event 是時間序列:

\[ \text{Event}\; a = [(\text{Time}, a)] \]

Behavior 與 Event 可以互相變換:Event 可以「接上」Behavior(在事件時刻切換行為),Behavior 可以取樣成 Event。積分把速度 Behavior 變成位置 Behavior:

\[ \text{integral}\; v = \lambda t.\, \int_0^t v(s)\, ds \]

線索三:FRP 的代數律#

FRP 不只是型別,還有一組代數律確保組合正確。Behavior 的線性組合律:

\[ \text{lift2}\; (+)\; (b_1 + b_2)\; b_3 = \text{lift2}\; (+)\; b_1\; (b_2 + b_3) \quad \text{(結合律)} \]

積分與微分的互逆律:

\[ \frac{d}{dt}\int_0^t v(s)\, ds = v(t) \qquad \int_0^t \frac{d b}{ds}\, ds = b(t) - b(0) \]

這些律讓優化器可以做「宣告式重算」:只重算受事件影響的子圖。

線索四:Elm 的簡化 FRP 與 Elm Architecture#

Evan Czaplicki 在 2012 年的 Elm 語言把 FRP 簡化:用 Signal(離散取樣的 Behavior)統一 Behavior 與 Event,並在 0.17 版之後收斂為「Elm Architecture」:Model -> Msg -> Model 的純函數更新 + 單向資料流。響應式的本質被定義為:對變化自動重算,資料流是宣告式的。

\[ \text{update} : \text{Model} \to \text{Msg} \to \text{Model} \]

可執行程式碼:Python 實作 mini FRP#

# mini FRP:Behavior(時間取樣)+ Event(事件流)+ map/filter/foldp 組合
class Behavior:
    def __init__(self, fn):        # fn: Time -> a
        self.fn = fn
    def at(self, t):
        return self.fn(t)
    def map(self, g):
        return Behavior(lambda t: g(self.fn(t)))
    def __add__(self, other):      # Behavior 的線性組合
        return Behavior(lambda t: self.fn(t) + other.at(t))
    def integral(self, dt=0.01):   # 積分:速度 -> 位置
        def pos(t):
            n, s, i = int(t / dt), 0.0, 0.0
            while i < t:
                s += self.fn(i) * dt
                i += dt
            return s
        return Behavior(pos)

class Event:
    """離散事件流:串列 [(time, value)]"""
    def __init__(self, events=None):
        self.events = events or []
    def map(self, g):
        return Event([(t, g(v)) for t, v in self.events])
    def filter(self, pred):
        return Event([(t, v) for t, v in self.events if pred(v)])
    def foldp(self, step, init):
        """foldp:以事件驅動的摺疊(Elm 的 reduce over time)"""
        out, acc = [], init
        for t, v in self.events:
            acc = step(acc, v)
            out.append((t, acc))
        return Event(out)

# --- 宣告式組合:只描述「是什麼」,不描述「何時重算」 ---
velocity = Behavior(lambda t: 10 * t)          # 速度 = 10t
position = velocity.integral()                 # 位置 = 5t^2(積分自動得證)
print("位置(2s):", position.at(2))             # 20.0

clicks = Event([(0.5, 1), (1.2, 1), (1.8, 1), (2.5, 1)])
doubles = clicks.map(lambda v: v * 2).filter(lambda v: v > 0)
total = doubles.foldp(lambda acc, v: acc + v, 0)
print("累計:", total.events)                    # [(0.5,2),(1.2,4),(1.8,6),(2.5,8)]

破案時刻:宣告式資料流#

# 同一個 position 行為被多處取樣——自動重算、無回呼、無狀態突變
for t in [0, 1, 2, 3]:
    print(f"t={t}: pos={position.at(t)}")      # 5t^2:宣告即證明

結案 -- 後果與影響#

  • Elm Architecture(2012-2016:Model、update、view 的單向資料流)成為 Redux 與 React 生態的藍本——Redux 作者明言受 Elm 啟發。
  • RxJS / ReactiveX(2013 前後)把 Event 串流的思想帶進主流 JavaScript 開發,成為「響應式程式設計」的工業代表。
  • SwiftUI(Apple, 2019)與 Jetpack Compose(Google, 2019)的宣告式 UI:UI 是狀態的純函數,FRP 思想內建進所有現代前端框架。
  • 「響應式 = 對變化自動重算」從動畫研究的偏門課題,變成前端開發的基本範式。
  • 本案證明:純函數可以表達時間性——「隨時間變化的值」不需要可變狀態,只需要把時間當參數。

關鍵人物與文獻#

  • Conal Elliott:本案偵探,Fran 的設計者,FRP 的開創者。
  • Paul Hudak:Yale 的 Haskell 建設者,Fran 的共同作者,2015 年逝世。
  • Evan Czaplicki:Elm 語言的設計者,把 FRP 簡化並推進前端主流。
  • Conal Elliott, Paul Hudak, "Functional Reactive Animation", ICFP, 1997.
  • Conal Elliott, "Push-Pull Functional Reactive Programming", Haskell Symposium, 2009.
  • Antony Courtney, Conal Elliott, "Genuinely Functional User Interfaces", Haskell Workshop, 2001.
  • Evan Czaplicki, "Elm: Concurrent FRP for Functional GUIs", senior thesis, Harvard University, 2012.
  • Evan Czaplicki, Stephen Chong, "Asynchronous Functional Reactive Programming for GUIs", PLDI, 2013.