yo, what's up?
到目前為止,目前我們把最基本的概念 pure function, curry, compose 到比較進階概念 lens, transduce 都介紹了一遍,有沒有感覺對於思考程式的方式稍微改變了呢? 接下來我們將窺探 Functor, Monad 的世界,但江湖再走,配備要有,所以筆者將花三到四的篇幅介紹一些預備知識。
而今天我們將介紹 Type Signature!
只要有寫過靜態語言的讀者們,對 Type Signature 一定不陌生
像是 Java 當我們要宣告一個函式時,我們必須將該函式參數跟其回傳的型別事先聲明
Java
public static int add(int a, int b);
而在 ML-influenced 語言,像是 haskell,其 Type Signature 只是用不同的方式去表達,這就是我們今天要介紹的主角 Hindley-Milner type signature
如果有使用 ramda 的讀者們,在閱覽它們文件的時候,應該都看過這個
add :: Number -> Number -> Number
這就是 Hindley-Milner type signature.
先從最簡單的函式 length
作為範例,
// length :: String -> Number
const length = str => str.length;
而 length :: String -> Number
,將其拆解分析
這樣我們就可以清楚知道 "有一個函式 length,需要傳入一個型別為 String 的參數,而回傳的型別為 Number."
然而像是 add
這種函式其接收多個參數,那要如何表達呢?
大家應該已經猜想的到,如果是多個參數就用 ,
拆分,並用 ()
包住
// add :: (Number, Number) -> Number
const add = (x, y) => x + y;
到這裡讀著們應該可以發現,這有點像 ES6 的 arrow 函式的寫法,只要將參數名稱, 運算式與 =>
改成參數型別,回傳型別跟 ->
就是 HM type signature.
ES6 Arrow function
(x, y) => x + y
HM type signature
(Number, Number) -> Number
那當參數是 Array 的話那要如何表達呢?
不意外的,就是用 []
包起來,如果 Array 內都是 String,那就是 [Stirng]
,如果都是 Number 那就是 [Number]
,以此類推
舉例,take
傳入目標位置,以及一組陣列,就會回傳該陣列中目標位置的值
const take = (position) => (arr) => arr.at(position)
而用 HM type signature 就可以這樣表達,
take :: Number -> [Number] -> Number
最後一個就是 HOF 了,這在 JS 的世界是非常常見的寫法,傳入的參數為函式,那這要如何用 HM type signature 表示呢?
例如,現在我們用 map
函式將一組元素皆是字串的陣列透過 length
轉換成字串的長度
const map = (fn) => (arr) => arr.map(x => fn(x));
map(length, ['hello', 'functional', 'programming'])
所以 map 的 HM type signature 就會是
map :: (String -> Number) -> [String] -> [Number]
剛剛看了map
的範例後,讀者們應該都有些疑惑,如果今天 transformer function 是轉換出來的型態不是 (String -> Number)
呢?
有可能是
map(double, [1, 2, 3, 4]) // [1, 4, 9, 16]
map(isEven, [1, 2, 3, 4]) // [false, true, false, true]
而其對應的 HM type signature 為
map :: (Number -> Number) -> [Number] -> [Number]
map :: (Number -> Boolean) -> [Number] -> [Boolean]
在面對這麼多種可能下,我們總不能把所有可能的結果列出一張大表。
這就是 Type variable 存在的原因,其概念有點類似泛型,像是 map
這種通用函式,就需要用更通用的方式去表達
map :: (a -> b) -> [a] -> [b]
這邊值得注意的地方有
1. 型別的寫法區分
a
, b
, c
,...String
, Number
, Boolean
...2. 通用型別在同一個 type signature 不能夠進行複用,除非該通用型別皆指向相同的特定型別
Bad
例如 map
, 不能將 a
同時代表不同型別
map :: (a -> a) -> [a] -> [a]
3. 不同的通用型別可以代表相同的特定型別
例如 map
map(double)([1, 2, 3, 4]) // [1, 4, 9, 16]
而上述這段函式是長這樣 (Number -> Number) -> [Number] -> [Number]
,其對應到 map
的 HM type signature (a -> b) -> [a] -> [b]
,則 a
, b
皆可以代表 Number.
若今天某個特定的 method 只能接受某些特定的型別呢? 這也是 Type Constraint 存在的原因
在之後的章節我們會頻繁的看到,像是
equals :: Setoid => a -> a -> Boolean
可以看到這裡出現了先前沒看過的新的符號 =>
!那這個代表什麼呢?
其代表以
=>
作為分界點,只要滿足分界點左邊的所有條件,那麼右邊的表示式就是有效的。
白話一點可以這樣解釋, 若 a
是 Setoid
, 則 a -> a -> Bool
成立。
在這裡先別管 Setoid 是什麼,這之後我們有機會講解到。而在這裡就先理解為比較兩值是否相等的 Type class。
令人哀傷的是,JavaScript 為動態語言,其並沒有自建 Type checking 功能,我們無法知道開發者是不是真的放入正確的資料型別,舉例來說,現在有一個 type signature 其要求某值必須是要 [a]
,但實際上我們沒有能力限制開發者不能放入 [1, '1', true, () => 1]
之類的值。
但它的存在還是非常重要的,至少可以在註解上清楚的知道這個函式應該要放入的正確型別!
有時函式名稱不一定是最精準,若對方亂取,則很容易讓人誤導,Type Signature 除了可以讓協作者用讀你的程式時更快進狀況,也能更正確的使用你寫的函式。另外一個優點我想就是 Free Theorems,而該作者已經有很好的解釋,筆者就不多做贅述。
感謝大家閱讀!
NEXT: Type class & ADT
感謝分享
上述這段函式是長這樣 (Number, Number) -> [Number] -> [Number],
請問這邊是指 (Number -> Number) -> [Number] -> [Number]嗎?
期待後續的 Monad