結論
map
, filter
を object にも使う方法の一つとして、fp-ts というライブラリを利用する という方法があります。
fp-ts の map
, filter
は基本的には pipe
関数と共に用います。1
import { pipe } from "fp-ts/function"
import { array as A, record as R } from "fp-ts"
// utils
const isEven = (n: number) => n % 2 === 0
const double = (n: number) => n * 2
// data
let data1 = [1, 2, 3, 4, 5, 6]
let data2 = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 }
// data1. filter(isEven). map(double) // ES6
const result1 = pipe(data1, A.filter(isEven), A.map(double)) // fp-ts Array
const result2 = pipe(data2, R.filter(isEven), R.map(double)) // fp-ts Record ← filter, map を object に使う例
console.log(result1) // [ 4, 8, 12 ]
console.log(result2) // { b: 4, d: 8, f: 12 }
対象読者
- Array の
map
,filter
を Object にも使いたいと思ったことがある方 - 解説は JavaScript で行うため TypeScript の知識は不要です
- 関数型プログラミングの知識は不要です
解説
「結論」に記載した以下のコードについて解説します。
const result2 = pipe(data2, R.filter(isEven), R.map(double))
1. メソッドチェーンで書いた場合
下記の単純なコードを fp-ts で書き直す流れで説明していきます。
// utils
const isEven = (n) => n % 2 === 0
const double = (n) => n * 2
// data
let data = [1, 2, 3, 4, 5, 6]
let result
// ES6
result = data.filter(isEven).map(double) // 1. メソッドチェーン
console.log(result) // [ 4, 8, 12 ]
2. パイプラインで書いた場合
data.filter(isEven)
では Array.prototype.filter
関数内の this を data
、引数を 関数isEven
として実行します。
まず、filter
と map
を this に依存するメソッドではなく、引数のみに依存する関数として定義します。
// filter = (fn) => (thisArg) => thisArg.filter(fn) // Array.prototype.filter を メソッド として利用
const filter = (fn) => (thisArg) => Array.prototype.filter.call(thisArg, fn) // Array.prototype.filter を 関数 として利用
const map = (fn) => (thisArg) => Array.prototype.map.call(thisArg, fn) // filter, map は 『引数:fn(関数), 戻り値:「引数:thisArg(配列), 戻り値:配列 の関数」』の関数
上記 filter
と map
は関数を返す関数(高階関数)です。
const filter = (fn , thisArg) => Array.prototype.filter.call(thisArg, fn)
のように引数を2つ持つ関数として記載しないのは パイプラインによる処理 を可能にするためです。(後述します。)
なお、
const filter = (fn , thisArg) => Array.prototype.filter.call(thisArg, fn)
を
const filter = (fn)=>(thisArg) => Array.prototype.filter.call(thisArg, fn)
のように記載することを カリー化 と言います。2
const filterIsEven = filter(isEven) // filterIsEven は「引数:配列, 戻り値: 配列」の関数
const mapDouble = map(double) // mapDouble は「引数:配列, 戻り値: 配列」の関数
filterIsEven
, mapDouble
は一つの引数を受け取って一つの値を返す形の関数なので、後述するようにパイプライン処理ができるようになります。
(本稿では 後述の pipe
関数 を使った処理を「パイプライン処理」と呼びます。)
以下のように、filterIsEven
, mapDouble
を使って、結果([ 4, 8, 12 ])を得ることができます。
const filtered = filterIsEven(data)
const result = mapDouble(filtered)
console.log(result) // [ 4, 8, 12 ]
関数呼び出しをネストすることで、使う変数を減らす書き方をすると、下記のようになります。
result = mapDouble(filterIsEven(data))
console.log(result) // [ 4, 8, 12 ]
しかし、上記のように記載すると、関数(ここでは、filterIsEven
, mapDouble
など)が増えた場合にネストが深くなり少し読みずらく(書きずらく)なります。
そこで、pipe
関数 を使うと、下記のように記載できます。
import { pipe } from "fp-ts/lib/function.js"
result = pipe(data, filterIsEven, mapDouble)
console.log(result) // [ 4, 8, 12 ]
// fp-ts の pipe を簡略化する場合(引数を3つに限定する場合)、
// const pipe = (a, ab, bc) => bc(ab(a)) と記載できます。
// ※ a はデータ、ab, bc は引数が1つの関数
fp-ts の pipe
関数3 は 第一引数 に処理対象となるデータを取り、第二引数以降は、『引数を一つのみ受け取り何らかの値を返す関数』を取ります。そのため、filterIsEven
, mapDouble
を引数が一つの関数になるように作りました。
filterIsEven
, mapDouble
を 一つ前の状態 (isEven
, double
をそれぞれ filter
, map
に渡す前の状態) に戻すと下記のようになります。
// data. filter(isEven). map(double) // 1. メソッドチェーン (前掲したもの)
result = pipe(data, filter(isEven), map(double)) // 2. パイプライン
console.log(result) // [ 4, 8, 12 ]
こうして並べてみると、メソッドチェーン と パイプライン は書き方が似ています。
メソッドチェーン は this(=data
)が、
パイプラインでは data
自体 が
左から右に順に(パイプを通るかのように?)評価されます。
なお、pipe (function.ts の最下部あたり) は引数が 1個でも、20個でも良いように実装されています。
3. fp-ts を使って書いた場合
Array用に実装された関数(高階関数)であるfilter
,map
を使用します。
import * as A from "fp-ts/lib/Array.js" //追加
// data. filter(isEven). map(double) // 1. メソッドチェーン
// pipe(data, filter(isEven), map(double)) // 2. パイプライン (filter, map は Array.prototype 由来)
result = pipe(data, A.filter(isEven), A.map(double)) // 3. パイプライン (filter, map は fp-ts/lib/Array.js 由来)
console.log(result) // [ 4, 8, 12 ]
4. Object に map, filter を使う場合
(タイトル回収ですが)
Object型のデータを扱う場合、Object(Record4)用に実装された高階関数であるfilter
,map
を使用します。
import * as R from "fp-ts/lib/Record.js" //追加
data = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 }
// data. filter(isEven). map(double) // 1. メソッドチェーン
// pipe(data, filter(isEven), map(double)) // 2. パイプライン (filter, map は Array.prototype 由来)
// pipe(data, A.filter(isEven), A.map(double)) // 3. パイプライン (filter, map は fp-ts/lib/Array.js 由来)
result = pipe(data, R.filter(isEven), R.map(double)) // 4. パイプライン (filter, map は fp-ts/lib/Record.js 由来)
console.log(result) // { b: 4, d: 8, f: 12 }
5. (補足)もし Pipeline operator が使えるようになったら
Pipeline 演算子 (「|>」) を JavaScript に導入しようという 提案 があり、これを使えるようになったら pipe
関数 は不要になります。
// data. filter(isEven). map(double) // 1. メソッドチェーン
// pipe(data, filter(isEven), map(double)) // 2. パイプライン
result = data |> filter(isEven) |> map(double) // 5. Pipeline operator
fp-ts に関する補足
-
map
は(配列等の)各要素を引数に取る関数しか扱いませんが、「index (object なら key) +各要素」を引数に取る関数を扱えるmapWithIndex
も用意されています -
map
と同様に、reduce
も用意されています - Array や Object (Record) 以外にも、Map や Set、Tuple(タプル / fp-tsでは「要素が2つの配列」を Tuple としています)など、様々なデータ型を扱う関数(or 高階関数)が用意されています
- Array や Object, Map, Set 等の既存の“構造” だけでなく、Option や Either といった関数型プログラミング言語特有の“構造”、及びそれらを扱う関数も用意されています
Gist
-
flow
関数も頻繁に使いますが、本稿では割愛します。
以下はfilter
,map
を object に使うシンプルな例です。 ↩ -
ここでは“手動”でカリー化していますが、“自動”でカリー化する関数(curry)を提供している有名な関数型プログラミングライブラリ(Ramda)があります。(もっとも、TypeScriptでは型に難があり筆者はcurryはほぼ使いません。)
関数を返す関数(高階関数)である filter と map に それぞれ、 関数isEven
と 関数double
を渡すことで、
配列を受け取って、配列を返す関数(下記filterIsEven
,mapDouble
)を作成します。 ↩ -
同じ
pipe
という名前の関数でも、別のライブラリ(例えば ramda)だと機能が異なっている場合があります。 ↩ -
TypeScript 未学習者向けに補足すると、ここでは「Record は Object」とざっくりとした理解で良いです。(Record は Object の部分集合。) ↩