iT邦幫忙

2021 iThome 鐵人賽

DAY 9
0
Software Development

Functional Programming For Everyone系列 第 9

Day 09 - Type Signature

yo, what's up?

到目前為止,目前我們把最基本的概念 pure function, curry, compose 到比較進階概念 lens, transduce 都介紹了一遍,有沒有感覺對於思考程式的方式稍微改變了呢? 接下來我們將窺探 Functor, Monad 的世界,但江湖再走,配備要有,所以筆者將花三到四的篇幅介紹一些預備知識。

而今天我們將介紹 Type Signature!

What's type signature?

只要有寫過靜態語言的讀者們,對 Type Signature 一定不陌生

像是 Java 當我們要宣告一個函式時,我們必須將該函式參數跟其回傳的型別事先聲明

Java

public static int add(int a, int b);

Java

而在 ML-influenced 語言,像是 haskell,其 Type Signature 只是用不同的方式去表達,這就是我們今天要介紹的主角 Hindley-Milner type signature

Hindley-Milner type signature

如果有使用 ramda 的讀者們,在閱覽它們文件的時候,應該都看過這個

add :: Number -> Number -> Number

ramda

這就是 Hindley-Milner type signature.

Basic

Single argument

先從最簡單的函式 length 作為範例,

// length :: String -> Number
const length = str => str.length;

length :: String -> Number ,將其拆解分析

Imgur

這樣我們就可以清楚知道 "有一個函式 length,需要傳入一個型別為 String 的參數,而回傳的型別為 Number."

Multiple argument

然而像是 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

List of values

那當參數是 Array 的話那要如何表達呢?

不意外的,就是用 [] 包起來,如果 Array 內都是 String,那就是 [Stirng],如果都是 Number 那就是 [Number],以此類推

舉例,take 傳入目標位置,以及一組陣列,就會回傳該陣列中目標位置的值

const take = (position) => (arr) => arr.at(position)

而用 HM type signature 就可以這樣表達,

take :: Number -> [Number] -> Number

Function (higher-order function)

最後一個就是 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]

Type Variable

what's the problem?

剛剛看了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]

在面對這麼多種可能下,我們總不能把所有可能的結果列出一張大表。

how to solve?

這就是 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.

Type Constraints

若今天某個特定的 method 只能接受某些特定的型別呢? 這也是 Type Constraint 存在的原因

在之後的章節我們會頻繁的看到,像是

equals :: Setoid => a -> a -> Boolean

可以看到這裡出現了先前沒看過的新的符號 =>!那這個代表什麼呢?

其代表以 => 作為分界點,只要滿足分界點左邊的所有條件,那麼右邊的表示式就是有效的。

白話一點可以這樣解釋, 若 aSetoid, 則 a -> a -> Bool 成立。

在這裡先別管 Setoid 是什麼,這之後我們有機會講解到。而在這裡就先理解為比較兩值是否相等的 Type class

令人哀傷的是,JavaScript 為動態語言,其並沒有自建 Type checking 功能,我們無法知道開發者是不是真的放入正確的資料型別,舉例來說,現在有一個 type signature 其要求某值必須是要 [a],但實際上我們沒有能力限制開發者不能放入 [1, '1', true, () => 1] 之類的值。

但它的存在還是非常重要的,至少可以在註解上清楚的知道這個函式應該要放入的正確型別!

Why

有時函式名稱不一定是最精準,若對方亂取,則很容易讓人誤導,Type Signature 除了可以讓協作者用讀你的程式時更快進狀況,也能更正確的使用你寫的函式。另外一個優點我想就是 Free Theorems,而該作者已經有很好的解釋,筆者就不多做贅述。

小結

感謝大家閱讀!

NEXT: Type class & ADT

Reference


上一篇
Day 08 - Transduce II
下一篇
Day 10 - Algebraic structure
系列文
Functional Programming For Everyone30

1 則留言

1
Ken Chen
iT邦新手 5 級 ‧ 2021-09-25 17:33:24

感謝分享

上述這段函式是長這樣 (Number, Number) -> [Number] -> [Number],

請問這邊是指 (Number -> Number) -> [Number] -> [Number]嗎?

期待後續的 Monad

我要留言

立即登入留言