為何需要 fp-ts Array? 它能幫上什麼忙?
由於 TypeScript 不是真正的 type safe ,所以當存取 Array 不存在的元素時,會出現 undefined,並且也沒提供好用的操作。fp-ts Array 提供了一些進階的函數編程風格的操作。本文介紹了 TypeScript Array 的不足,以及為什麼需要這些操作。
舉個例子來說
const food = [];
console.log(food[0]); // undefined
不少語言都會抓出這種錯誤,例如 Haskell,便會告訴你嘗試從一個空陣列拿東西。
head []
*** Exception: Prelude.head: empty list
在 TypeScript 中,解法是這樣,其實,這就很 FP 了。
const foo = [1, 2, 3, 4, 5]
const sum = foo
.map((x) => x - 1)
.filter((x) => x % 2 === 0)
.reduce((prev, next) => prev + next, 0)
console.log(sum) // 6
如果要換到 fp-ts,寫法如下。
import * as A from 'fp-ts/lib/Array'
import { pipe } from 'fp-ts/lib/function'
const foo = [1, 2, 3, 4, 5]
const sum = pipe(
A.array.map(foo, (x) => x - 1),
A.filter((x) => x % 2 === 0),
A.reduce(0, (prev, next) => prev + next),
)
console.log(sum) // 6
你可能會覺得,為何要多此一舉,這是因為 fp-ts 將 TypeScript 的內建型別再加一層,放進 Functor 裡面,以區分 pure
跟有 side effect
的環境。
而在訪問陣列中的元素時保持類型安全,我們應該使用查找函數。
import * as A from 'fp-ts/lib/Array'
import { pipe } from 'fp-ts/lib/function'
pipe([1, 2, 3], A.lookup(1)) // { _tag: 'Some', value: 2 }
pipe([1, 2, 3], A.lookup(3)) // { _tag: 'None' }
若確定 Array 非空,則可以直接拿到值而不是 Option
。
import * as A from 'fp-ts/lib/Array'
import * as NEA from 'fp-ts/lib/NonEmptyArray'
const foo = [1, 2, 3]
if (A.isNonEmpty(foo)) {
const firstElement = NEA.head(foo) // 1
}
但當遇到 Array 不全是同一型別時,
// Homogenous
const foo = [1, 2, 3] // number[]
// Non Homogenous
const bar = [1, '2', 3] // (string | number)[]
要將所有元素相加,會讓程式碼看來不簡潔。
import * as A from 'fp-ts/lib/Array'
import * as NEA from 'fp-ts/lib/NonEmptyArray'
import * as O from 'fp-ts/lib/Option'
import * as E from 'fp-ts/lib/Either'
import { pipe } from 'fp-ts/lib/function'
const compute = (arr: Array<Foo | Bar>) =>
pipe(
A.array.partitionMap(arr, (a) =>
a._tag === 'Foo' ? E.left(a) : E.right(a),
),
({ left: foos, right: bars }) => {
const sum = A.array.reduce(foos, 0, (prev, foo) => prev + foo.f())
const max = A.array.reduce(bars, Number.NEGATIVE_INFINITY, (max, bar) =>
Math.max(max, bar.g()),
)
return sum * max
},
)
因此需要再了解 Monoid 來簡化。Monoids 是 Semigroup 的擴展,但它也包括空元素或預設元素。
我們可以定義一個 monoidMax
處理兩個同質值併產生單個同質值的類型,以及空值要怎麼處理。
import { Monoid } from 'fp-ts/lib/Monoid'
const monoidMax: Monoid<number> = {
concat: semigroupMax.concat,
empty: Number.NEGATIVE_INFINITY,
}
這東西的用處是可以簡化因為處理空值的複雜邏輯,這樣你就能讓程式碼變成
import { Monoid, monoidSum } from 'fp-ts/lib/Monoid'
const compute = (arr: Array<Foo | Bar>) =>
pipe(
A.array.partitionMap(arr, (a) =>
a._tag === 'Foo' ? E.left(a) : E.right(a),
),
({ left: foos, right: bars }) => {
const sum = A.array.foldMap(monoidSum)(foos, (foo) => foo.f())
const max = A.array.foldMap(monoidMax)(bars, (bar) => bar.g())
return sum * max
},
)
monoidSum
也是類似概念。只是當你要達到 type safety,要做到 generic 是非常挑戰的事情。