Functor 與 JS/TS
用數學角度出發去看 Functor 的定義什麼,我們怎麼用 Typescript
實現它,以及 Option
、List
幫我們做了什麼。
定義
根據維基百科描述「Functor 是範疇之間的映射」,而且要視為 Functor 要達成幾項條件:
先假設有 與 兩個範疇,而 functor 是 與 之間的映射
- 範疇中的每個 object 都與 之中每個 相關聯
- 將 中的每個 morphism 關聯到 中的 morphism 並且達成以下兩個條件:
- 對於 中的每個 ,
- 對於 中的 和 等所有 morphism ,
其他關於 Functor 定義的參考連結
每篇文章對 Functor 定義可能有些許不同,但你能找出他們之間的共同點,而那個共通點就是 Functor 的公認定義。
建立一個 Functor
我們需要一個從範疇 轉到範疇 的 function,而範疇 在程式中通常代表了所有原始型別與自定義型別之間的關係,範疇 表示另一個空間與範疇 互相對應。
先定義一個 Functor
interface Functor<T>{
value:T
}
// X -> F(X)
function of<T>(value:T): Functor<T> {
return {
value: value
}
}
of
就是將範疇 的 object 帶到範疇 的一個函式。
map / fmap
但是當我們今天在範疇 有個 morphism 是 ,那範疇 也要有相應的 morphism,但總不可能 有多少個 morphism, 也跟著建多少個 morphism,所以我們 需要一個 map
函式將 的 morphism 映射過去。
interface Functor<T>{
value:T
map<U>:(fn: (v:T) => U): Functor<U>;
}
// X -> F(X)
function of<T>(value:T): Functor<T> {
return {
value: value
map: (fn)=> of(fn(value))
}
}
我們來實際運行看看是否有達成 Functor 定義
// X
const x: number = 10;
// F(X)
const fx: Functor<number> = of(x);
->
const f = (x: number): string => x.toString();
const fx: Functor<number> = of(10);
const fy: Functor<string> = fx.map(f);
const f = (x) => x * x; // number -> number
const g = (y) => y.toString(); // number -> string
const fx: Functor<number> = of(10);
const fz1: Functor<string> = fx.map((x) => g(f(x)));
const fz2: Functor<string> = fx.map(f).map(g);
fz1
跟 fz2
結果是相同的
建完了 Functor 然後呢?
我們做了一個 Functor,但是看不出來有什麼用。確實沒有什麼用,就只是把值帶入 範疇裡而已,不過我們可以對 動手腳,並且只要我們有達成條件,那就還是 Functor。
創造一個「有」跟「沒有」的世界
既然我們要看出 Functor 的實際效果,那我們需要加點魔法,我們在寫程式時常常需要判斷欄位或者值存不存在,確定存在才往下執行。
function fn(value: number | undefined): numbner | undefined {
if(value !== undefined){
return value * 2 + 50;
}
return undefined;
}
看起來還好,不過當量多起來的時候你會變得很煩躁,每次都要檢查值存不存在,難道我不能只說我要怎麼算,是不是空的無所謂,空的就自動幫我跳過。
Option / Maybe
Option 跟 Maybe 就是在解決這個問題,讓我們來實作一個 Option
interface IOption<T> {
map: <U>(fn: (value: T) => U) => Option<U>;
}
class Some<T> implements IOption<T> {
value: T;
constructor(value: T) {
this.value = value;
}
map<U>(fn: (value: T) => U): Some<U> {
return new Some(fn(this.value))
}
}
class None implements IOption<undefined> {
value: undefined;
constructor(value?: any){
this.value = undefined;
}
map<U>(fn: (value: any) => U): None {
return this;
}
}
type Option<T> = Some<T> | None;
function of<T>(value: T): Option<T>{
if(value === undefined) return new None();
return new Some(value);
}
上方只是個範例,實作方法以及 null
該不該認定為空值都取決於開發者本身。
那我們把原本的範例修改一下:
function fn(value: Option<number>): Option<number>{
return value.map((v) => v * 2 + 50);
}
而在傳進去之前將一個不確定是不是 number
的值用 of
轉成 Option<number>
,一切一如往常,運算式照算並且不需要一直判斷,只有真正要用到這個值時你才要確認他到底存不存在。
所以我們在 of
跟 map
上做了一些調整,如果以單純 Type 來看,還是達成了 Functor 的條件
of = T -> Option<T>
- ->
const f = (x: number): string => x.toString();
const optionX: Option<number> = of(10);
const optionY: Option<string> = optionX.map(f);
const f = (x) => x * x; // number -> number
const g = (y) => y.toString(); // number -> string
const optionX: Option<number> = of(10);
const optionZ1: Option<string> = optionX.map((x) => g(f(x)));
const optionZ2: Option<string> = optionX.map(f).map(g);
optionZ1
與 optionZ2
結果會相同
非常神奇對吧,我們把判斷 有 跟 沒有 這件事隱藏起來了,而另一個常見的 Functor 都藏匿在我們日常開發中,那就是 Array / List。
Array / List,把 for loop 隱藏起來
在日常開發中我們常需要對整個 Array 每個值作一些運算,所以最原始的方法都是用 for loop。
function fn(arr: Array<number>): Array<string>{
const newArr = [];
for(num of arr){
newArr.push(num.toString());
}
return newArr;
}
好像也還好,但是當今天你有更多樣化的需求時怎麼辦?可能是時間轉換、取物件裡某個欄位等等,那你可能每次都要重新寫一個 for loop function 處理這件事,非常的麻煩且無意義。
在 JS 中其實已經有 map
method 讓你用了,那我們來看是否都達成了 Functor 的條件
const arr: Array<number> = Array.of(1); // 或任何其他創建 Array 的方式
- ->
const f = (x: number): string => x.toString();
const arrX: Array<number> = Array.of(1, 2, 3);
const arrY: Array<string> = arrX.map(f);
const f = (x) => x * x; // number -> number
const g = (y) => y.toString(); // number -> string
const arrX: Array<number> = of(1, 2, 3);
const arrZ1: Array<string> = arrX.map((x) => g(f(x)));
const arrZ2: Array<string> = arrX.map(f).map(g);
arrZ1
與 arrZ2
結果會相同
我們把枯燥乏味的 for loop 隱藏起來了!
結語
我們從數學上的定義與在 FP 語言中常出現的 Functor type 做了對應,以了解數學與程式之間如何配合,以及這些 Functor 解決了我們日常開發的哪些問題,甚至 Either
也可以自己試著了解並實作。