この記事は ウェブクルー 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. 生成
静的には some
や none
から生成できます。
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 と同様に fromNullable
や fromPredicate
が用意されています。
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 さんです。よろしくお願いいたします。