Clojureの公式ドキュメントを試しに読んでみている。Transducers、響きが甘くて心地いい。Reduceなどduce
が接尾辞につくものは、なにか流れるようなものを感じて心地が良いのだ。ちょっと読んでみる。できたら記事にしよう。
Transducersの基本概念
Transducersとは、組み合わせられる変換を実現するための設計だそうだ。組み合わせられる変換というならば、素朴に関数を利用するので良いのではないか?
なぜTransducersと特別な名前をつけて、設計パターンを抽出したのだろうか。関数型プログラミングのパラダイムでは、プログラムを大きな関数と捉えて、小さな関数の組み合わせで構築するというのが当たり前ではなかったか。
Transducersの語源と意味
変換を一定の特別な語として定義しているわけだ。Transducersという語に特別な意味があるのだろうか?少し語義を調べてみることが一つの手掛かりになりそうだ。
transduce
- 〔エネルギーなどを他の形態に〕変換する
- 《遺伝》〔~を〕形質導入する
語義を調べてみると、どうやら1番の語義がよりclojureのTransducerに近いと思う。
ちなみに形質導入を調べてみた。ある生物から別の生物へ、特定の遺伝子を移すことを指すらしい。今回の用法には関係なさそうだ。ただ、遺伝子を移すという行為は高い濃度の人工性を感じるので、転じて変換それ自体を高度に抽象化することに繋がっている。
Clojureの用語の元としては、もっぱら工学だったり情報技術の文脈においての変換という意味。まあとにかく、Transducersは変換するものである。
Transducersの汎用性
先ほどの語義から、多分Transducersは変換を明確に抽象化している。ドキュメントに戻ろう。Transducersは、入力と、そして出力の種類から独立していて、本質的な変換のみを定義する。具体的には一体どういうことだろう?
Transducersは入力や出力の種類に関係なくどこでも使えるものでなくてはいけない。つまり、Collections・Streams・Channels・Observablesで使えなくてはいけないということだそうだ。
これら4つの種類についてあまり詳しく知らないな。Collectionsだけはわかりやすい。けれど、この4つの共通点を考えてみると、一連のデータ
と見ることができそうだと思う。
Transducersは一連のデータそれ自体への変換を定義でき、自由に組み合わせられるのだという予測を立てておいて、次の章へ進もう。
次の章は具体的なパーツについて説明される。定義されている用語をみたい。
Transducersを構成するパーツ
transducerの仕組みを説明するために2つの用語が定義されている。reducing functionとtransducerだ。
reducing functionに関しては普段JavaScriptを書いている人なら見覚えがあるはず。集積された値と項目を受け取って、再計算された値を返す関数だ。例えばあるリストから合計値を算出するのに使ったことがある人が多いのではないだろうか。(acc, item) => acc
そして肝心のtransducerというのは、reducing functionを受け取って新たなreducing functionを返す(!)関数だ。そうか、transducerはデータに直接作用するのではなくて、データの変換それ自体を加工するためのものみたいだ。流れが変わって急激に面白くなってきたな。
多分、例えばデコレーターがそうするようにreducing functionを加工して、その前後に処理やらフィルタリングを追加するみたいなものなんじゃないだろうか。
具体例
ドキュメントにはコードが提示されている。
(filter odd?)
(map inc)
(take 5)
うーん、これはつまりどういうことだろうか?reducing functionを受け取って、~~その終わりに変換を足す、みたいなことかな。~~いや、どうやら仕組みを見ていくと、末尾ではなく先頭に付け足すみたいだ。
例えていうなら漏斗みたいな感じ。ちょうどフィルターしているイメージだ。すなわちreducing functionに渡すinputを加工している。
ただ、やはりまだあまり利点がわからないな。なぜ「データ変換自体を加工」という形にしたのだろうか。単純に関数を連続で組み合わせる形式では、何か達成できないことがあるのだろうか?
ここでさらに、Transducersの設計の利点と直接的な関係をいえるわけではないのだが、この設計の利点は、CPSすなわち継続渡し形式の利点と深い本質で繋がっているのではないかという印象がある。
2つに共通するのは、高階関数スタイルであるということ。
そこからわかるのは、関数よりさらに高い抽象度で何かを行えるということ。これを念頭に置いておけば、より深くTransducersが理解できるのではないか?
CPSでは、継続として受け取った関数を実行するかをコントロールすることができる。一方Transducersでは入力を一気に変更することができる。それを、reducing functionの抽象化能力と組み合わせることで、広汎な変換の抽象化に成功しているのだ。
では、Transducersを深く理解するには、reducing functionが変換をどのように抽象化しているのかを理解すればよい。最後にその部分を具体例とともに見ていこう。関数型にあまり馴染みがないと、いまいちピンとこないかもしれないから。
reduceは、沢山のものが一列に並んでいて(配列)、雪玉をその上に転がして変化させるというイメージだ。個人的にいつもそうやって想像してる。
例えばさっきもあげたけれど、最も基本的な使い方として「配列の合計を計算する」場面がある。数値の配列があったとき、reduceを使って全ての数値を足し合わせることができる。これは、雪玉が数字をを通過する度に大きくなるみたいなもの。
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((total, current) => total + current, 0);
console.log(sum); // 15
次に、「文字列の配列を一つの文字列に結合する」例を見よう。joinを使う以外にも、reduceを用いることで配列内の各文字列を順に結合していくことができる。この場合、雪玉は文字列のかけらを拾い上げ、最終的に一つの大きな文字列に成長する。
const words = ["Clojure", "が", "好きなんだ", "よなあ"];
const sentence = words.reduce((acc, word) => acc + word, "");
console.log(sentence); // "Clojureが好きなんだよなあ"
最後に、より実用的な例として「オブジェクトの配列から特定のプロパティの合計を求める」ことを考えてみる。例えば、商品のオブジェクトがあり、それぞれに価格が設定されているとする。reduceを使用して、これらの商品の総価格を計算できる。
const products = [
{ name: "リンゴ", price: 200 },
{ name: "バナナ", price: 100 },
{ name: "オレンジ", price: 150 },
];
const totalPrice = products.reduce((total, product) => total + product.price, 0);
console.log(totalPrice); // 450
このようにreduce関数を使うことで、様々な形のデータを効率的に処理し、必要な結果を導き出すことができる。
reducing functionは、reduce関数に渡されることで、雪玉を使った配列の変換ができるのだ。この変換の入力を抽象的に操作できるような設計パターンが、Transducersということだ。