在本系列文中,所有的程式碼以及測試都可以在 should-i-use-fp-ts 找到,今日的範例放在 src/day-08 並且有習題和測試可以讓大家練習。
延續昨天的話題, Option 有以下三種型別,所有變數經過 Option 處理後,都會是 None 或是 Some<A>,這樣設計有助於掌握變數當前的狀態並且統一處理不合法的數值。
export type None = { readonly _tag: 'None' };
export type Some<A> = { readonly _tag: 'Some'; readonly value: A };
export type Option<A> = None | Some<A>;
const one = O.of(1) // { readonly _tag: 'Some'; readonly value: 1 };
但在這種情況下要使用 Option 內容的數值會變的比較繁瑣:
const one = O.of(1); // { _tag: 'Some', value: 1 }
type IncO = (x: O.Option<number>) => O.Option<number>;
const incO: IncO = x => x._tag === 'None' ? O.none : O.some(x.value + 1);
const two = pipe(
one, // { _tag: 'Some', value: 1 }
incO, // { _tag: 'Some', value: 2 }
);
每一個處理都需要做一個 Option 的版本才能享受到使用 Option 的好處但會讓程式碼變的繁複,所以我們需要建立一個函數 map 來處理 將數值從 Option 拿出來,並且將結果放回 Option 的容器之中 的情境,以下就是 O.map 的實作。
export type Map = <A, B>(f: (a: A) => B) => (x: Option<A>) => Option<B>;
export const map: Map = f => x => x._tag === 'None' ? none : some(f(x.value));
O.map 會接收一個函數 (f: A => B), 之後接收到變數 (x: A) 便開始運算,如果 x 是 None 則回傳 none,x 是 Some 則將 f(x.value) 的結果放入 some 之中,如此一來就可以使用原始的 function 來處理上面的情況。
const twoMap = pipe(
one, // { _tag: 'Some', value: 1 }
O.map(x => x + 1), // { _tag: 'Some', value: 2 }
);
const noneCase = pipe(
O.none, // { _tag: 'None' }
O.map(x => x + 1), // { _tag: 'None' }
);
如此一來就可以在確保每一步過程合法的情況下不斷運算下去,下面是一個範例:
期末考調分:每個人的份數為 1.2 倍後四捨五入,超過 100 分則以 100 分計算,調分後不足 60 分則視為 None。
type IsFailed = (x: O.Option<number>) => O.Option<number>;
const isFailed: IsFailed = (x) => {
if (x._tag === 'None') return O.none;
return x.value > 60 ? O.some(x.value) : O.none;
};
type AdjustScore = (x: number) => O.Option<number>;
const adjustScore: AdjustScore = flow( // use 40 as an example
O.of, // { _tag: 'Some', value: 40 }
O.map(x => x * 1.2), // { _tag: 'Some', value: 48 }
isFailed, // { _tag: 'None' }
O.map(Math.round), // { _tag: 'None' }
O.map(x => x > 100 ? 100 : x), // { _tag: 'None' }
);
const studentA = adjustScore(40); // { _tag: 'None' }
const studentB = adjustScore(60); // { _tag: 'Some', value: 72 }
const studentC = adjustScore(100); // { _tag: 'Some', value: 100 }
這邊可以看到 isFailed 特別獨立出來寫而不是使用 O.map,這是因為在這裡使用 O.map 的話,會導致中途有 Option 嵌套,而無法順利運算下去,等到明天學 flatMap 之後就可以處理的更加漂亮。
/**
* We can not use `O.map` to implement `adjustScore2`
* because `O.map` will wrap the result in `Option` again.
*/
const adjustScore2: AdjustScore = flow(
O.of, // Option<number>
O.map(x => x * 1.2), // Option<number>
O.map(x => x > 60 ? O.some(x) : O.none), // Option<Option<number>>
O.map(Math.round), // never: type mismatch
O.map(x => x > 100 ? 100 : x), // never
);
今天的主題在 should-i-use-fp-ts src/day-08 有習題和測試可以練習。