今回は Clojure の関数型言語らしい機能として、「関数の合成」と「トランスデューサー」について説明します。
関数 f(x) と g(x) を合成して新しい関数 h(x) を作ることを考えてみましょう。関数 h(x) を次のように定義します。
h(x) = f( g( x ) )
たとえば、f(x) = 2 * x + 1, g(x) = x * x + 3 * x とすると、h(x) は次のようになります。
h(x) = f( g( x ) ) = 2 * (x * x + 3 * x) + 1 = 2 * x * x + 6 * x + 1
実際のプログラムは数式を展開するのではなく、g(x) の評価結果を f(x) に渡すだけなので簡単です。第 1 引数と第 2 引数に関数 f と g を受け取り、それらを合成する関数 compose は次のようになります。
user=> (defn compose [f g] #(f (g %))) #'user/compose
簡単な例を示しましょう。
user=> (defn f [x] (inc (* 2 x))) #'user/f user=> (defn g [x] (+ (* x x) (* 3 x))) #'user/g user=> (f (g 4)) 57 user=> (def h (compose f g)) #'user/h user=> (h 4) 57
関数 f と g を定義します。f と g の合成は f( g( x ) ) と表すことができます。実際に 4 を計算すると 57 になります。この関数は compose で合成することができます。(compose f g) の返り値を変数 h に束縛すると、h を合成関数として使うことができます。
clojure には compose と同じ働きをする関数 comp が用意されています。
(comp f g) (comp f g & fs)
comp は 2 個以上の関数を合成することができます。簡単な例を示しましょう。
user=> (def h1 (comp f g)) #'user/h1 user=> (h1 4) 57
ところで、Clojure には Threading Macros という便利な機能があり、(f (g x)) という関数呼び出しを次のように表すことができます。
(-> x g f)
user=> (-> 4 g f) 57
-> はマクロ (Threading first Macro) で、次の S 式と同じ働きをします。
(-> item (form1 x1 y1 ...) (form2 x2 y2 ...) (form3 x3 y3 ...)) ≡(form3 (from2 (form1 item x1 y1 ...) x2 y2 ...) x3 y3 ...)
item の評価結果が、次の form1 の第 1 引数に渡され、その評価結果が次の form2 の第 1 引数に渡されます。これを最後まで続けていき、最後の form の評価結果が -> の返り値になります。関数は左から右へ評価されていくので、関数合成 comp の引数とは逆順になることに注意してください。
-> は reduce の動作と似ています。簡単な例を示しましょう。
user=> (-> 0 (+ 1) (+ 2) (+ 3) (+ 4) (+ 5)) 15 user=> (-> 0 inc inc inc inc inc) 5
Clojure には評価結果を form の最後の引数に渡すマクロ ->> (Threading last Macro) や、挿入位置を名前で指定するマクロ as-> もあります。
(as-> expr name (form1 ...) (form2 ...) ...)
as-> の場合、expr の評価結果が name の変数に束縛され、(form1 ...) の中でそれを参照することができます。そして、form1 の評価結果が name の変数に束縛され、次の form2 に渡されます。
簡単な例を示しましょう。
user=> (->> '(5 6 4 7 3 8 2 9 1) (map #(- % 5)) (filter pos?)) (1 2 3 4) user=> (as-> '(5 6 4 7 3 8 2 9 1) xs (map #(- % 5) xs) (filter pos? xs)) (1 2 3 4)
このほかにも、Clojure には便利な Threading Macros が用意されています。興味のある方は Clojure のリファレンスをお読みくださいませ。
トランスデューサー (transducer) には「変換器」という意味があります。Clojure の場合、入力されたデータを変換する特別な関数のことをトランスデューサーといいます。Clojure の場合、コレクションなどを操作する関数において、入力データを表す引数を省略すると、トランスデューサーを返す関数が用意されています。なお、Clojure ではトランスデューサーのことを xf とか xform と記述することがあります。
(map #(* % %)) => 入力データを 2 乗する xform (filter odd?) => 入力データから奇数値を取り出す xform (take 10) => 入力データから要素を 10 個取り出す xform
Clojure の xform は関数 comp で簡単に合成できるのが特徴です。threading macro で高階関数を並べるよりも、xform を comp で合成したほうが、入力データを効率的に処理できる場合があります。
xform を comp で合成する場合、適用順序は左から右となり、通常の関数とは逆になることに注意してください。つまり、threading macro と同じ順番になります。
xform に入力データを適用するには、以下の関数を使います。
1 は畳み込み (reduce) の動作と似ています。
(transduce xform f coll) (transduce xform f init coll)
引数 f は reduce に渡す関数と同じで、これを reducing 関数といいます。transduce は引数 coll を xform と f を合成した関数で畳み込みを行います。簡単な例を示しましょう。
user=> (def integers (range 1 ##Inf)) #'user/integers user=> (take 10 integers) (1 2 3 4 5 6 7 8 9 10) user=> (reduce + 0 (take 10 integers)) 55 user=> (transduce (take 10) + 0 integers) 55 user=> (reduce + 0 (take 10 (map #(* % %) integers))) 385 user=> (transduce (comp (map #(* % %)) (take 10)) + 0 integers) 385 user=> (reduce + 0 (take 10 (map #(* % %) (filter odd? integers)))) 1330 user=> (transduce (comp (filter odd?) (map #(* % %)) (take 10)) + 0 integers) 1330
結果をシーケンスとして受け取りたい場合は 2 の sequence を使います。
(sequence xform coll) (sequence xform coll & colls)
基本的には xform 用のマップ関数みたいな動作になります。簡単な例を示しましょう。
user=> (sequence (take 10) integers) (1 2 3 4 5 6 7 8 9 10) user=> (sequence (comp (map #(* % %)) (take 10)) integers) (1 4 9 16 25 36 49 64 81 100) user=> (sequence (comp (filter odd?) (map #(* % %)) (take 10)) integers) (1 9 25 49 81 121 169 225 289 361) user=> (sequence (map list) (range 1 11) (range 11 21)) ((1 11) (2 12) (3 13) (4 14) (5 15) (6 16) (7 17) (8 18) (9 19) (10 20)) user=> (sequence (comp (map list) (take 5)) (range 1 11) (range 11 21)) ((1 11) (2 12) (3 13) (4 14) (5 15))
3 の into は xform を使用することができます。
(into to xform from)
into はコレクション from の要素に xform を適用し、その結果をコレクション to に格納して返します。簡単な例を示しましょう。
user=> (into '() (take 10) integers) (10 9 8 7 6 5 4 3 2 1) user=> (into '(0 -1 -2) (take 10) integers) (10 9 8 7 6 5 4 3 2 1 0 -1 -2) user=> (into [] (take 10) integers) [1 2 3 4 5 6 7 8 9 10] user=> (into [0 -1 -2] (take 10) integers) [0 -1 -2 1 2 3 4 5 6 7 8 9 10] user=> (into '() (take 10) integers) (10 9 8 7 6 5 4 3 2 1) user=> (into [] (comp (map #(* % %)) (take 10)) integers) [1 4 9 16 25 36 49 64 81 100]
最後の eduction はちょっと変わっています。
(eduction xform ... coll) => clojure.core.Eduction
eduction は Eduction 型のデータを直ぐに返します。Eduction 型は reduce や反復処理で使用することができます。eduction は複数の xform を受け取ることができます。comp で合成する必要はありません。評価する順番は comp と同じく左から右です。
eduction を評価するとき、xform は coll の要素にまだ未適用であることに注意してください。reduce や反復処理などで Eduction の値が必要になったとき、はじめて coll に xform が適用されて値が求まります。また、遅延評価と違って、求めた値はキャッシュされません。値が必要になるたび、その都度計算が行われます。
簡単な例を示しましょう。
user=> (def xf (comp (filter (fn [x] (println "filter" x) (odd? x))) (map (fn [x] (println "map" x) x)))) #'user/xf user=> (def xs (eduction xf (range 10))) #'user/xs user=> xs (filter 0 filter 1 map 1 filter 2 filter 3 map 3 filter 4 filter 5 map 5 filter 6 filter 7 map 7 filter 8 filter 9 map 9 1 3 5 7 9) user=> (reduce + 0 xs) filter 0 filter 1 map 1 filter 2 filter 3 map 3 filter 4 filter 5 map 5 filter 6 filter 7 map 7 filter 8 filter 9 map 9 25 user=> (doseq [x xs] (println x)) filter 0 filter 1 map 1 filter 2 filter 3 map 3 filter 4 filter 5 map 5 filter 6 filter 7 map 7 filter 8 filter 9 map 9 1 3 5 7 9 nil
トランスデューサーの実体は、reducing 関数を受け取り、reducing 関数を返す関数となります。ただし、2 引数の関数 (fn [a x] ...) を返すだけでは駄目で、引数なしと引数が 1 つの関数も定義する必要があります。Clojure のドキュメント トランスデューサー(transducer) (翻訳) より引用します。
(fn [rf] (fn ([] ...) ([result] ...) ([result input] ...)))
1 は畳み込みで初期値が与えられなかった場合です。(rf) の評価結果が初期値になります。2 は通常の畳み込みで呼び出される処理です。3 は入力データがなくなったときの処理です。xform は連鎖していることがあるので、引数 rf の関数に result を渡して処理することになります。
簡単な例として map のトランスデューサーを作ってみましょう。簡単にするため、コレクションは一つだけ受け取ることにすると、プログラムは次のようになります。
リスト : map (トランスデューサー) (defn my-map [f] (fn [rf] (fn ([] (rf)) ([result] (rf result)) ([result input] (rf result (f input))))))
引数無しは (rf) を評価し、引数 1 つの場合は (rf result) を評価します。引数 2 つの場合、入力データ input に関数 f を適用し、それを result を rf に渡して呼び出します。これでマッピングの動作になります。簡単な実行例を示します。
user=> (sequence (my-map #(* % %)) (range 1 11)) (1 4 9 16 25 36 49 64 81 100) user=> (transduce (my-map #(* % %)) + 0 (range 1 11)) 385 user=> (transduce (my-map #(* % %)) + (range 1 11)) 385 user=> (sequence (comp (filter odd?) (my-map #(* % %))) (range 1 11)) (1 9 25 49 81) user=> (transduce (comp (filter odd?) (my-map #(* % %))) + 0 (range 1 11)) 165
正常に動作していますね。
Clojure には畳み込みを途中で終了する機能が用意されています。
関数 reduce で畳み込みの処理を途中で終了するのは簡単です。reducing 関数の中で reduced を呼び出すだけです。簡単な実行例を示します。
user=> (reduce (fn [a x] (if (neg? x) (reduced :neg) (+ a x))) '(1 2 3 4 5)) 15 user=> (reduce (fn [a x] (if (neg? x) (reduced :neg) (+ a x))) '(1 2 -3 4 5)) :neg
要素 x が負ならば、reduced で :neg を返します。負の要素が無ければ合計値を返します。
トランスデューサーの場合、halt-when を使うと途中で処理を終了することができます。
halt-when pred halt-when pred retf
halt-when はトランスデューサー版の reduced と考えてください。halt-when は pred が真を返す要素を見つけると、処理を中断して、その要素をすぐに返します。引数 retf は 2 引数の関数で、最初が今までに計算された値、第 2 引数は pred が偽を返す要素になります。
簡単な実行例を示します。
user=> (transduce (halt-when neg?) + 0 '(1 2 3 4 5)) 15 user=> (transduce (halt-when neg?) + 0 '(1 2 -3 4 5)) -3 user=> (transduce (halt-when neg? #(list %1 %2)) + 0 '(1 2 -3 4 5)) (3 -3) user=> (transduce (halt-when neg? #(list %1 %2)) + 0 '(1 2 3 4 -5)) (10 -5)
ライブラリ core.async のチャネルはトランスデューサーを指定することができます。
(chan buf-or-n xform) (chan buf-or-n xform ex-handler)
xform を使用する場合、チャネルにバッファを設定する必要があります。ex-handlerは 1 つの引数を持つ関数で、xform で例外が送出された場合に呼び出され、ex-handler の返り値に置き換えられます。
簡単な実行例を示します。
user=> (require '[clojure.core.async :as a]) nil user=> (def c (a/chan 4 (map #(* % %)))) #'user/c user=> (a/>!! c 1) true user=> (a/>!! c 2) true user=> (a/>!! c 3) true user=> (a/>!! c 4) true user=> (a/<!! c) 1 user=> (a/<!! c) 4 user=> (a/<!! c) 9 user=> (a/<!! c) 16 user=> (def c1 (a/chan 1 (map (fn [x] (if (neg? x) (throw (Exception. "xform error")) (* x x)))) (fn [e] (.getMessage e)))) #'user/c1 user=> (a/>!! c1 10) true user=> (a/<!! c1) 100 user=> (a/>!! c1 -1) true user=> (a/<!! c1) "xform error"
xform に mapcat を指定すると、シーケンスの要素をチャネルに送信することができます。簡単な実行例を示します。
user=> (def c2 (a/chan 4 (mapcat identity))) #'user/c2 user=> (a/>!! c2 (range 4)) true user=> (a/<!! c2) 0 user=> (a/<!! c2) 1 user=> (a/<!! c2) 2 user=> (a/<!! c2) 3
トランスデューサーの基本的な機能を簡単に説明しました。トランスデューサーにはもっと高度な使い方もあると思いますが、Clojure 初心者 (M.Hiroi も含む) にはちょっと難しいかもしれません。トランスデューサーはとても興味深い機能なので、高度な使い方は今後の課題にしたいと思います。