Help us understand the problem. What is going on with this article?

fp-tsでTypeScriptでも関数型プログラミング

この記事は ウェブクルー Advent Calendar 2019 10日目の記事です。
昨日は @wchikarusato さんの「steamゲームのmodから学ぶ設計」でした。

0. はじめに

普段はScalaをメインにWebアプリケーションの開発をしています。

たまにTypescriptも触るんですが、Scalaと比べるとコレクション操作で痒いところに手が届かなかったりして、ちょっとモヤモヤする時も。Scalaっぽく開発できないかな〜と思っていた所に、fp-ts という関数型プログラミングのエッセンスを取り入れられるライブラリが目についたので、興味本位で使ってみました。

Haskell, PureScript, Scalaから影響を受けているようで、メソッド名も関数型プログラミング界隈で馴染み深いものになっています。(まあ Haskell, PureScript は触ったことが無いのですが・・・)

1. インストール

普通に npm パッケージで公開されているので npm i fp-ts でインストールできます。

公式: https://github.com/gcanti/fp-ts

なお、CommonJS と ESModule の両方が用意されています。
import する際のパス指定でどちらを利用するか選択できます。

import { Option, some, none } from 'fp-ts/lib/Option'; // CommonJS
import { Option, some, none } from 'fp-ts/es6/Option'; // ESModule

webpack等のバンドラーを使用する場合、ESModule で import すると TreeShaking によりバンドルサイズの削減が期待できます。フロントエンド向けの開発ではバンドラーを利用する事が多いと思いますが、その際は ESModule で import すると良いかと思います。

※本記事内では Node.js 上で実行する為に CommonJS で import しています。

2. 全体的な利用方法

fp-ts では関数型プログラミングでお馴染みの Option, Either 等が独自の型で提供されますが、これらの型自体にメソッドが提供される訳ではありません。別途提供されている処理用の function を利用します。

import { some, isSome } from 'fp-ts/lib/Option';

const n = some(1);
// n.isSome() // メソッドがある訳ではない
isSome(n);    // 操作用の function を import して利用

function はカリー化されていますので、ロジックを部分適用して使います。

import { map } from 'fp-ts/lib/Array';

const f = map((s: string) => ({ value: s })); // ロジックを部分適用して
f(['foo', 'bar', 'baz']);                     // 実行する
// => [
//   { value: 'foo' },
//   { value: 'bar' },
//   { value: 'baz' }
// ]

3. Option

Option は以下のように定義されています。

export declare type Option<A> = None | Some<A>;

export interface None {
  readonly _tag: 'None';
}

export interface Some<A> {
  readonly _tag: 'Some';
  readonly value: A;
}

3-1. 生成

静的には somenone から生成できます。

fromNullable で値が null と undefined では None, それ以外は Some で生成できます。
fromPredicate で条件にあわない場合は None, あった場合は Some で生成できます。

import { none, some, fromNullable, fromPredicate } from 'fp-ts/lib/Option';

const isEven = (n: number) => n % 2 === 0;
console.log(none);                     // => { _tag: 'None' }
console.log(some(1));                  // => { _tag: 'Some', value: 1 }
console.log(fromNullable(undefined));  // => { _tag: 'None' }
console.log(fromNullable(null));       // => { _tag: 'None' }
console.log(fromPredicate(isEven)(1)); // => { _tag: 'None' }
console.log(fromPredicate(isEven)(2)); // => { _tag: 'Some', value: 2 }

3-2. function

利用頻度が高そうなもの、便利そうなものを抜粋します。

isNone/isSome

None, Some を判定します。

import { none, some, isNone, isSome } from 'fp-ts/lib/Option';

console.log(isSome(some(1))); // => true
console.log(isSome(none));    // => false
console.log(isNone(some(1))); // => false
console.log(isNone(none));    // => true

map

Some の場合、中の値を変換します。

import { none, some, map } from 'fp-ts/lib/Option';

const f = map((a: number) => a * 2);
console.log(f(some(5))); // => { _tag: 'Some', value: 10 }
console.log(f(none));    // => { _tag: 'None' }

flatten

ネストした Option を平坦化します。

import { none, some, flatten } from 'fp-ts/lib/Option';

console.log(flatten(some(some(1)))); // => { _tag: 'Some', value: 1 }
console.log(flatten(some(none)));    // => { _tag: 'None' }

getOrElse

Some の場合は中の値を、None の場合は代わりの値を取得します。

import { none, some, getOrElse } from 'fp-ts/lib/Option';

const f = getOrElse(() => 0);
console.log(f(none));    // => 0
console.log(f(some(1))); // => 1

fold

None の場合と Some の場合でそれぞれの処理を指定します。

import { none, some, fold } from 'fp-ts/lib/Option';

const f = fold(() => 'None', (a: number) => `Some(${a})`);
console.log(f(none));    // => None
console.log(f(some(1))); // => Some(1)

exists

条件に一致した値が存在するか判定します。

import { none, some, exists } from 'fp-ts/lib/Option';

const f = exists((a: number) => a === 2)
console.log(f(none));    // => false
console.log(f(some(1))); // => false
console.log(f(some(2))); // => true

tryCatch

例外が発生しうる function を渡し、戻り値を Option で表現できます。
function が正常に処理できた場合は Some に、例外が発生した場合は None になります。

import { tryCatch } from 'fp-ts/lib/Option';

const x = tryCatch(() => JSON.parse('{ "text": "test" }'));
const y = tryCatch(() => { throw 0 });
console.log(x); // => { _tag: 'Some', value: { text: 'test' } }
console.log(y); // => { _tag: 'None' }

4. Either

Either は以下のように定義されています。

export declare type Either<E, A> = Left<E> | Right<A>;

export interface Left<E> {
  readonly _tag: 'Left';
  readonly left: E;
}

export interface Right<A> {
  readonly _tag: 'Right';
  readonly right: A;
}

4-1. 生成

Option と同様に fromNullablefromPredicate が用意されています。

import { left, right, fromNullable, fromPredicate } from 'fp-ts/lib/Either';

const isEven = (n: number) => n % 2 === 0;

console.log(left(1));                                       // => { _tag: 'Left', left: 1 }
console.log(right(1));                                      // => { _tag: 'Right', right: 1 }
console.log(fromNullable('default')(null));                 // => { _tag: 'Left', left: 'default' }
console.log(fromNullable('default')(undefined));            // => { _tag: 'Left', left: 'default' }
console.log(fromNullable('default')('foobar'));             // => { _tag: 'Right', right: 'foobar' }
console.log(fromPredicate(isEven, n => `is Odd: ${n}`)(1)); // => { _tag: 'Left', left: 'is Odd: 1' }
console.log(fromPredicate(isEven, n => `is Odd: ${n}`)(2)); // => { _tag: 'Right', right: 2 }

4-2. function

isLeft/isRight

Left, Right を判定します。

import { left, right, isLeft, isRight } from 'fp-ts/lib/Either';

console.log(isLeft(right(1)));  // => false
console.log(isLeft(left(1)));   // => true
console.log(isRight(right(1))); // => true
console.log(isRight(left(1)));  // => false

map/mapLeft

map は Right に対して、mapLeft は Left に対して処理されます。

import { left, right, map, mapLeft } from 'fp-ts/lib/Either';

const length = (s: string) => s.length;
const f = map(length)
const g = mapLeft(length)
console.log(f(left('foo')));  // => { _tag: 'Left', left: 'foo' }
console.log(g(right('bar'))); // => { _tag: 'Right', right: 3 }
console.log((right('foo')));  // => { _tag: 'Right', right: 'foo' }
console.log((left('bar')));   // => { _tag: 'Left', left: 3 }

flatten

外側が Right の場合、ネストを平坦化します。

import { left, right, flatten } from 'fp-ts/lib/Either';

console.log(flatten(right(right(1)))); // => { _tag: 'Right', right: 1 }
console.log(flatten(right(left(1))));  // => { _tag: 'Left', left: 1 }
console.log(flatten(left(right(1))));  // => { _tag: 'Left', left: { _tag: 'Right', right: 1 } }
console.log(flatten(left(left(1))));   // => { _tag: 'Left', left: { _tag: 'Left', left: 1 } }

getOrElse

Right の場合はその値を、Left の場合は代わりの値を取得します。

import { left, right, getOrElse } from 'fp-ts/lib/Either';

const f = getOrElse(() => 0);
console.log(f(left(1)));  // => 0
console.log(f(right(1))); // => 1

orElse

getOrElse との違いは、戻り値が Either になる点です。

import { left, right, orElse } from 'fp-ts/lib/Either';

const f = orElse(() => right(1));
console.log(f(left(0)));  // => { _tag: 'Right', right: 1 }
console.log(f(right(2))); // => { _tag: 'Right', right: 2 }

fold

import { left, right, fold } from 'fp-ts/lib/Either';

const f = fold(
  (n: number) => `left : ${n}`,
  (n: number) => `right: ${n}`
);
console.log(f(left(0)));  // => left : 0
console.log(f(right(1))); // => right: 1

exists

Right に対して条件に一致した値が存在するか判定します。
Left の場合は false になります。

import { left, right, exists } from 'fp-ts/lib/Either';

const f = exists((n: number) => n % 2 === 0);
console.log(f(left(1)));  // => false
console.log(f(left(2)));  // => false
console.log(f(right(1))); // => false
console.log(f(right(2))); // => true

tryCatch

Option にもあった tryCatch の Either 版です。
function が正常に処理できた場合は Right に、例外が発生した場合は Left になります。

import { tryCatch } from 'fp-ts/lib/Either';

const f = (s: string) => tryCatch(() => JSON.parse(s), (e) => `${e}`);

console.log(f('{ "msg": "Hello, world!" }'));
// => { _tag: 'Right', right: { msg: 'Hello, world!' } }
console.log(f('(')); 
// => {
//   _tag: 'Left',
//   left: 'SyntaxError: Unexpected token ( in JSON at position 0'
// }

parseJson

実は上の JSON.parse() を tryCatch する処理はそのまま用意されていたりします。

import { parseJSON } from 'fp-ts/lib/Either';

console.log(parseJSON('{ "msg" : "Hello, world!" }', (e) => `${e}`));
// => { _tag: 'Right', right: { msg: 'Hello, world!' } }
console.log(parseJSON('(', (e) => `${e}`));
// => {
//   _tag: 'Left',
//   left: 'SyntaxError: Unexpected token ( in JSON at position 0'
// }

5. Array

リストについては独自の型を提供せず、標準でビルドインされている Array を利用します。

5-1. function

takeLeft/takeLeftWhile/takeRight

指定数ぶんだけ、配列の要素を取得します。
takeLeft で先頭方向から、takeRight で末尾方向から取得します。

takeLeftWhile は要素の取得条件を指定する事ができます。
先頭方向から取得条件に合致する要素を取得します。
取得条件に合致しない要素が出現した場合、それ以降の配列は取得されません。

import { takeLeft, takeLeftWhile, takeRight } from 'fp-ts/lib/Array';

const cond = (n: number) => n < 3;
console.log(takeLeft(2)([1, 2, 3, 4, 5]));         // => [1, 2]
console.log(takeRight(2)([1, 2, 3, 4, 5]));        // => [4, 5]
console.log(takeLeftWhile(cond)([1, 2, 3, 2, 1])); // => [1, 2]

dropLeft/dropLeftWhile/dropRight

指定数ぶんだけ、配列の要素を削除します。
dropLeft で先頭方向から、dropRight で末尾方向から削除します。

dropLeftWhile は要素の削除条件を指定する事ができます。
先頭方向から削除条件に合致する要素を削除します。
削除条件に合致しない要素が出現した場合、それ以降の配列は保持されます。

import { dropLeft, dropLeftWhile, dropRight } from 'fp-ts/lib/Array';

const cond = (n: number) => n < 3;
console.log(dropLeft(2)([1, 2, 3, 4, 5]));         // => [3, 4, 5]
console.log(dropRight(2)([1, 2, 3, 4, 5]));        // => [1, 2, 3]
console.log(dropLeftWhile(cond)([1, 2, 3, 2, 1])); // => [3, 2, 1]

foldLeft/foldRight

配列の畳み込みを行います。

foldLeft は配列を先頭要素と先頭の後続要素に分割します。
foldRight は配列を先頭要素と先頭の後続要素に分割し。

const f: (as: number[]) => number = foldLeft(
  // 配列が空の場合
  () => 0,
  // n: 先頭要素, tail: 先頭以外の要素
  (n, tail) => tail.length,
);
console.log(f([1, 2, 3, 4, 5])); // => 4
console.log(f([]));              // => 0

const g: (as: number[]) => number = foldRight(
  // 配列が空の場合
  () => 0,
  // n: 末尾要素, init: 末尾以外の要素
  (init, n) => n,
);
console.log(g([]));              // => 0
console.log(g([1, 2, 3, 4, 5])); // => 5

reduce/reduceWithIndex/reduceRight/reduceRightWithIndex

配列の各要素に対して畳み込み処理を行います。

reduce は配列を先頭方向から、reduceRight は配列の末尾方向から順に処理します。
両者間で引数上のアキュムレータ (※下記ソース例でいうacc) の位置が異なる点に注意しましょう。

~WithIndex はインデックス値をあわせて取得できます。

import { reduce, reduceWithIndex } from 'fp-ts/lib/Array';

const f = reduce(0, (acc, a: number) => acc + a);
const g = reduceWithIndex('', (i, acc, a) => `${acc}${i}:${a}`);
console.log(f([1, 2, 3, 4, 5]));       // => 15
console.log(g(['foo', 'bar', 'baz'])); // => 0:foo1:bar2:baz
import { reduceRight, reduceRightWithIndex } from 'fp-ts/lib/Array';

const f = reduceRight(0, (a: number, acc) => acc + a);
const g = reduceRightWithIndex('', (i, a, acc) => `${acc}${i}${a}`);
console.log(f([1, 2, 3, 4, 5]));       // => 15
console.log(g(['foo', 'bar', 'baz'])); // => 0:foo1:bar2:baz

makeBy

生成条件を指定して配列を生成します。

import { makeBy } from 'fp-ts/lib/Array';

const f = (n: number) => n * n;
console.log(A.makeBy(5, f)); // => [ 0, 1, 4, 9, 16 ]

range

数値の範囲指定で配列を生成します。

import { range } from 'fp-ts/lib/Array';

console.log(range(1, 5)); // => [ 1, 2, 3, 4, 5 ]

replicate

要素数と値を指定して配列を生成します。

import { replicate } from 'fp-ts/lib/Array';

console.log(replicate(3, 'foobar')); // => [ 'foobar', 'foobar', 'foobar' ]

head/last

head で先頭要素を、last で末尾要素を取得します。
戻り値は Option となります。

import { head, last } from 'fp-ts/lib/Array';

console.log(head([1, 2, 3])); // => { _tag: 'Some', value: 1 }
console.log(last([1, 2, 3])); // => { _tag: 'Some', value: 3 }
console.log(head([]));        // => { _tag: 'None' }

tail

先頭要素を除いた配列を取得します。
戻り値は Option となります。

ちょっとした注意点として、要素が1つしかない配列を処理すると Some で空配列が返却されます。

import { tail } from 'fp-ts/lib/Array';

console.log(tail([1, 2, 3])); // => { _tag: 'Some', value: [ 2, 3 ] }
console.log(tail([1]));       // => { _tag: 'Some', value: [] }
console.log(tail([]));        // => { _tag: 'None' }

lookup

インデックス値を指定して要素を取得します。
戻り値は Option となります。

import { lookup } from 'fp-ts/lib/Array';

console.log(lookup(1, [1, 2, 3])); // => { _tag: 'Some', value: 2 }
console.log(lookup(4, [1, 2, 3])); // => { _tag: 'None' }

isEmpty/isNonEmpty

配列の要素有無を判定します。
なお isNonEmpty は TypeGuard の機能も持っており、変数を isNonEmpty で条件判定した場合、その条件判定が有効なブロックスコープ内では 変数の型が NonEmptyArray として扱われるようになります。

import { isEmpty, isNonEmpty } from 'fp-ts/lib/Array';

const a = [1, 2, 3];
console.log(isEmpty([]));    // => true
console.log(isEmpty(a));     // => false
console.log(isNonEmpty([])); // => false
console.log(isNonEmpty(a));  // => true

if (isNonEmpty(a)) {
  a // type: NonEmptyArray<number>
}
a // type: number[]

rotate

要素の順序をずらします。

import { rotate } from 'fp-ts/lib/Array';

console.log(rotate(2)([1, 2, 3, 4, 5]));  // => [ 4, 5, 1, 2, 3 ]
console.log(rotate(-2)([1, 2, 3, 4, 5])); // => [ 3, 4, 5, 1, 2 ]

reverse

要素の順序を逆転させます。

import { reverse } from 'fp-ts/lib/Array';

console.log(reverse([1, 2, 3])); // => [ 3, 2, 1 ]

union

指定した全ての配列を結合し、その上で重複値を排除した配列を生成します。
等値比較条件には Eq を使用します。

import { union } from 'fp-ts/lib/Array';
import { eqNumber } from 'fp-ts/lib/Eq';

console.log(union(eqNumber)([1, 2], [2, 3])) // => [ 1, 2, 3 ]

uniq

重複を排除した配列を返却します。
等値比較条件には Eq を使用します。

import { uniq } from 'fp-ts/lib/Array';
import { eqNumber } from 'fp-ts/lib/Eq';

console.log(uniq(eqNumber)([1, 2, 2, 3])); // => [ 1, 2, 3 ]

intersection

積集合となる配列を返却します。
返却値の配列構造は、intersection に渡す (xs, ys) のうち xs 側の配列がベースになります。
等値比較条件には Eq を使用します。

import { intersection } from 'fp-ts/lib/Array';
import { eqNumber } from 'fp-ts/lib/Eq';

console.log(intersection(eqNumber)([1, 2, 2], [2, 3])) // => [ 2, 2 ]
console.log(intersection(eqNumber)([1, 2], [2, 2, 3])) // => [ 2 ]

difference

差集合となる配列を返却します。
返却値の配列構造は、difference に渡す (xs, ys) のうち xs 側の配列がベースになります。
等値比較条件には Eq を使用します。

import { difference } from 'fp-ts/lib/Array';
import { eqNumber } from 'fp-ts/lib/Eq';

console.log(difference(eqNumber)([1, 2], [2, 3]));    // => [ 1 ]
console.log(difference(eqNumber)([1, 1, 2], [2, 3])); // => [ 1, 1 ]
console.log(difference(eqNumber)([1, 2], [1, 2, 3])); // => []

sort/sortBy

sort では Ord を比較条件に使用し、要素順序の並び替えを行います。

import { sort } from 'fp-ts/lib/Array';
import { ordNumber } from 'fp-ts/lib/Ord';

console.log(sort(ordNumber)([1, 5, 2, 4, 3])); // => [ 1, 2, 3, 4, 5 ]

sortBy ではソート条件を指定し、要素順序の並び替えを行う事ができます。

import { sortBy } from 'fp-ts/lib/Array';
import { ord, ordString, ordNumber } from 'fp-ts/lib/Ord';

interface User {
  id: number;
  kind: string;
  name: string;
}

// ソート条件の作成 (※ソート条件は複数持つこともできます)
const sortCond = ([
  ord.contramap(ordString, (user: User) => user.kind), // 第1ソート条件: kind
  ord.contramap(ordNumber, (user: User) => user.id),   // 第2ソート条件: id
]);

const users = [
  { id: 2, kind: 'A', name: 'tanaka' },
  { id: 5, kind: 'A', name: 'suzuki' },
  { id: 4, kind: 'B', name: 'yamada' },
  { id: 3, kind: 'B', name: 'sakurai' },
  { id: 1, kind: 'A', name: 'sato' },
];

console.log(sortBy(sortCond)(users));
// => [
//   { id: 1, kind: 'A', name: 'sato' },
//   { id: 2, kind: 'A', name: 'tanaka' },
//   { id: 5, kind: 'A', name: 'suzuki' },
//   { id: 3, kind: 'B', name: 'sakurai' },
//   { id: 4, kind: 'B', name: 'yamada' }
// ]

filter/filterWithIndex

filterWithIndex はインデックス値があわせて取得できます。

import { filter, filterWithIndex } from 'fp-ts/lib/Array';

const f = filter((s: string) => s.startsWith('b'));
const g = filterWithIndex((i: number, s: string) => i >= 2 && s.startsWith('b'));
console.log(f(['foo', 'bar', 'baz'])); // => [ 'bar', 'baz' ]
console.log(g(['foo', 'bar', 'baz'])); // => [ 'baz' ]

map/mapWithIndex

mapWithIndex はインデックス値があわせて取得できます。

import { map, mapWithIndex } from 'fp-ts/lib/Array';

const f = map((s: string) => ({ value: s }));
const g = mapWithIndex((i: number, s: string) => ({ index: i, value: s }));
console.log(f(['foo', 'bar', 'baz']));
// => [
//   { value: 'foo' },
//   { value: 'bar' },
//   { value: 'baz' }
// ]
console.log(g(['foo', 'bar', 'baz']));
// => [
//   { index: 0, value: 'foo' },
//   { index: 1, value: 'bar' },
//   { index: 2, value: 'baz' }
// ]

flatten/compact

Option の配列を平坦化する場合は compact を使います。

import { flatten, compact } from 'fp-ts/lib/Array';

console.log(flatten([[1], [2, 3], [], [4]]));      // => [1, 2, 3, 4]
console.log(compact([]));                          // => []
console.log(compact([some(1), some(2), some(3)])); // => [1, 2, 3]
console.log(compact([some(1), none, some(3)]));    // => [1, 3]

findFirst/findIndex/findLast/findLastIndex

findFirst は先頭から見て、findLast は末尾から見て、最初に Hit した要素を取得します。
findIndex, findLastIndex は Hit した要素のインデックス値を取得します。
戻り値は Option となります。

import { findFirst, findIndex, findLast, findLastIndex } from 'fp-ts/lib/Array';

const cond1 = (user: { id: number, name: string }) => user.name === 'tanaka';
const cond2 = (user: { id: number, name: string }) => user.name === 'sato';
const users = [
  { id: 1, name: 'yamada' },
  { id: 2, name: 'tanaka' },
  { id: 3, name: 'sakurai' },
  { id: 4, name: 'suzuki' },
  { id: 5, name: 'tanaka' },
];

console.log(A.findFirst(cond1)(users));     // => { _tag: 'Some', value: { id: 2, name: 'tanaka' } }
console.log(A.findIndex(cond1)(users));     // => { _tag: 'Some', value: 1 }
console.log(A.findLast(cond1)(users));      // => { _tag: 'Some', value: { id: 5, name: 'tanaka' } }
console.log(A.findLastIndex(cond1)(users)); // => { _tag: 'Some', value: 4 }
console.log(A.findLast(cond2)(users));      // => { _tag: 'None' }

partition

条件を指定し、条件に一致しなかったものを left に、一致した要素を right にまとめます。

import { partition } from 'fp-ts/lib/Array';

const isEven = (n: number) => n % 2 === 0;
console.log(partition(isEven)([1, 2, 3, 4, 5])); // => { left: [ 1, 3, 5 ], right: [ 2, 4 ] }

6. NonEmptyArray

要素が最低1つ以上ある事を保証している型です。
プロパティ 0 の値が存在している Array として定義されています。

export interface NonEmptyArray<A> extends Array<A> {
    0: A;
}

6-1. 生成

import { of, cons, snoc } from 'fp-ts/lib/NonEmptyArray';

const x: NonEmptyArray<number> = of(1);
const y: NonEmptyArray<number> = cons(1, [2, 3]);
const z: NonEmptyArray<number> = snoc([1, 2], 3);
console.log(x); // => [1]
console.log(y); // => [1, 2, 3]
console.log(z); // => [1, 2, 3]

6-2. 説明

NonEmptyArray は Array を継承しているので、通常の配列と同様の操作が可能です。
違いとしては、通常の配列を操作する関数群とは別に、NonEmptyArray を操作する関数群が別途用意されているので、そちらを利用する事でセーフティーな実装ができるようになっています。

import * as A from 'fp-ts/lib/Array';
import * as N from 'fp-ts/lib/NonEmptyArray';

const n = [1, 2, 3];
A.head(n); // type: Option<number>

const m = N.cons(1, [2, 3]);
N.head(m); // type: number

6-3. 注意点

NonEmptyArray 自体を通常の Array と同じようにコレクション操作を行うと、各メソッドの戻り値が NonEmptyArray になっていないので、型情報が失われてしまいます。(当然といえば当然なのですが...)

また、fp-ts/lib/Array が提供する function も同様に戻り値が Array になります。

import * as A from 'fp-ts/lib/Array';
import * as N from 'fp-ts/lib/NonEmptyArray';

const n: N.NonEmptyArray<number> = N.cons(1, [2, 3]);
A.map((a: number) => a * 2)(n); // type: number[]
n.concat([4, 5]);               // type: number[]

型情報を維持してコレクション操作を行いたい場合、fp-ts/lib/NonEmptyArray が提供する function を利用しましょう。

6-4. function

fp-ts/lib/Array では提供されず fp-ts/lib/NonEmptyArray でのみ提供される function もあります。

groupBy

グルーピング条件を指定し、配列をグループ毎に分割します。

import { groupBy } from 'fp-ts/lib/NonEmptyArray';

const f = groupBy((s: string) => String(s.length));
console.log(f(['foo', 'bar', 'foobar'])) // => { '3': [ 'foo', 'bar' ], '6': [ 'foobar' ] }

7. パイプライン処理

fp-ts/lib/pipeable で提供されている pipe という function でパイプライン処理ができます。

fp-ts ではそれぞれの型にメソッドがある訳ではないので、メソッドチェーン式に記述する事はできません。
関数を順次適用していく場合は、こちらを利用するとネスト地獄にならず見やすく記述できます。

import { none, some } from 'fp-ts/lib/Option';
import { pipe } from 'fp-ts/lib/pipeable';
import { compact, map, sort } from 'fp-ts/lib/Array';
import { ordNumber } from 'fp-ts/lib/Ord';

const array = [
  some('strawberry'),
  none,
  some('apple'),
];

const n = pipe(
  array,
  compact,                // ['strawberry', 'apple']
  map(a => a.length * 2), // [20, 10]
  sort(ordNumber),        // [10, 20]
);

console.log(n); // => [10, 20]

8. 関数合成

fp-ts/lib/function が提供する flow で関数合成ができます。

import { flow } from 'fp-ts/lib/function';

const len = (s: string): number => s.length;
const double = (n: number): number => n * 2;
const f = flow(len, double);
console.log(f("foobar")); // => 12

9. 最後に

fp-ts の浅い所しか触れていませんが、結構使いやすいと思いました。

本記事では各種機能のほんの一部分しか紹介できていませんので、ぜひAPIリファレンスを参照してみて下さい。
https://gcanti.github.io/fp-ts/modules/

テストコードを見ると実装例まで一緒になっているのでわかりやすいです。
https://github.com/gcanti/fp-ts/tree/master/test

もっと深くキャッチアップしていきたい場合は、下記等にも情報がまとめられています。
https://gcanti.github.io/fp-ts/introduction/learning-resources.html

明日は @wc-kobayashiT さんです。よろしくお願いいたします。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした