for文ベタ書きから関数型へリファクタリングしていく記事です。
ベタに書く
各項目を3倍するプログラムを考えてみましょう
const input = [1,2,3,4,5,6,7,8,9,10]
const output = []
for(const item of input) {
output.push(item * 3)
}
さて次は各項目の偶数だけを抽出するプログラムを考えてみましょう
const input = [1,2,3,4,5,6,7,8,9,10]
const output = []
for(const item of input) {
if(item % 2 === 0) {
output.push(item)
}
}
ちょっとギョッとするコードですが、これらのプログラムはもう少しシンプルに副作用がなくできますね
なぜギョッとするかというとoutputにpushしている部分に副作用があるからです。
高階関数を使ってみる
const input = [1,2,3,4,5,6,7,8,9,10]
const output = input.map(item => {
return item * 3
})
if文の方はこんな感じ
// map
const input = [1,2,3,4,5,6,7,8,9,10]
const output = input.filter(item => {
return item % 2 === 0
})
最初のコードと比べるとネストループが単純になりoutputにpushしなくなるなど無駄で副作用のある変数がなくなりました。
さてではfilterとmapの操作を同時にする以下の処理を考えてみましょう
const output = input
.map(i => i * 2)
.filter(i => i % 3 === 0)
.filter(i => i < 10)
このコードは処理が分かりやすくよいのですが、filter、map毎に全ての項目を計算する必要があり計算量が多くなっていまします。
この処理で言えば10 * 3回配列を捜査してその都度中間の配列を生成することになります。効率的ではないですね
効率良いコードを書きたい
本当に欲しいのは各項目が一度だけ中間の計算を通って出力する処理です
for文を使えば簡単ですね。
const input = [1,2,3,4,5,6,7,8,9,10]
for(const i of input) {
if(i % 2 === 0) {
const item = i * 3
if(item < 10) {
output.push(item)
}
}
}
最初に戻ってしまいました。ほしいのは副作用がなく分かりやすく、計算も各項目ずつだけで済む処理です。
reduceを使う
reduceを使ってみましょう。reduceを使えば簡単にfilterとmapを実装することができます。
mapです
const input = [1,2,3,4,5,6,7,8,9,10]
const output = input.reduce((acc, item) => {
return [...acc, item];
})
filterです
const input = [1,2,3,4,5,6,7,8,9,10]
const output = input.reduce((acc, item) => {
if(item % 2 === 0) {
return [...acc, item];
}
return acc
})
ちなみに[...acc, item]
は配列に項目を一つ追加して配列を返す処理です。スプレッドシンタックスのおかげですごいシンプルにかけます。
reduceをもっと抽象的に使う
とreduce関数を使ってmapやfilterを実装できることはわかりました
次はreduce関数を使ってこのようなmapTやfilterTを定義してみましょう。
ここら辺からちょっと抽象的になってきます
const mapT = transformFn => reduceFn => (acc, item) => {
return reduceFn(acc, transformFn(item))
}
const filterT = testFn => reduceFn => (acc, item) => {
if(testFn(item)) {
return reduceFn(acc, item)
}
return acc
}
この関数はreduceの中で使用されることを想定しています。各項目はtransformFnにて計算され、reduceFnに引き渡されます。
(acc, item)
の部分が直接reduce関数から呼ばれる部分でこのmapTやfilterTはreduceから呼ばれる関数の一次受けの役割を果たします。
const input = [1,2,3,4,5,6,7,8,9,10]
input.reduce(
mapT((item) => item * 2)((acc, item) => {
return [...acc, item]
})
, [])
input.reduce(
filterT((item) => item % 2 === 0)((acc, item) => {
return [...acc, item]
})
, [])
mapTは各項目を計算して、配列に追加するだけとなります。filterTも同様。
さて結果は一緒になるので、つまりこれは.map
, .filter
をreduce関数を使って少し抽象化したと考えていいでしょう。
この処理を詳しく見ていくと、acc(現在の状態)とitem(入力)がoutput(出力)に変換する処理と捉えることができますね。
そのように考えると出力はなんでもいいわけです。このように
const input = [1,2,3,4,5,6,7,8,9,10]
input.reduce(
mapT((item) => item * 3)(
filterT((item) => item % 2 === 0)(
filterT(i => i < 10)((acc, item) => {
return [...acc, item]
})
))
, [])
難しいように思えますが、結局最初にreduceから呼ばれていた関数が最後に呼ばれ、各項目に対してmapTした値をfilterTに渡してfilterTが値を返却しているだけです。
これは何が嬉しいのでしょうか。
処理の最初を見てもらえばわかると思いますが、この処理は各項目に対してreduceをしているだけです。mapやfilterがあるからと言ってその分配列を捜査するということも行なっていません。
最初に示した例
const input = [1,2,3,4,5,6,7,8,9,10]
input.map(i => i * 3).filter(i => i % 2 === 0).filter(i => i < 10)
と計算結果は同じになりますが、reduceによる処理を行なったことで中間の配列を生成することなくより効率的な計算となっています。
魔法みたいですね。
とまぁここまではいいとして、このコード、ネストが深くてちょっと気持ち悪いですよね。
compose関数を使ってみる
そこで汚い仕事を引き受けてくれるcompose関数に登場してもらいましょう。
const compose = (...fns) => input => {
return input.reduce(fns.reduce((acc, fn) => {
return (...args) => fn(acc(...args))
}, x => x)((acc, i) => [...acc, i]), [])
}
compose関数は複数の関数を受け取って前の関数が実行した結果を次の関数に流す関数です。
このcompose関数を使ってもう少し読みやすくしてみましょう。
compose(
mapT(item => item * 3),
filterT(item => item % 2 === 0),
filterT(item => item < 10),
)([1,2,3,4,5,6,7,8,9,10])
さてという感じにみやすくなりました。
まとめ
ただ実用でこんな難しいことは考えずに関数型のライブラリがいい感じにしてくれます。
今回はライブラリがどんな動きをするか、何を解決しようとしてるかを理解できました。