由於 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 是非常挑戰的事情。