Java 8 を関数型っぽく使うための(中略)をClojureでやってみた
はじめに
このエントリーは、Java8のlambda式についてのエントリー派生シリーズへの便乗記事です。まだ下の記事を見ていない方はリンク先を見ておいてください、そうしないと意味がわかりません。
- Java 8を関数型っぽく使うためのおまじない - きしだのはてな
- Java 8を関数型っぽく使うためのおまじないをF#でやってみた - ぐるぐる~
- Java 8 を関数型っぽく使うためのおまじないを F# でやってみたを Scheme でやってみた - 月の塵
SchemeがあるならClojureがあってもいいですよね。
なお、本エントリーは 2013年5月時点の最新バージョンである Clojure 1.5.1にて検証しています。
IFn型 (インターフェース)
Clojureでは関数のように振る舞うオブジェクトはIFn*1というインターフェースを実装します。パッケージ名まで含めると clojure.lang.IFnです。
インターフェースを実装する方法はいくつかありますがここではreifyを使います。
こんな感じになります。
(def enclose (reify clojure.lang.IFn (invoke [_ s] (str "[" s "]"))))
invokeの第一引数はthisオブジェクトですがここでは使用しないので _ というシンボルで定義しています。
これを呼び出そうとすると、こんな感じになります。
(println (.invoke enclose "foo"))
こうすると、次のような表示になります。
[foo]
もう一つ関数を定義してみましょう。最初の文字を大文字にする関数です。
(def capitalize (reify clojure.lang.IFn (invoke [_ s] (str (-> s (.substring 0 1) .toUpperCase) (-> s (.substring 1) .toLowerCase)))))
2文字未満の文字列を与えると死にます。
呼び出してみます。
(println (.invoke capitalize "foo"))
こんな感じの表示になります。
Foo
この2つを順に呼び出して、capitalizeしてencloseしようとすると、こんな感じになりますね。
(println (.invoke enclose (.invoke capitalize "foo")))
表示はこうなります。
[Foo]
こういう場合Java8ではandThenがつかえるそうですが、Clojureにはそういったものはありません。
……ああ、そうそう、うっかり失念していましたがIFnを実装したオブジェクトは関数そのものでした。
(capitalize "foo") ;=> Foo
したがって comp を使って連結することができます。
(println ((comp enclose capitalize) "foo"))
関数合成です。
reifyで関数が実装できることがわかりました。
別名を使う
さて、reifyによる関数生成は微妙に長いので、Fとかみじかく書きたいですね。
そんなときにはマクロで定義してしまいます。
(defmacro F [args & body] `(reify clojure.lang.IFn (~'invoke [~'_ ~@args] ~@body)))
(def enclose (F [s] (str "[" s "]"))) (def capitalize (F [s] (str (-> s (.substring 0 1) .toUpperCase) (-> s (.substring 1) .toLowerCase))))
すっきりしました。呼び出しもinvokeを書く必要はありません。
関数合成
compで関数合成ができることはすでに述べました。しかしClojureのcompは F# の >> とは適用順が逆になります。
Clojureにも逆順で適用するcompがあってもよい気はしますので定義しておきます。
(defmacro comp> [& fs] `(comp ~@(reverse fs)))
さらにもうひとつ関数を用意して次のように書いてみます。まんなかあたりを取り出す関数です。
(def middle (F [s] (.substring s (-> s .length (quot 3)) (-> s .length (* 2) (quot 3)))))
(println ((comp> middle capitalize enclose) "yoofoobar"))
関数合成を使わないと、次のようになりますね。
(println (enclose (capitalize (middle "yoofoobar"))))
このように、実際に呼び出す順と記述が逆になります。middleしてcapitalizeしてencloseするのに、先にencloseから書く必要があります。
もっともそれはcompを使った場合も同じなので、関数合成というよりもcomp>のお手柄なんですけどね。
あと、関数合成ではありませんが、Clojureにはスレッディングマクロがあるので、次のように書くこともできます。
(println (-> "yoofoobar" middle capitalize enclose))
関数合成ではないけど便利ですね! スレッディングマクロ。
ここまで1引数のinvokeだけ定義してきました、2引数以上のバリエーションも必要なんじゃないかと思うかもしれません。
実際IFnは引数0〜20までの固定数引数のinvokeとさらに可変長引数のinvokeまで用意しているので多くの引数をもったinvokeを実装できます。
しかし2引数以上の関数は甘えらしいので1引数だけにしておきます。
戻値の無い関数
Clojureには戻値の無い関数はありません。戻値がvoidのJavaメソッドを評価しても、その評価値をバインドすることが出来ます。値はnilです。
Clojureにおける関数は、IFnのinvokeを実装しており、このinvokeの返却値はすべてObject型と定義されているので戻値が無いことはありえないのです。
戻値がないことを表す unit のようなシンボル、キーワード、あるいは型を定義することは可能ですが、あまりメリットも無いので深く考えないことにします。
カリー化
さて、2引数以上の関数は甘えらしいと書きましたが、実際2つ以上のパラメータを渡したいときはどうすればいいんでしょう?
こういうときに使うのがカリー化です。カリー化は、ひとつの引数をとって関数を返すことで、複数のパラメータに対応します。
例えばはさむ文字とはさまれる文字を指定して、文字を文字ではさむ関数、通常の2引数関数であらわすなら次のようなsandwichがあるとします。
(defn sandwich [enc s] (str enc s enc))
これを1引数関数でカリー化して書くと次のようになります。
(def sandwich (F [enc] (F [s] (str enc s enc))))
sandwich自体は、文字列をとって、「文字列をとって文字列を返す関数」を返す関数になっています。
呼び出しは次のようになります。
(println ((sandwich "***") "sanded!"))
表示は次のようになります。
***sanded!***
3引数だとこんな感じですね。
(def encloseC (F [open] (F [close] (F [s] (str open s close)))))
encloseCは、文字列をとって、「文字列をとって、「文字列をとって文字列を返す関数」を返す関数」を返す関数になっています。
呼び出しはこんな感じです。
(println (((encloseC "{") "}") "enclosed!"))
開きカッコの連続はなんだか落ち着きませんね、
糖衣構文に甘えましょう。
(defmacro aps ([f a] `(~f ~a)) ([f a & more] `(aps (~f ~a) ~@more)))
定義の方もネストが煩わしいのでマクロFをいじります。
(defmacro F [[a & more] & body] (if more `(reify clojure.lang.IFn (~'invoke [~'_ ~a] (F [~@more] ~@body))) `(reify clojure.lang.IFn (~'invoke [~'_ ~a] ~@body))))
このマクロを使うと関数生成と呼び出しは次のようになります。
(def encloseC (F [open close s] (str open s close))) (println (aps encloseC "{" "}" "enclosed!"))
ふつうの多引数関数のようになりました。
ところで、このカリー化形式の encloseC には引数を部分的に渡しておくことが出来ます。
(let [enclose-curly (aps encloseC "{" "}")] (println (aps enclose-curly "囲まれた!")))
こうやって部分適用することで、新しい関数が作れるわけです。
……え? なんでそこだけletバインディングなのかって?
(def enclose-curly (aps encloseC "{" "}")) ;;=> clojure.lang.Compiler$CompilerException: java.lang.AbstractMethodError, compiling:...
なんかVar*2にしようとするとコンパイルエラーが……
エラーメッセージを見てみると AbstractMethodError がコンパイル時に発生しているようです。ということはメソッド実装が足りていないということですね。
IFnのinvokeは沢山のオーバーロードメソッドが定義されていますが、今回書いたコードでは、その沢山のオーバーロードメソッドのうちの1つしか実装していません。怪しいですね。
ではすべてのメソッドを実装すればいいのか?というと別の問題があります。
IFnのinvokeは0~20個の引数固定数のメソッドと、それに加えて、可変長引数のメソッドも定義されています。
そして、なんと reify や protocols では可変長引数のメソッドを実装できない*3という重大な問題があるのです。
どうしたらいいんだろうか……
AFnクラスとproxy
clojure jvmには IFnを実装したabstract classである、AFn*4というクラスがあります。パッケージ名まで含めると clojure.lang.AFnです。
ありがたいことにこのAFnはIFnのメソッドを(可変長引数のものも含めて)すべて実装してくれているんですね。
IFnとAFnは、ちょうどjava.awt.eventの XXXXListener と XXXXAdapterの関係にあります。AFnを使えば可変長引数を無視して実装ができるわけです。
ただし残念なことにreifyはprotocolまたはinterface しか継承(実装)できません。
抽象クラスの派生クラスのインスタンスを作るには proxyを使います。
(defmacro F [[a & more] & body] (if more `(proxy [clojure.lang.AFn] [] (~'invoke [~a] (F [~@more] ~@body))) `(proxy [clojure.lang.AFn] [] (~'invoke [~a] ~@body))))
(def encloseC (F [open close s] (str open s close))) (def enclose-curly (aps encloseC "{" "}")) (println (aps enclose-curly "囲まれた!"))
これでようやく、カリー化した関数のVarを作ることができました。
おわりに
Clojureでは関数(クロージャー)は空気のようなものなので同じことをやっても元ネタのような驚きはありません。そこで敢えて fn や #(...) を使わずに関数生成フォームから実装をしてみました。
「reifyとmacroによる構文拡張を使えばClojureを改造/拡張/再実装することも結構簡単にできるんですよ」と結論づけるつもりでしたが、
- interfaceメソッド実装不足によるエラー(なぜかletではエラーにならない)
という現象に遭遇し、最後にどんでん返しされてしまいました。
proxyはどことなくレガシーな印象を(勝手に)持っているのですが、状況によっては頼れるやつなんですね。失礼いたしました。
補足
このエントリーではClojure組み込みの関数生成フォームを使わないという制約を設けたため上記のような問題が浮上しましたが、fnを使えば、
(defmacro F [[a & more] & body] (if more `(fn [~a] (F [~@more] ~@body)) `(fn [~a] ~@body)))
のようによりシンプルに実装することが可能です。もちろんカリー化した関数をVarにすることもできます。
*1:[ソース]clojure.lang.IFn
*2:CLのスペシャル変数みたいなもの
*3:[参考] protocolで可変個引数は取れない - fatrow
*4:[ソース]clojure.lang.AFn