Functional Reactive Programming 的入門心得

Ray Shih
8 min readJan 31, 2015

--

從這幾年 Scripting language 的興起到可以觀察到 Functional programming 已經不再是古老被人唾棄的觀念了。就連 C++、Java 等被認為有些 old fashion 的語言也開始導入匿名函數,各種 framework 也開始出現大量使用 Functional Paradigm 的設計。那麼 Functional Programming 到底有什麼過人之處?明明一些問題使用 OO Design Pattern 就可以解決,導入 Functional 以後真的會比較好嗎?而 Reactive 又是什麼樣的觀念呢?

Single Source of Truth

很多邏輯上的錯誤都是因為在資料分散的情況下,疏於管理造成的資料不一致的狀態造成的。最簡單避免類似問題的方法就是:讓同樣的資料只有一份,不要複製並分散在程式的各個角落。但資料還是要處理,那怎麼辦呢?我想最佳的方法就是使用 Reactive 的觀念來作為資料傳遞的方式。

什麼是 Reactive

中文翻譯叫做「反應」。物件的行為是根據其他物件的行為。有點像是反射動作,就好像往膝蓋的某個地方敲,小腿會自己往前踢一樣。那麼為什麼這樣做比較好呢?因為在 Reactive 觀念下,開發者只要關心「input 如何轉換成 output 」就行了,整個過程是沒有時間觀念的,畢竟身為三次元生物,沒有辦法穿越時間,所以要分析跟時間相關的事情對人來說是困難的。

有 reactive 就有 proactive,我們可以舉一個簡單的例子來比較兩者:假設 obj 中有兩個值 a、b,並且希望 b 永遠都是 a + 1。如果是 proactive 的話就會像這樣:

// JavaScript
// proactive
var obj = {
a: 0,
b: 1
};

obj.a = 3;
obj.b = obj.a + 1; // proactive
console.log(obj.b); // b = 4

也就是每一次 a 改變的時候都要多寫一行,如果在很多地方需要更動 a 的值,那就很容易漏改 b。你可能會想「為什麼不寫一個 setA function ,並把修改 b 這件事情放在 setA 裡面就好了?」當然可以,不過如果 a 跟 b 不屬於同一個物件的時候,寫在 setA 裡面會造成兩個物件產生不必要的相關性。

如果是 Reactive 呢?

// JavaScript
// reactive
Object.observe(obj, function (changes){
obj.b = changes[0].object.a + 1;
});

obj.a = 5;
console.log(obj.b); // 6

這段 code 的意思是:去觀察 obj 的改變,並在每一次改變的時候把 b 指定成 a + 1。如此,不管在哪個地方修改 a,b 都會自動被更新。你可能會問,這不就是 Observer Pattern 嗎?對,這就是 Observer Pattern。

Reactive vs Observer Pattern

那為什麼要特別提到 Reactive ,不直接使用 Observer Pattern?這是我一開始碰到這個名詞的時候的想法。經過一番思考以後,我認為 Reactive 是一種形容詞,而 Observer Pattern 是一種實作 Reactive 的方法。就個人學習經驗上,這兩這還是有一些不同。

使用 Observer Pattern 時的想法是基於物件:

  1. 物件改變狀態時主動通知
  2. 被觀察的物件 Y 不需要知道觀察他的物件 X(也就是 Decouple)

而觀察者物件 X 要對被觀察者物件 Y 做什麼事情則沒有規定,也就是觀察者也可以主動跟被觀察者要資料

但在使用 Reactive 觀念時的想法是基於資料流:

  1. 資料 A 出現後如何變成資料 B

最明顯的差別是在 Reactive 的觀念中是沒有物件概念,自然就沒有 B 跟 A 要資料這件事情發生

為何是 Functional Reactive 而不是 OO Reactive

Android 寫多了多少都會以下類似問題:

為了處理某個按鍵點擊,需要實作一個類別。而因為這個 class 只會針對這個 UI ,所以使用匿名物件比較合理,而裡面只實作了這麼一個 method 。這時就會內心 OS :「整個邏輯的重點是這個 method,為了要實作這個 method 而要多打整個匿名物件宣告,好麻煩喔!為什麼不像 JS 一樣傳一個 callback function 就好了呢?」

這是一個很鮮明的例子,說明了在做資料或事件處理的時候「類別」其實是一種累贅。我想這就是為什麼比較少人把 OO 跟 Reative 拿出來一起談的原因。

這就是 Functional Programming 啊!何必要提到 Reative ?

這是我一開始接觸到 Reactive 這個名詞時,腦袋冒出的另外一個問題。但我開始認真學習 Functional Programming 後才發現,其實 Functional 並沒有要求你一定要這麼做,尤其是在現今各種語言都開始支援匿名函數的當下,是很容易回到純 OO 觀念去撰寫。例如 JS 可以說是一種廣義的 Functional Language, Backbone 類型的 MVC Framework 沒有明確的 Reactive 觀念在其中。而最近較紅的 AngularJS 與 ReactJS 則非常強調 data-binding,尤其是 ReactJS 出現後,在前端領域中越來越多人討論 Reactive 這件事情,例如:ReactConf 2014 中的 What does it mean to be Reactive (講者為 Reactive Extensions 的作者),也有批評 ReactJS 不夠 reactive 的 Don’t React

ReactiveCocoa 範例、心得

ReactiveCocoa 是一個 iOS 上面的 FRP library。我找到的入門教學文是

ReactiveCocoa Tutorial — The Definitive Introduction

擷取其中範例並稍加簡化如下:

目標:有個頁面需要驗證使用者輸入,例如 username ,如果不通過則把 textField背景改成黃色。

常見的實作方法:

首先我們要先註冊 textField 變化的 event:

[self.usernameTextField addTarget:self
action:@selector(usernameTextFieldChanged)
forControlEvents:UIControlEventEditingChanged];

並且建立 delegate function 來執行驗證:

- (void)usernameTextFieldChanged {
self.usernameIsValid = [self
isValidUsername:self.usernameTextField.text];
[self updateUIState];
}

最後用 updateUIState function 來修改顏色:

- (void)updateUIState {
self.usernameTextField.backgroundColor =
self.usernameIsValid ?
[UIColor clearColor] : [UIColor yellowColor];
}

你可以看到 updateUIState 需要在每一次有需要更新畫面時呼叫到,如果狀況一多就會發生跟本文開頭所描述一樣的問題:漏掉。

我們來看看 ReactiveCocoa 實作版本:

[[[self.usernameTextField.rac_textSignal                  // 1
map:^id(NSString *text) {
return @([self isValidUsername:text]); // 2
}]
map:^id(NSNumber *passwordValid) {
return [passwordValid boolValue] ?
[UIColor clearColor] : [UIColor yellowColor]; // 3
}]
subscribeNext:^(UIColor *color) {
self.passwordTextField.backgroundColor = color; // 4
}];

沒錯!只有一段。

稍微解說如下:

  1. 取得 usernameTextField 的 signal ,相當於註冊 event 的動作
  2. 驗證,本來是用 delegate function 實作
  3. 依據驗證結果選顏色
  4. 更新 UI,與步驟 3 一起完成本來的 updateUIState function

在跑完教學以後我有以下感想:

  1. 輕易的產生、註冊 Event : 使用 ReactiveCocoa 中的 createSignal 方法可以很輕易的產生 Event,並以 Signal 物件作為註冊 event 的出發點,也有提供不少 macro 輔助產生、處理 event signal 。而且在引入 ReactiveCocoa 後,大部份的 UI 元件都會自動擁有 Signal。
  2. 邏輯不再分散:在原本實作中,要處理每個 UI Event 都必須要宣告一個 method 來處理,因此一個流程的邏輯會分散在不同的 method 中。
  3. 避免 Callback Hell:雖然 Cocoa 提供的 API 都是註冊 class method 為主,但也有一些 library/API 採用 block 的方式來實作 callback,沒處理好還是會有 Callback Hell 的問題。有了 Signal 以後,可以直接使用匿名函數,而且因為 Signal 的 Chaining 設計,使我們可以避免 Callback Hell,最終使整個流程的邏輯集中。

結論

Single Source of Truth 的原則可以減少資料不一致時發生的邏輯錯誤, Reactive 則描述了要達成這個原則的觀念,而 FRP 是我目前看過,實作這個觀念最好的方法。

--

--

Ray Shih

Functional Programming Advocator, Haskell, Idris, PureScript Lover. Work at Facebook and Machine Learning student.