4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

map, filter を object にも使う方法【fp-ts の紹介】

Last updated at Posted at 2021-05-08

結論

map, filter を object にも使う方法の一つとして、fp-ts というライブラリを利用する という方法があります。
fp-ts の map, filter は基本的には pipe関数と共に用います。1

.ts
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 として実行します。
まず、filtermap を 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(配列), 戻り値:配列 の関数」』の関数

上記 filtermap は関数を返す関数(高階関数)です。
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関数 を使うと、下記のように記載できます。

.mjs
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

  1. flow関数も頻繁に使いますが、本稿では割愛します。
    以下は filter, map を object に使うシンプルな例です。

  2. ここでは“手動”でカリー化していますが、“自動”でカリー化する関数(curry)を提供している有名な関数型プログラミングライブラリ(Ramda)があります。(もっとも、TypeScriptでは型に難があり筆者はcurryはほぼ使いません。)
    関数を返す関数(高階関数)である filter と map に それぞれ、 関数isEven と 関数double を渡すことで、
    配列を受け取って、配列を返す関数(下記 filterIsEven, mapDouble)を作成します。

  3. 同じ pipe という名前の関数でも、別のライブラリ(例えば ramda)だと機能が異なっている場合があります。

  4. TypeScript 未学習者向けに補足すると、ここでは「Record は Object」とざっくりとした理解で良いです。(Record は Object の部分集合。)

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?