紙箱

覚えたことをため込んでいく

Clojureの世界観

ブログを書くのは久々です。
京都で小さな会社をやっていて、自社開発でClojureとClojureScriptを使用し続けて、概ね3年くらい使い続けています。その過程で、Clojure自体にも小さいながらソースレベルの貢献ができたりして、オープンソースプロジェクトとしても面白かったのですが、もともとオブジェクト指向言語ばかりやってきたところから、Clojureという、まったくオブジェクト指向言語ではない言語に飛び込んだ経験や考えたことなんかを、ブログにストックすると、何か他の人にも役立つこともあるかと思って、ブログに書くことにしました。

このところずっと、自社の仕事とは別に、恵比寿にある 株式会社ユーザベース さんのお仕事に参加しています(私が法人を作る前からなので、もう5、6年くらいになります)。そちらの方でもClojureやシステム設計の話(プレゼンなど)などを何度かさせてもらったり、ここ半年くらいは、ユーザベースさんでもClojureを実開発へ投入し始めたため、開発支援として携わりました(Clojureのシステムは現在も鋭意開発中です。ぶっちゃけClojure開発者募集中です)。
ユーザベースさんのシステムにおいても、Clojureでgo-blockを実現する並行処理ライブラリ core.async を使って並行処理システムを作ったりしているので、そこで得られた、Clojure+core.asyncでの並行プログラミングの知見なんかも書いていきたく、これからしばらく、不定期でClojureについてブログを書いていくつもりです。気力が続けば。

今回は、Clojureを使うことによって、Clojureが前提としている世界観が、自分が空気のように前提としていたものと異なってるところとして、Clojureにおける抽象データ構造と、ポリモフィズムの仕組みについて書いてみます。

少ない抽象データ構造と、たくさんの関数

Clojureでは、オブジェクト指向言語と異なり、関数とデータはおおよそ完全に分離しています。オブジェクト指向言語でないのだからそうなのだろうってことは皆、知ってはいるのだろうけども、関数(メソッド)をデータと結びつけて考えてしまうというのは、オブジェクト指向言語に慣れた人のみたいなものとして、染みついているところのようにも思います。

Java的な、クラスベースのオブジェクト指向言語の場合、メソッドを作るにはクラスを定義してそこにメソッドを足すわけなので、メソッド(関数)はあるデータの集まり(オブジェクト)を操作する専用の関数として定義しがちです。そりゃ、「そのオブジェクトを操作するためのメソッド」なのだから当たり前です。

一方、データと関数が分離している場合は、関数を特定のクラスやオブジェクトに結びつける意味はないですし、逆に、ある関数のために専用のクラスを作る意味もない。極端に考えれば、すべての関数が共通のデータ構造を処理するようにした方が、多様な関数を柔軟に活用できるようになる。

というよりも、オブジェクトに関数が結びついているからこそ「このメソッドはこのオブジェクト構造を処理するためのものだ(他の用途には使えない)」という風に専門化できていたんであって、データと関数を個別のものと扱う以上は、「この関数はこのオブジェクトだけを扱う」という前提を置けないのです(当たり前です)。あるオブジェクトと別のオブジェクトが、型であったりクラスであったりが異なったとしても、関数は、そのオブジェクトが、関数の処理できる構造であれば、処理できるべきなんです。関数とオブジェクトが独立しているというのはそういう意味であるべきです。であれば、すべての関数にまたがるような、共通の汎用データ構造があって、すべてのデータはその汎用性を担保してたほうがいい。

もちろん、「縦」と「横」という情報を持ったデータを使って「面積」を求める関数は、データが「縦」と「横」という情報を持っていることを求めるだろうけども、RectangleだとかTableだとかの特定のクラスにダイレクトに結びつくべきではない。

この課題にはいろんなアプローチがあるんでしょうが(Structural Typeとか?)、Clojureの場合は、言語全体が前提とする抽象データ構造があります。リスト状のものはSeqと呼ばれる抽象データ(代表例は配列)として、マップ状のものはAssociativeと呼ばれる抽象データ(代表例はマップ)として扱います。事実上ほとんどのデータがこの2つで表現されていて、多くの関数が、引数としてSeqまたはAssociativeを受け取る(あるいは内部で自動で変換する)し、SeqまたはAssociativeを出力します。
自分が関数を書くときにも、独自のデータ型ではなく、SeqかAssociativeを前提として書くのがオススメです。そうすることで、関数は他の関数のインプットになりえますし、他の関数の出力を入力できます(各関数の入力と出力が同じ抽象データ構造だから)。

f:id:t_yano:20170409211658p:plain

この抽象化は徹底していて、例えば、Clojureにはdefrecordがあり、これを使えば、 レコードと呼ばれる、独自のとフィールドを持ったデータ構造を作り出すことができます。さらに、レコードに対してプロトコルを実装することで、Javaにおけるインターフェースを実装するような感じのコードを書くこともできます。

(defrecord User [name age mail-address]
  IAuthenticate
  (auth [this param] (something-great param)))

これだけ見ると、まるでクラスを定義できるかのように感じます。ところがこのレコードはすべてAssociativeでもあります。つまり、すべてのレコードはAssociativeを処理する関数に渡すことが可能です。また、このデータを使う側は、データが実はレコードであるということを気にする必要も(原則としては)ありません。それはAssociativeだということさえわかればいい。実際、あるライブラリのある関数の戻り値が、あるバージョンまでマップ(典型的なAssociativeデータ)であったものが、次のバージョンからレコードに変わっていたとしても、特に影響はないはずです。

;; マップ
(def data1 {:name "t_yano"})

;; レコード
(defrecord User [name])
(def data2 (->User "t_yano"))

;; どちらもAssociativeなので、:name関数で:nameの値を取り出せる
(:name data1) ;=> "t_yano"
(:name data2) ;=> "t_yano"

このような構造はJavaのインタフェースのようなものがあれば、他でも実現できると思うだろうけども(実際、Associativeと Seqは、Javaのインタフェースとして定義されています)、それは「注意深く設計すればそのライブラリ内ではそうできる」という話であって、言語として標準の抽象データを定義して、すべてをそこに集約させよう、という世界では、他の言語機能が、すべて、このような仕組みを支援するように作られています。言語としての前提であるからです。たとえば、JavaですべてのデータをMapで作っても、苦しいだけで利点などないでしょう。そういう前提で言語が作られていないからです。

このような抽象データ構造があるからこそ、関数とデータを分離できるわけです。関数は共通の抽象データ構造しか見ていないからこそ、実際のクラスとは結びつく必要がないのです。

「10種類のデータ構造にそれぞれを扱う10個の関数があるよりも、ひとつのデータ構造を扱う100個の関数がある方が良い (It is better to have 100 functions operate on one data structure than to have 10 functions operate on 10 data structures)」という言葉あります。

データ構造にそれぞれ関数がある

一つのデータ構造を扱う100個の関数

この一文に賛成の人も反対の人もいるでしょうが、Clojureは明確に「ひとつのデータ構造を扱う100個の関数がある」ような世界の方がいい、という前提で作られているのです。

アドホックな多態(ポリモフィズム

さて、少ない抽象データ構造とたくさんの関数、という理屈はわかったとして、オブジェクトに結びついてない関数における、多態(ポリモフィズム)はどうなるんでしょうか。用途ごとに異なる関数を用意すればいいってことなんでしょうか。
もちろん、実際の開発には多態は必要です。多態なしで、MySQLPostgreSQLで異なる動きをするconnect関数をどうやって定義したらいいんでしょう。 mysql-connect と psql-connect を使い分ける、というようなことが避けたいところです。

ですから、Clojureにも多態関数を定義する仕組みがあります。defrecordによって独自の型を定義し、引数の型に関数実装を紐づけることもできます。あるいはdefmulti / defmethod を使ってマルチメソッドを定義することで、データの(実際には式の結果ですが)によって関数実装を切り替えることもできます。例えば、引数として渡されたDB接続定義のドライバ名に基づいて、connect関数の実装を切り替える、といった使い方もできます。

関数(メソッド)を多態にする仕組みについてはJavaScriptみたいなアプローチや、引数の型の組み合わせによって切り替えるマルチプルディスパッチがあったり、世の中にはすでに色々なものがあるので、Clojureの多態の仕組みがそれほど独自のものだというわけではないでしょう。強いていえば、Clojureのマルチメソッドで追加した関数は、動的に紐付けを追加したり切り離したりできるのが面白いところですが、それ自体も、他の言語(特に動的な言語)でできないというものでもありません。

多態を実現する仕組みよりも、Clojureの、多態に対する態度というか、考え方の方が、面白いのではないかと思います。
Clojureでは、Clojureの多態の仕組みのことをAd-hoc Polymophismと呼んでます。このワード自体は(Clojureよりも前に)結構昔からあるようなので、新しい概念ではないのでしょうが、それによる実際の開発への影響が結構面白い。

多態(ポリモフィズム)というのは、オブジェクト指向の世界では、「あるオブジェクトと別のオブジェクトが同じメソッドを持っているけども違う動作をする」とか、逆の視点から、「あるオブジェクトと別のオブジェクトは、実際には違う動作をするだろうが、同じメッセージに反応するので同じオブジェクトとみなせる」といった文脈で使われる感じがします。オブジェクト指向のメッセージ・パッシングの考え方にのっとれば、あるオブジェクトAとBに同じメッセージを送っても、実際に動く処理(メソッド)は別かもしれない、だが同じメッセージに応答できるのだから、両者は同じオブジェクトとみなせる、というわけです。
つまり、いつもオブジェクトとともに、オブジェクト同士の関係(同じメッセージに応答するのだから実質同じとして扱える、とか、サブタイプである、とか)として語られる傾向があります。

一方、Clojureにとって多態とは、関数ディスパッチの話と捉えられています。関数はデータ(オブジェクト)とダイレクトには結びついてないので、多態の説明として、オブジェクトやクラスと結びつけて語ることはできません。ある関数を呼んだ時に、実際に実行される関数実装はどれなのか?というディスパッチの問題に過ぎないのです。

ディスパッチの問題、ということは、実際のところ、関数を使う側にとっては、実は関数が多態であるかどうかということは、使用上あまり関係がないということです。関数は関数であって、それが多態であるかはユーザーには関係がない。
例えば、ただの関数は、その関数を呼ぶと関数本体に直接ディスパッチされます。これも関数ディスパッチのひとつの形です。
これがマルチメソッドであれば、引数の値(実際には値を計算した結果)によって実際に使う関数実装が探された後に呼び出されます。defrecord / defprotocol によるディスパッチでは、引数の型によってディスパッチされます。それは実装の詳細であって、関数を使う側にとっては、関数を呼べば関数が実行されればいいのです。

もしある別の人の作った関数実装が、内部でプログラムとしてif文を使って別の関数に処理をディスパッチしていたとしても、使う側にとっては関係ないのと同じです。if文での分岐は、手動の関数ディスパッチと考えれば、マルチメソッドと同じく関数ディスパッチだと言えますし。

使う側にとってはいずれにせよただの関数に見えるのですから、Clojureにおいて、関数はあとでいつでも多態に切り替えられる存在なわけです。

過去に私がDB操作ライブラリの一つ Korma に送ったパッチでは、列名をクオートする処理をMySQLPostgreSQLとで分ける必要に対応しました。対応は簡単で、クオートする関数をマルチメソッドに変更し、引数として渡ってくるDB接続定義の接続URLを使って適切なクオート処理にディスパッチするだけです。関数がマルチメソッドに変わったわけですが、関数を呼び出す側から見ると、やはりただの関数に見えますので、他の部分にはまったく影響しません。

つまり、コードを書いている時に、ある関数を多態にするかどうかというのは後から考えても大丈夫な仕組みなわけです。もちろん、作ってる段階で、設計として「ここの関数は外から拡張できるようにしたいから、マルチメソッドにしておこう」とか「ここはユーザー利便性を考えてプロトコルを定義しておくか」ということはありますが、そうでないところを後からマルチメソッドやプロトコル関数化することも、結構簡単に行えるのです。

だからAd-hoc(場当たり的な)ポリモフィズムと呼ぶわけです。もちろん注意すべきことはあって、対象の処理が関数に分離されてなければ、後で多態関数化することもできませんから、できれば関数は細かく分けておいた方が、後から対処しやすいと思います。そのような注意点さえクリアしておけば、柔軟に後からコード変更可能だ、というのも、Clojureの利点の一つです。

他の文化を無理やり持ち込まないの大事

プログラミング言語には、言語ごとに文化というか、大事にしている考え方があるものです。ClojureにはClojureの大事にしているものがあって、言語仕様自体が、その大事にしているものを前提に設計されているはずです。上に紹介したものも、Clojureという言語に意図的に組み込まれた仕様な訳です。

他の言語から新しい言語に移ってくると、最初は、今まで馴染んでいた言語の文化と、新しい言語のやり方とが、まったく異なっていることに混乱して、自分の馴染んでいる文化と同じにしようとしてしまいがちです。しかし、言語仕様とその言語の文化は強くつながってるものなので、文化を無視して別の文化で書こうとしても苦しいだけです。

そのためには、やはり、その言語が大事にしているものはなんなのか、どういう意図で、どういうことを実現したくてそういう言語デザインになっているのか、を知るのが大事だと思うのです。意図がわかれば、馴染めなかったものにも急に「なるほど、そういうことか」と納得感が得られるものです。抽象データ構造や多態の仕組みなんかも、そのような例の一つです。

ClojureJavaみたいな型ベースのオブジェクト指向言語とは、かなり違う言語なのですが、一見「これってJavaのインターフェースと同じか」とか「これってLombokで@Valueでクラス定義するようなもの?」とか思えてしまう機能もあったりします。しかしもちろん、それらはイコールではないし、意図してることも異なってることもあります。
他の言語から移ってきた時には、「なんでそういう仕組みになっているのか?その背景はなんなのか?」を把握するのがとても大事だと思います。意図さえわかってしまえば、とても簡潔で書きやすい言語ですから。

抽象データ構造やAd-hoc多態以外にも、Clojureには、シンプルで構造化しやすい言語を作るために、いろんなアイデアが取り込まれていて面白いので、今後も、理解できた範囲で書いていきたいところです。