作為一個 FP 愛好者,當你學習到某種程度時你多多少少會碰點數學,即便沒有直接接觸數學本身也會了解一下 FP 語言,Haskell 肯定聽過吧?

但單純的學寫語言已經滿足不了你了,你想好好探究一下數學對 FP 的奧妙之處,而你第一個聽到的就是範疇論(category theory),你滿心期待的想了解這一切的起源了,不過脫離數學太久的你似乎無法應付,況且這領域還是極度抽象的。你卡關了,你到處找影片、找文章,試圖理解這到處都是圓圈圓點符號箭頭的東西到底是什麼?

失敗了

你努力過最後還是失敗,雖然許多教學都幫你把程式跟範疇論的一些事物做對應,腦中還是滿滿疑惑,似懂非懂。

對於試圖做過努力得你肯定也看過 fantasy-land 的這張圖:

algebra

我也相信你應該沒耐心把內容全部看完(就是我),並且焦點全在左側的 Functor 樹狀圖(還是我),以為這才是你最需要學習的,不過在因緣際會之下讀了 fp-ts 作者的 FP 介紹文章才發現我似乎在越級打怪。

Algebraic Structure

我相信你或多或少也在 wiki 搜尋到或見過這張圖:

你可能也覺得這應該不是我需要的,所以就略過了(又是我),但 fp-ts 作者的文章就是從這裡開始說起,而你再仔細看一下整張圖最源頭 「Magma」的數學定義,是由一個 集合(set) 跟一個 運算(operation) 組成。這太棒了,我們似乎找出了一些蛛絲馬跡,那這裡的集合到底代表什麼呢?

集合 Set

說到集合,我們必須來複習一下定義

集合是一種把若干個元素放在一起的方式,不考慮元素的順序重複性。集合可以用大括號 來表示,例如 是一個集合,包含了三個元素 1, 2 和 3。集合的定義是抽象的,可以用來描述任何事物的分類或歸納

我們平常看待的型別

以往學習程式語言,型別通常對開發者是一種標記,好讓讀的人知道這是什麼,當然以計算機的角度會是不同意義,所以我們這裡只討論開發者的感受。

首先我們從 TS 常見的 primitive type 講起,最常見的有三個,booleannumberstring

  • boolean : 只有 truefalse 兩種狀況

  • number : 包含正負整數、浮點數(有最大值跟最小值,但在這我們先不考慮)

  • string : 字元的集合,在不限制字數情況下是無限多種組合

或是利用 primitive type 組成的複合型別

type Coord = {
  x: number;
  y: number;
}

這些在日常開發上有做到提醒的作用,避免掉一些 bug,但除了 bug 之外我還在意一件事,那就是「該型別到底涵蓋多少 elements ?」。

帶入集合的概念

假如我們把 boolean 視為一個集合,那它擁有多少元素呢?

當然會是兩個,因為只有 truefalse 兩種,所以 boolean 這個型別是寫成以下這樣:

type Boolean = true | false;

 

我們依此類推,number 這個集合會有幾個元素?

我們有正負整數跟浮點數,有個最大值最小值,所以 number 這個型別實際上是寫成:

type Number = Min_Number ... 0 | 1 | 2 ... Max_Number

實在是好多我寫不完

string 也是同理而且跟 number 很像,元素也非常多:

type String = '' | 'a' | 'b' | ... | 'ab' | ...

如果今天是元素量是有限的,並且量不多的話寫 union type 的形式當然是最好,這樣更能精確表達該型別一定會出現什麼,不可能出現什麼,像是一年只有 12 個月或是一星期只有 7 天

type Month = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
 
type DayOfWeek = 'Mon' | 'Tue' | 'Wed' | 'Thu' | 'Fri' | 'Sat' | 'Sun'

那如果今天我要一個偶數的型別 EvenNumber 呢?元素很多,我們不可能一一寫出,並且你沒法在靜態檢查時檢查出來,唯一的辦法是寫一個 function 來限制它,這裡我們利用 Option 來表示,如果是 Some 那就是偶數,是 None 就不是偶數:

function evenNumber(n: number): Option<number>{
  if(Number.isInteger(n) && n % 2 === 0) return some(n);
  return none()
}

function 在程式裡過濾資料是非常常見的,就像是資料驗證就是從眾多可能元素過濾出我們需要的,而這個結果會是輸入資料集合裡的一個子集合。

Set

而這個 function 扮演的似乎就是我們 Magma operation(運算) 的一個角色。

複合型別

複合型別就是將各種 type 組合成一個新的 type,而複合型別這個集合的元素就是所有這些屬性的組合:

type Person = {
  name: string;
  age: number;
  gender: 'Male' | 'Female' | 'None'
}
 
const person: Person = {
  name: 'John',
  age: 20,
  gender: 'Male'
}

person 這個變數就是 Person 集合裡的一個元素。

Sum Type 跟 Product Type

我們很常在 ADT(Algebraic Data Type) 聽到這兩個詞,似乎很重要的東西,但我們該怎麼識別它呢?

對於識別一個 type 是 Sum Type 還是 Product Type 網路上有很多方法,但對我而言都不夠明確,你能識別以下幾個 type 嘛 ?

type Color = [number, number, number]  // r, g, b
 
type Coord = {
  x: number;
  y: number;
}
 
type Diection = 'up' | 'down' | 'left' | 'right';
 
 
type Milk = {
  // ...
}
 
type Tea = {
  // ...
}
 
type Coffee = {
  // ...
}
 
type Drink = Milk | Tea | Coffee;

元素數量

我自己一個最簡單的方法是利用計算元素數量的方式來辨別

假設你想計算 Color 這個集合總共有幾個元素,那就會是先看 number 集合有幾個元素,我們假設 number 集合元素有 個,而 Color type 是三個 number 的組合,所以元素的數量會是

Drink 呢?我們假設 Milk, Tea, Coffee 元素數量分別為 ,那 Drink 集合的元素數量會是

注意到符號不一樣了嘛,一個是 一個是 ,而加總跟乘積的英文正好就是 sumproduct

以後就可以利用計算元素數量的方式來辨別是個 Sum Type 還是 Product Type

結語

透過將型別視為集合來思考,建立型別實際上就是在建立一個集合。而函數就像是對這些集合進行操作,因此進一步讓我深入了解了 Functional Programming 的思想。現在,型別對我們的作用已不僅僅是輔助識別或防止錯誤,使用集合的概念設計型別以及操作資料,可以讓程式更加清晰明確。