44
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

1.3, 1.4, 1.5: Clojureの過去、現在、(とても近い)未来

Last updated at Posted at 2012-12-11

はじめに

プログラミングClojure』が発売されてから、もうすぐ3年が経ちます。原著である"Programming Clojure"の第一版はClojure 1.0の時代に書かれたもので、日本語版も当時開発中だった1.1における変更点を訳注で補足していますが、基本的に1.0ベースです。

Clojureのコードが登場する日本語の書籍は他にも『7つの言語 7つの世界』や『実践プログラミングDSL ドメイン特化言語の設計と実装のノウハウ』などがあります。しかし、タイトルから分かるようにClojureはこれらの本では数あるトピックの一つであり、読めばClojureの要素に一通り慣れ親しむことができるというわけではありません。Clojureも「比較的古びていない書籍は皆英語で書かれている」というよくあるパターンにハマっています。

Pythonが「Pythonらしさ」を保ちながらも、より「Pythonらしく」あるために変化し続けているのと同じように、Clojureも『プログラミングClojure』出版当時既にあった「Clojureらしさ」を捨てることなく、しかし今でも変化し続けています。そのため、『プログラミングClojure』を読んでClojureを始めると、大小様々な部分で戸惑いを感じるかもしれません。特に、バージョン1.3の変更は『プログラミングClojure』のサンプルコードに影響するものがいくつかあります。

そこで、私はClojureのバージョン1.3から近々リリースされる予定の1.5までの主な変化と、リリースされた頃のイベント等について紹介したいと思います。Clojureについて少々の知識を持っていることを前提としているので、Clojureそのものについてもっとよく知りたいという方は12/1の記事を参照してください……と書こうと思っていたけど、これもClojureの知識を持っている前提の文書でした。おすすめのチュートリアルをご存じの方はぜひお知らせください。

特に言及しない限りJVM版の実装を対象に紹介します。ClojureCLR派やClojureScript派、Clojure-Py派、clojure-scheme派その他の皆さん、ごめんなさい。来年何かしらのこういったイベントがあれば、ぜひあなたの好きなClojureで投稿してください。 :-)

また、以下の文章ではjava.langパッケージ、clojure.langパッケージ、clojure.core名前空間、clojure.core.reducers名前空間を断りなく省略している箇所があります。

この文書に誤りなどございましたら、コメントやTwitterなどで指摘していただけると幸いです。

過去:1.3

タイトルの「過去」にあたるバージョン1.3は2011年9月にリリースされました。つい最近だった気がしますが、もう1年も経っているのですね。秘密のプロジェクトだったClojureScriptが公表されたのが同年7月、そして11月にはClojureコミュニティにとっての重要イベントであるClojure/conj 2011が開催されました。また、1.3のリリースとほぼ同時期に開催されたプログラミングのカンファレンスであるStrange Loop 2011では、TwitterのNathan Marzからリアルタイム分散処理システムのStormをオープンソース化すると発表され、Twitterの#clojureハッシュタグが盛り上がったことが印象に残っています。

1.3ではクラスやそのインスタンスを渡すとクラスの情報をClojureのマップで返すclojure.reflect/reflectや関数を繋いで新しい関数を作るevery-predsome-fnなどの新しい関数の追加、メタデータを記述する際によく使われる^{:foo true}の略記法として^:fooの導入、エラーメッセージの改善なども盛り込まれましたが、中でも次の変更が大きな影響を及ぼしました。

Enhanced Primitive Support

特に反響が大きかった1.3の変更は「強化されたプリミティブのサポート (enhanced primitive support)」です。1.2までのClojureは関数の引数や返り値でプリミティブ型の数値を扱う際に必ずboxingしていました。これは数値演算の性能を低下させる大きな要因でした。1.3ではプリミティブ型のサポートを強化し、longdoubleはboxingせずに引数や返り値として扱われるようになりました。また、サフィックスのない数値リテラルはlong(整数リテラルの場合)やdouble(小数リテラルの場合)として読み込まれます。Javaコードとやり取りする部分でlongdouble以外のプリミティブ型が要求される場合は、オーバーフローチェック後に自動変換されます。

また、以前は多倍長整数としてBigIntegerを直接利用していましたが、十分小さな値の演算であればプリミティブ型で演算されるclojure.lang.BigIntを導入しました。clojure.lang.BigIntには十分小さな値であればIntegerLonghashCode()の値が等しくなるという特徴もあります。ただし、BigIntegerも今まで通りfirst-classな数値型としての扱いを受けています。(.add a b)と書かなければいけないということはありません。

更に、数値演算に利用される+incのような関数にも変更が加わりましたが、「+incなどはプリミティブな整数型から多倍長整数型へと暗黙の自動拡張をしないようにする。自動拡張を行うバージョンとして+'inc'などを別に用意する」という変更は「これは自動拡張を前提とした既存のコードベースを破壊する。後方互換性のために自動拡張のないバージョンを'付きの名前にするべきだ」「いや、自動拡張が必要になるような巨大な数を扱うコードは実際には少ない。より一般的なケースに一般的な名前を割り当てるべきだ」と論争の火種になりましたが、最終的に上記の変更は採用されました。

なお、自動拡張が行われない+などの関数はオーバーフローする場合にデフォルトでArithmeticException例外を送出します。コンパイル時の*unchecked-math*trueでない限り、黙って間違った値を返すことはありません。

Earmuffed Vars are No Longer Automatically Considered Dynamic

1.2までのClojureは全てのvarが動的に再束縛できる、つまりダイナミック変数として扱えるようになっていました。おかげでコアの関数をbindingで置き換えて内側のコードの特定の関数呼び出しにフックするというハックが可能でしたが、意図しない関数を呼び出すように外部から割り込まれたり、定数として用意したvarが書き換えられたりする危険性がありました。Clojureはレキシカルスコープの変数を基本とするプログラミング言語ですが、いつどんな関数が別物にすりかわった状態で自分のコードが呼び出されてもおかしくないダイナミックスコープの世界でもあったのです。

しかし、1.3では:dynamicメタデータの値がtrueのvarのみが動的な再束縛を許されるようになりました。*foo*をダイナミック変数として使うためには(def ^{:dynamic true} *foo* 42)、または(def ^:dynamic *foo* 42)(1.3以降でのみ動くコードの場合)のように書く必要があります。これも後方互換性を壊す変更のため、動的な再束縛を利用するライブラリの作者が修正に追われました。「ダイナミック変数は*で名前の前後を囲む」という慣習に従っているコードに対してはコンパイラが「ダイナミック変数らしきvarの:dynamicメタデータがtrueでないので、メタデータをつけるか、*を外してダイナミック変数でないことを明確にせよ」という内容の警告を表示するようになったため、修正箇所の把握そのものは難しくなかったライブラリも多かったようです。

Modular Contrib

言語自体の変更ではありませんが、1.3のリリースと同時期にcontribへの変更も行われました。contribとはClojureのコアには含めていないものの、Clojureの公式ライブラリに準ずるものとして保守、開発されるライブラリの集合です。

1.2までのcontribはclojure-contrib.jarという単一のJARアーカイブに収められ、clojure.contrib名前空間の中に全てのライブラリが収められていました。また、contribは対応するClojureのバージョンと同じバージョン番号を持ち、Clojureの新バージョンリリース後にリリースされていました。しかし、この頃のcontribはたとえ一つのライブラリを利用するだけであっても大きなclojure-contrib.jarを同梱する必要があり、これをスリムアップするためには各ライブラリの依存関係を自力で調べて不要なライブラリを削除する必要がありました。また、リリースサイクルを言語と合わせるためにバグ修正の提供も遅れがちになり、多くのライブラリの中にはもはや活発に保守されていないものや、実験的なコードの断片のみで実用的でないものも混ざっていました。

1.3を前にしてClojureの開発者達が採った方針は、巨大なcontribをライブラリごとに分割して言語のリリースと独立させると同時に、各ライブラリのメンテナを明確にする「モジュラーなcontrib」というものでした。clojure-contrib.jarの各ライブラリは別個のリポジトリを与えられましたが、メンテナに立候補する人がいなかったライブラリや、既にClojureのコアに統合されていたライブラリは移行されませんでした。また、ライブラリはclojure.contrib.<ライブラリ>名前空間ではなく、clojure.<カテゴリ>.<ライブラリ>形式の名前空間に移動されました。例えば、clojure.contrib.combinatoricsライブラリはclojure.math.combinatoricsに、clojure.contrib.jmxライブラリはclojure.java.jmxに、といった具合です。

モジュラーなcontribは当初「どこにライブラリが移動したかわかりづらい」「すぐに1.3へ移行できるわけではないのに分断させるのはやめて欲しい」「自分が使っているライブラリは移行されていない」「一つのJARをクラスパスに置いておけばどれでも使える手軽さが失われた」などの批判を浴びました。開発者達もそれに応じて「clojure.contribはどこに行ったのか」やライブラリの一覧といった文書を用意しました。また、モジュラーなcontribは継続的インテグレーションを活用しており、Clojureの1.2から開発版(執筆時点では1.5)までの各バージョンとSun JDK 1.5、1.6、Oracle JDK 1.7、IBM JDK 1.5、IBM JDK 1.6、OpenJDK 1.6の各Java実装を組み合わせてテストされています。(例:clojure.math.combinatoricsのマトリクス)移行されなかったライブラリの中でも人気なものには代替となるライブラリがあったことや、プロジェクト管理ツールのLeiningenが既に広まっていたこともあって、モジュラーなcontribは中止されることなく、現在も新しいライブラリを仲間に迎えながら継続されています。

現在:1.4

バージョン1.4は2012年4月にリリースされました。つい最近だった気がしますが、もう半年も経っているのですね。2012年の3月はClojure/Westが開催されました。「/West」は東海岸のノースカロライナ州ダーラムで毎年開催されているClojure/conjに対して、西海岸のオレゴン州ポートランドで開催されたことに由来しています。また、5月にはEuroClojureが開催され、ヨーロッパのClojureコミュニティの熱気を感じました。

1.4は1.3に比べるとおとなしいリリースですが、堅実な改善が含まれています。mapfilterの結果をベクタで得るための(vec (map ...))(vec (filter ...))に代わるmapvfilterv、マップを対象にreduceする時の定型句(reduce (fn [acc [k v]] ...) ...)に代わるreduce-kvは、よくあるパターンをコアの語彙として取り込む姿勢を感じさせます。また、追加の情報をマップとして持たせることができるclojure.lang.ExceptionInfo例外とそれを扱うためのex-infoex-dataもJavaの枠組みに従いつつ利便性を高めようという試みの一環でしょう。その他にもコンパイルオプションを指定する*compiler-options*の追加などがありますが、ここでは二つの変更に焦点を当てようと思います。

Reader Literals

ClojureはCommon Lispのdefmacroに近いマクロ(バッククオートがシンボルを名前空間込みで扱うことにより、展開時の変数捕捉は意図的に捕捉されるように書かない限り回避される点が異なる)を持っています。しかし、ユーザーがリーダーマクロを定義する機能は提供されていません(が、リーダーの実装クラスにリフレクションで手を加えれば書けます。止めた方が良いですが)。ClojureはPaul Grahamの言う「言語の全てが常に在る」言語としては少々難があるというわけです。

確かに、リーダーマクロがあると字句レベルで文法が異なる方言を簡単に作り出せるため、場合によっては他者とコードを共有する妨げにもなりえます。しかし、正規表現を書くたびに細心の注意をはらってメタ文字をエスケープ(NGワード:Emacs Lisp、:help 'magic')した上で(java.util.regex.Pattern. escaped-pattern)と書く代わりに#"パターン"というリテラルをコアで用意したのと同じように、簡便のためのリテラルをユーザーが用意することさえ1.3までは許されませんでした。1.4で追加されたreader literals(tagged literalsとも呼ぶ)はその制限を緩めるものです。

1.4から、クラスパスのルートでdata_readers.cljを探し、そこに書かれているマップの情報に従ってリーダーに関数を登録し、コードの中で対応する記述があれば通常のリードの後にその関数へフォームを渡します。例えば、foo.jarのルートに{foo/person foo.person/from-literal}と書かれたdata_readers.cljがあるとします。そこで、foo.jarを利用するコードで#foo/person {:name "Masanori Ogino", :tz :JST}と書くと、(foo.person/from-literal {:name "Masanori Ogino", :tz :JST})が評価され、その値が#foo/person {:name "Masanori Ogino", :tz :JST}の値になります(実際には#'foo.person/from-literal:prevateメタデータがtrueでも動作します)。また、read時の*data-readers*を同様のマップで束縛することでも同じ効果が得られます。

1.3までのClojureはdata_readers.cljを読み込まないため、単一のJARで1.3以前を利用するプログラムに対しては関数を提供しつつ、1.4以降を利用するプログラムには関数に加えてリテラルも提供することができます。ちなみに、登録する関数の:privateメタデータの値にかかわらず、リテラルを使うコードの名前空間からその関数を含む名前空間に対するreferによる依存関係がないとエラーが発生します。また、foo/のような接頭辞のないリーダー名は言語自身のために予約されています。

これで#""に相当するものが自分で書ける……というわけではありません。というのも、通常のリードが終わった後に登録した関数が通常のリードによって得られるフォームを受け取るので、"\n"をそのリテラルに与えると、登録した関数が受け取る頃にはバックスラッシュと小文字のnから改行に変換されているのです。しかし、見方を変えればClojureのデータ構造として表すことで文字のストリームをパースする手間を回避できるとも言えます。

reader literalsという枠組みを提供すると同時に、言語自身にもこれを利用した2種類のリテラルが導入されました。1つは日時を表す#instリテラルで、デフォルトではjava.util.Dateに変換されますが、*data-readers*clojure.instant名前空間の関数を使うことでjava.util.Calendarjava.util.Timestampにも変換できます。もう1つはUUIDを表す#uuidリテラルで、java.util.UUIDに変換されます。

New Dot Syntax for Record and Type Field Access

2011年に公開されたClojureScriptはJVMやCLRのClojureとは異なる点を持ってはいますが、Clojureファミリーの新しい仲間としてコミュニティに歓迎され、急速に改善が続けられてきました。特にWebアプリケーション開発者にとってのClojureScriptは、クライアントサイドもClojureファミリーで書けるようになり、更にはNodeさえあればサーバーからクライアントまでClojureScriptで書けるというエキサイティングなプロジェクトです。

しかし、困ったことも起きました。その1つが(.p o)(. o p)といったドット記法です。JavaScriptではメソッドといえどプロパティの値が関数オブジェクトというだけなので、(.p o)(. o p)o.pの値を意味し、(. o (p))(.p o arg)(. o p arg)(. o (p arg))o.p()またはo.p(arg)を意味するようになっていました。この挙動は引数やプロパティを囲む括弧の有無によって意味が変化する点で混乱を招くため、(.-p o)のように-をプロパティの名前に前置するとプロパティの値を意味し、(.p o)(. o p)は引数や括弧がなくとも関数呼び出しとして扱われるようになりました。

この変更は他の公式実装にも導入され、defrecorddeftype(1.2で追加された、recordやtypeと呼ばれる新しい種類のデータ型の定義に使うマクロ)で定義された型のフィールドを(.-name user)のように書いて参照できるようになりました。(Clojure-Pyもこれを採用)ClojureとClojureScript両方で動くライブラリを書く際にも役立つようです。とは言ってもClojureScriptと他の実装の間には他にも様々なギャップがあるので、両方で動かすことを意識して書かないと動かないことに変わりはありません。

未来:1.5

未来のことはお伝えできません。お察し下さい。しかし、既にClojure 1.5のbeta 1がリリースされています。ここではこの文章を書くのが来年の12月の私だったら確実に言及するであろう、1つの変更を紹介します。cond->as->some->などのユースケースがうまくイメージできなくて、自分で書けるトピックが1つしか無かったのは秘密)

最終的なリリース時期がわからないのでリリースされた頃のイベントと呼べるのかどうかわかりませんが、2012年11月にはClojure/conj 2012が開催されました。今のところClojure/conj 2012のビデオは公開されていませんが、タイトルを見る限りでは昨年に続いてClojureScriptの発表が複数あり、コミュニティの強い関心が伺えます。

clojure.core.reducers

Java SE 7に追加された要素の中で、あなたのお気に入りは何ですか。invokedynamic?(JRuby、Dyn.js、Nashorn等の方面の方でしょうか)文字列に対するswitch?それともtry-with-resources?他の機能?そもそもJava SE 7は使ってない?

人によって異なると思いますが、Clojureコミュニティの中で特に注目されてきたのはinvokedynamic……ではなく、Fork/Joinです。java.util.concurrentパッケージはClojureにとって誇張せずとも生命線であり、gensymでさえアトミックなカウンタを書くために利用しています。また、Clojureがデータ構造の不変性を好むのも並行プログラミングの困難さを軽減し、状態遷移の管理をデータ構造から共有戦略に合わせて用意したatomrefのような参照に委譲できることが主な理由であるなど、並行・並列プログラミングはClojureの設計の様々な部分に影響しています。そのため、Executorが対象とするよりも細粒度のタスクに合わせて設計されたFork/JoinはJava 6の時代からJSR-166yのリファレンス実装でClojureからの利用を実験されてきました。

かつてclojure.parで実験されていたコードは当初clojure.parallelに完成版が置かれると言われていましたが、最終的にはclojure.core.reducersとして1.5から導入される予定です。ClojureCLRも.NET 4で導入されたSystem.Threading.Tasksを使って同様のインターフェースを提供します。そのためclojure.java.reducersにはならなかったのでしょう。また、JVM版は前述のリファレンス実装を使うことでJava 6でも同じ機能が利用でき、CLR版は.NET 3.5用のフォールバック機能を内部に備えています。

clojure.core.reducersの中心となる関数はreducefoldreduceとほぼ同じ機能を持つ関数の名前として関数型言語でよく使われる)です。その内、foldは可能であればFork/Joinフレームワークを利用し、並列化できない場合はシーケンシャルなreduceにフォールバックします。並列化できるデータ構造はClojureのベクタとマップです。(正確にはIPersistentVectorを実装したクラスとPersistentHashMapのインスタンス。小さいマップに使われるPersistentArrayMapは除外される。実用上もそのような小さいマップはFork/Joinのオーバーヘッドで並列化した分の性能向上が打ち消されるので効果がない)

また、mapfiltertakeなどの関数も用意されていますが、clojure.coreの同名の関数のようにその場で変換したシーケンスを返すのではなく、foldまたはreduceした時に変換してから処理されるようにするラッパーを返します。mapfilterなどの要素の順序に依存しない関数のラッパーはfoldで並列化され、takeなどの要素の順序に依存する関数のラッパーはreduceにフォールバックされます。

並列化される変換関数とデータ構造の組み合わせを使っている場合でも注意しなければならないことがあります。foldは初期値を取りません。また、foldに渡す関数は要素の前後関係を入れ替えない範囲で要素を組み合わせる順序に依存せず、引数なしで呼び出された場合に任意のxに対して(= x (f x (f)) (f (f) x))を満たす値を返さなければなりません。引数なしで呼び出せない関数は実行に失敗し、それ以外のルールに反する関数は処理自体はできますが正しい結果を得られません。

引数なしの呼び出し以外について数学の言葉を借りて表現するなら、foldに渡す関数の演算は演算の対象になりえる任意の値の集合に対して結合律を満たし、なおかつ単位元が存在しなければなりません。このような演算と集合の組をモノイドと呼び、並列計算の分野ではモノイドをなす演算の畳み込み(fold)は並列化できることが知られています。例えば、+*strclojure.set/unionなどはこの条件を満たします。(clojure.set/intersectionは単位元である「取りうる値全てを含む集合」を表現する方法が今のところ用意されていないので条件を満たさない。ただし、そのような集合があらかじめ分かっていて、Clojureのセットとして用意できる場合は後述のmonoidを使えば可能)モノイドだが引数なしで呼び出せない関数のためにmonoidというヘルパー関数も用意されています。

おわりに

Clojureを活用していて最新状況も追っている方は感づいたかもしれませんが、私自身が最近のClojureを追えていません。この文章も元々はClojureの言語コアの変更を追うために書いていたメモが元になっています。Clojureでコードを書くために必要な情報であるライブラリや開発環境の変化についてカバーできていないので、有用性も低いかと思います。例えばRitzを知らないと、ブログの記事に従ってSwank Clojureを入れて、あとでアチャーってなりますよね。私のことです。また、コミュニティ関係もカバー率が低いです。ここで紹介されているイベントはClojure/coreのメンバーが参加し、コミュニティから遠ざかっていた私の耳にも入るくらい有名なイベントのみです。

もしあなたにとって少しでも有用な内容があったなら嬉しいです。

改訂履歴

  • 2012-12-12: Clojure/Westがヨーロッパで開催されたと誤解を与える文面を修正。Westの由来を説明する文を追加。
44
42
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
44
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?