iT邦幫忙

2023 iThome 鐵人賽

DAY 8
0
Software Development

Should I use fp-ts系列 第 8

[Should I use fp-ts?] Day 08 - fp-ts: Option map

  • 分享至 

  • xImage
  •  

在本系列文中,所有的程式碼以及測試都可以在 should-i-use-fp-ts 找到,今日的範例放在 src/day-08 並且有習題和測試可以讓大家練習。

O.map

延續昨天的話題, 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) 便開始運算,如果 xNone 則回傳 nonexSome 則將 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 有習題和測試可以練習。

Reference

Option.ts | fp-ts


上一篇
[Should I use fp-ts?] Day 07 - fp-ts: Option 型別以及建構子
下一篇
[Should I use fp-ts?] Day 09 - fp-ts: Option flatten, flatMap(chain)
系列文
Should I use fp-ts25
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言