いまどきのClojureのはじめかた
Clojure 1.9あたりから、Clojureの始め方が大きく変わったのですが、その辺りをまとめた記事が見当たらず、すでにClojureをやってる人しか知らない状態っぽいので、急ぎで書いてみました。
大きく変わったのは、 clojure
および clj
というコマンドが導入されたことです。これまではClojureの実行には Leiningen
のようなビルドツールを使うのが一般的で、スクリプト的なコードを書くのには向いてない印象でしたが、1.9からは、 clojure
コマンドに .clj
ファイルを渡すと実行できるようになりました。また、コマンドが用意されたことで、シェル・スクリプト冒頭に #!
で clojure
コマンドへのパスを書くことで、シェルスクリプトとしてClojureコードを記述できるようになりました。
この二つのコマンドをインストールする手順が、環境ごとに用意されています。
インストール
Mac
homebrewに対応してます。
brew install clojure
Linux
まだapt-getやyumとかには対応していませんが、インストール・スクリプトが用意されているので、それを実行すれば簡単にインストールできます。
こちらのページに書いてある3行のコマンドを実行すると、Clojureがインストールされます。
https://clojure.org/guides/getting_started#_installation_on_linux
Windows
残念ながら、まだ提供できていないようです。 とはいえ、clojureは単なるjarファイルなので、clojure.jarを手動配置することでなら、もちろん実行可能です。
しかし、Microsoft自身がWindowsで動くLinux環境を提供しているので、そっちで実行した方が簡単なように思います。
REPL
なにはともあれ、REPLです。
Clojureをインストールすると、clj
コマンドとclojure
コマンドの2つのコマンドが使えるようになります。
clj
コマンドのほうがREPL実行用コマンドです。単にclojure REPLの実行だけではなく、REPL上でのコマンドヒストリ機能などがオンになるよう、セットアップしてくれます。
clojure
コマンドは.cljファイルを実行するためのコマンドです。こちらがあるので、clojureがインストールされている環境であれば、 #!/usr/bin/env clojure
をスクリプトファイルの先頭行に書くことで、シェルスクリプトのようにclojureプログラムを実行することもできます。
というわけで、clj
コマンドを実行すると、Clojure REPLが起動します。
(+ 1 2) ;;=> 3 (require '[clojure.string :refer [split upper-case]]) ;=> nil (->> (split "aaa,bbb,ccc" #",") (map upper-case)) ;=> ("AAA" "BBB" "CCC")
Clojureの関数を覚えてくると、ちょっとした計算とかはREPLでささっとプログラム書いたりしてしまいます。
使い終わったら、Ctrl+dで終了です。
スクリプトを書く
Clojure 1.9からは、Clojure自体に依存ライブラリを処理する機能が組み込まれました。1.8までは、Leiningenなどのビルドツールが必要でしたが、必要なことが「依存ライブラリの自動ダウンロード」だけなのなら、Clojure単体でできるようになったのです。
もちろん、ビルドツールには依存性管理以外にもいろんな機能があります。そちらを使いたい場合(「プロジェクト」規模になると大抵は必要でしょう)は、Clojure単体では難しいので、すなおにビルドツールを使いましょう。一番メジャーなのは Leiningen です。こちらもインストールスクリプトがあります(Windows用のbatファイルもあり)
さて、ちょっとしたスクリプトを書きたいけど、そのためには外部ライブラリが必要だ、というケースはよくあります。たとえばClojureでhttpアクセスが必要な場合、clj-http という有名なライブラリを使うことが多いですが、当然これは、別途用意する必要がありました。
Clojure 1.9には、 deps.edn
という特殊なファイルをカレント・ディレクトリに配置することで、必要な依存ライブラリを自動ダウンロードする機能が追加されました。
clojureコマンドを実行するときに、カレント・ディレクトリにdeps.ednがあれば、その中に記述された依存ライブラリをすべてダウンロードしてから、スクリプトを実行します。つまり、スクリプトとして書いたclojureプログラム(テキストファイル)といっしょにdeps.ednを配ることで、スクリプトから外部ライブラリを利用することができます。
deps.ednの解釈は、clojure
コマンド実行時だけでなく、clj
コマンド実行時にも行われますので、REPLで作業したいけど、その作業には外部ライブラリが必要、というときにも、ささっとdeps.ednを書けば、REPL内で外部ライブラリを使うことができます。
deps.ednは次のような、Clojureのマップ文法で書かれたファイルです。
{:deps {clj-http {:mvn/version "3.9.0"}}}
このようなファイルがおいてあるディレクトリでclj
コマンドを実行してみましょう。REPL起動時にclj-httpのダウンロードが行われます。
ちょっと、http接続を試してみましょうか。
(require '[clj-http.client :as http]) ;=> nil (http/get "https://google.co.jp/" {}) ;=> GoogleページのHTMLテキスト
こんな感じで、必要なライブラリを使ってREPLで作業できるわけです。
deps.ednを使ってもう少し複雑なプログラムを組む
1ファイルというほど小さくはないけど、ビルドツール使うほどでもない、使い捨てのツールプログラムを作る、みたいなこともあるでしょう。Clojureだと、プログラムは名前空間に分割して書いていくのが普通で、そうするとファイルも複数になりますし、名前空間に階層がある場合は、ディレクトリも必要です。
deps.ednと同じ場所に src
というディレクトリがあれば、その下にソースファイルが配置されているものと解釈します。srcの下に、名前空間に合わせたディレクトリ階層を作れば、ちゃんとソースを見つけ出してくれます。
- deps.edn - helloworld.clj - src/ - util/ - string.clj - net.clj
上記のように配置すれば、このプログラムは、 helloworld.clj
という実行用スクリプトとは別に、
- util.string
- util.net
の2つの名前空間が追加されたプログラムとなるわけです。たとえば、 helloworld.clj
から、require
を使って、util.string
やutil.net
名前空間を利用することができます。
Clojure単体でも、deps.ednと組み合わせることで、ちょっと規模の大きめのプログラムでも、作ることができるわけです。
エディタを用意する
ClojureはLISP系の言語ですので括弧を多用しますが、世のLISPerたちがどう括弧を扱ってるかというと、括弧の対を「見づらいもの」としてではなく、むしろ利用すべき「構造」と捉えています。全てが括弧で囲われているのだから、括弧単位で移動したりカットしたりできる、専用のエディタ機能を使っています。この操作に慣れると、括弧にはむしろありがたみを感じるのです。
Clojureプログラマは、エディタの力を使って、括弧を巧みに利用します。ただのテキストエディタで括弧を扱うのは、誰でもつらいものです。それは、他の言語でも同じです。おなじみの言語を、何のサポートもないテキストエディタで書こうとすると、結構大変なはずです。
また、書いているプログラムをREPLを使ってさっと動作確認しつつ書いていく、というスタイルがメジャーなのもあり、エディタから簡単にREPLを起動したり、REPLにソースを送り込んだり、REPLから作成中のプログラムの名前空間にアクセスして関数を実行したり、プロジェクト内の関数を探したりしたい。いわば、IDE的な機能が欲しいです。
これらのサポートなしにClojureコードを書くのはやはり大変で、ただのテキストエディタでJavaを書く的なしんどさがあります。だから、Clojureをサポートしたまともなエディタを用意しましょう。
Clojureでもっともメジャーな開発環境は
Emacs + CIDER
です。エディタとしてemacsを、そのプラグインのCIDERをIDE機能として開発します。
もし、Emacsは得意じゃないけどVimは使える、という方々は
Spacemacs + CIDER
です。SpacemacsはVimキーバインドで使えるemacsのようなもので、CIDERも使えます。
しかし、どちらも心得がない、という方が、プログラム言語をはじめるのにEmacsやVimのような複雑なエディタを覚えるところから始めないといけない、というのはあまりにもハードルが高いと思います。
なので、私の一押しは、最近いろんな言語をサポートしてることでユーザーの多い、IntelliJ IDEAを使うことです。IDEAのプラグインとして Cursive というClojure開発支援プラグインがあります。こちらはビジネスで使う場合は有料ですが、オープンソース開発や学習用とでは無料です。有料版は、個人に紐付くライセンスだと99ドルです。企業契約で個人に紐付かせない(いわゆる「ライセンス数」で買うタイプ)だともうちょっと高いです。
とりあえず、学習用に試す分には無料ですのでおすすめします。基本的なエディタ操作は、IDEAの標準操作がそのまま使えますので、すでにIDEA使っているなら学習コストも低いです。CursiveにはREPLサポートもあるし、作業中の関数をREPLに送る機能なども備わってます。
(ビジネスでつかうなら、自分が仕事で使うツールを作ってくれた作者への敬意として、ちゃんとお金を払おう)
構造編集 (Structural Editing)
いずれのエディタを使うにしても、括弧をかっこよく効率的に扱うための 構造編集(Structural Editing)
を覚えることをおすすめします。PareditとかParinferが有名です。Spacemacsだと、SPC+kを押したら表示されるメニューに、構造編集のための項目があります。
Clojureでコードを書いていると、「この括弧内に次の行の括弧を丸ごと移動したい」とか「この部分を括弧の外に追い出したい」とか「この括弧内のコード、もう不要だから丸ごと消したい」とか「この括弧全体を丸ごとコピーしたい」といったことがしょっちゅうあります。構造編集でこれができます。
(my-great-func {:name "yano", :place "japan"})
のようなプログラムがあるとして、「おっと、この my-great-func
を実行するには条件があるんだった。whenかifで囲わないと...」と考えたとします。
(when condition (my-great-func {:name "yano", :place "japan"})
とか書いてバグるわけです(上のコードは、閉じ括弧が足りません)。
まず、構造編集使用中は、括弧は常にペアで書き込まれます。開き括弧を書くと閉じ括弧も自動的に打ち込まれます。次のような感じになるでしょう。
(when condition) (my-great-func {:name "yano", :place "japan"})
勝手にペアになるので、(when conditionと書いていくと、必ず括弧の内側になります。
ここで、conditionの後ろに、(my-great-func ...)
の括弧を丸ごと持ってきたいわけです。
キー操作はエディタによって異なると思いますが、Spacemacs+CIDERなら、when condition
の括弧の内側にカーソルがある状態で、「SPC k s」とキーを打つと、「Slurp(吸い取る)」という操作が実行されます。つまり、(when ...) の次にある(my-great-func ...)という括弧のブロックを、(when ...)の括弧の中に吸い込むわけです。
結果として、次のようなコードに変わります
(when condition (my-great-func {:name "yano", :place "japan"}))
- 括弧を打ち込むと常にペアで打ち込まれる
- 括弧単位で、隣の括弧を吸い込んだり、今の括弧を外に追い出したりすることで、カット&ペーストによって間違って括弧を消してしまうなどのミスが起きなくなる
- 括弧単位で移動するので単なるカット&ペーストより圧倒的にミスりにくい
Clojureプログラマ(そしておそらくはLISPプログラマ)は構造編集で括弧を扱っているので、括弧を「どこで閉じてるのかよくわからない読みにくいもの」ではなく「括弧単位で追い出したり吸い込んだり、カットしたりペーストしたりできる、便利な編集単位だ」と思ってるのです。かならず開き括弧と閉じ括弧で囲われている、というLISPの特性があってこそなのです。
余談ですが、Clojureは括弧の種類が () だけではなく、[], {}, なども使うので、エディタ上ではこれらが色分けされてちょっと読みやすい、という利点もあったりします。
構造編集にはいろんな機能があって、正直私も全部覚えてはいないです。
- Slurp (隣の括弧を吸い込む)
- Barf (括弧を追い出す)
- カット (括弧単位でのカット)
- コピー (括弧単位でのコピー)
- 削除 (括弧を丸ごと消す)
この5つを覚えるだけで、作業効率が全く変わってくるので、おすすめです。
SlurpとBarfは、「前を吸い込む」と「後ろを吸い込む」など、前後どっちを処理するかで、それぞれ2種類ありますが、まずは「後ろを吸い込む」「後ろに追い出す」のほうを覚えることをおすすめします。実際のコーディングでやりたいことは、大抵こっちのはずです。慣れたら前も処理できるように覚えればいいでしょう。
構造編集は、Emacs, Spacemacs, Cursiveのいずれのエディタでも使えます。ただし、これらの機能を呼び出すキー操作はエディタによって異なっているので、自分の好きなエディタでの操作を調べてみてください。
まとめ
JavaVMが入っている前提であれば、Clojure 1.9からは、 clojureコマンドのインストール
だけで、簡単にClojureを始められ、シェル・スクリプトのようにClojureコードを実行することができるようになりました。若干ハードルが下がったように思います。また、 deps.edn
が導入されたことで、外部ライブラリをスクリプトコードから簡単に利用することできるようになりました。
ちょっと、試してみるのはどうでしょうか。
オブジェクト指向とはまったく違うClojureの世界と実際のWeb開発
今回は、記事ではなく、【京都】LINE Developer Meetup #38で、ClojureとWeb開発について話したので、その時のスライドを紹介します。
こちらからご覧ください。(注:Slideshareの仕様なのか、元のスライドでは改行位置など揃っていたのが崩れてしまっています)
Clojure + core.async による非同期&並列プロセスの世界
core.asyncによる非同期プログラミング
core.async はClojure用の、事実上標準の非同期プログラミングのライブラリです。
core.asyncの一番わかりやすい説明は、「Go-langのchannelのClojure版」という言い方でしょう。goマクロによってgo-blockを作り、そのブロック内が非同期に動きます。このブロックが常駐すれば、軽量プロセスというやつになります。プロセス同士のやりとりをする口として、チャネル(channel)があります。core.asyncを使ったプログラムでは、チャネルへの入出力を介して非同期軽量プロセスにデータを処理させることで、全体のシステムを作り上げます。
goマクロはステートマシンを作り、チャネルへの入力があるたびにマシンが1回転します。この一回転時に、チャネルを待ち受けていたgoブロックにスレッドが割り当てられ、次のチャネル入出力までCPUを使って処理が動き、チャネルの入出力でまた別のgoブロックに処理が映り、という形で、限られたCPU上で、スレッドを山ほど起動することもなく、効率よく動作するのが売りの一つです。
このような仕組み(OSの協調型マルチプロセスと同じような原理)なので、goブロックは実際にはプロセスでもスレッドではなく、ステートマシンによって管理されたプログラム単位に過ぎません。よって、core.asyncはスレッドが一つであってもちゃんと動きます。core.async開発当初から、ClojureScript(JavaScriptをホスト言語としたClojure実装)でも動くことを想定して作っていたということですので、ならではの実装でしょう。
シンプルな仕組み
core.asyncの使い方については公式ドキュメントとかのほうが詳しいので詳細はそちらを見てもらうとして、簡単に概要だけを書くと、実行単位をgoで囲み、そのなかでchannelを読んだり書いたりすると、goで書かれた実行単位が次々と切り替わって実行されます。
以下は、CloureのREPLに打ち込めばそのまま動作する、core.asyncを使ったプログラムコードです。
(import '[java.util Date]) (require '[clojure.core.async :refer [chan go-loop >! <! timeout] :as async]) (def ch (chan)) ; チャネルを作る ;; 書き込み非同期ブロック (go-loop [] (when (>! ch (Date.)) ; チャネルに書く (<! (timeout 2000)) ; 2秒待つ (recur))) ;; 読み込み非同期ブロック (go-loop [] (when-let [date (<! ch)] ; チャネルを読む (println "now:" date) (recur)))
go-loop
は
(go (loop [] ;; 処理 ))
の省略形で、多用するので用意されています。
単純なgoブロックは、非同期処理が終わるともう2度と実行されません。しかし、loopすれば「ずっと動き続ける非同期ブロック」を作れます。これが「軽量プロセス」に近いものです。軽量プロセス同士がチャネルを使ってデータをやりとりする、というプログラムを作るには、毎回goとloopを書かなければいけなくて、少々面倒なので、二つをまとめたgo-loopマクロが用意されています。
このプログラムは、片方のgoブロックが現在日時をチャネルに書き込んで2秒待つ、もう一つのgoブロックは同じチャネルを読み込み、読めたらそれを画面に出力します。いずれのgoループもチャネルが閉じられるまでloopし続けます。現在時刻を生成するプロセスと、受け取った時刻を出力するプロセスの、2つの軽量プロセスが動いていると考えればいいでしょう。
このように、core.asyncでは、プログラムを処理単位ごとにgoブロックで囲って非同期処理にし、そのgoブロック間でのデータのやりとりにはチャネルを使います。
goブロックは、チャネルからデータを読み書きしようとして、もしチャネルにまだデータがなかったり、チャネルにまだ書けない状態だったら、park(待機)状態になりスレッドを解放します。そして準備ができればまたスレッドに割り当てられて動き出します。
非常に少ないスレッド数で、たくさんの非同期ブロックを実行できるわけです。
複雑なスレッド制御を書かなくとも、シンプルにチャネルを読み書きするところをgoで囲むことで、簡単に効率よい並列プログラムを書けるのがcore.asyncの強みです。プログラムを書く側は単純にやりたいことを上から下へ書いていくだけでいいのです。callback hellと呼ばれるような、非同期コールバック関数が何段にも重なるようなことはありません。goブロックを上からどんどん書いていけばいいのです。
と、ここまでは、core.asyncにもともと備わっていた基本機能です。ここに、Clojure 1.7の言語拡張により、新しい要素が追加されました。この機能により、core.asyncは一段と便利になりました。
Transducerの登場
core.asyncのために、Clojureの言語レベルでの拡張までも行われました。それが、Clojure 1.7でのTransducersの導入です。
Transducerというのはおおざっぱに言うと、map処理やfilter処理から、対象となるオブジェクト(コレクション)を省いて、変換処理だけを抜き出して抽象化したものです。(map change-fn coll)という処理なら、重要なものは「ひとつひとつの要素にchange-fnを適用する」という変換処理であって、collは引数に過ぎない、だったらその変換処理部分だけを抜き出して別のオブジェクトとして扱えるようにしよう、というわけです。
実際、transducerの作り方は、上の説明通りのものです。
(map change-fn coll) ; いつものmap処理。collの各要素にchange-fnを適用する遅延リストを作り出す
(let [xf (map change-fn)] ; 同様の処理を行うtransducerを生成する。引数は後から渡すことができる。 (sequence xf coll)) ; collの各要素にxfというtransducerを適用する遅延リストを作る
いつものmapやfilter関数呼び出し時に、対象となるcollectionを渡さなければ、変換処理だけを取り出したtransducerになるのです。
さらに、transducerは合成することもできます。transducerは特定のルールに則って実装された関数に過ぎないので、Clojure標準の関数合成関数 comp で簡単に合成できます。
(comp 第1のtransducer 第2のtransducer 第3のtransducer)
ただの関数をcompした場合、一番後ろの関数から順に実行されます。しかしtransducerの場合、その構造上、実際の実行は頭から行われます。上の例では、第1のtransducerから順に、3まで実行するような、合成transducerができます。
(require '[clojure.string :as string]) ;; string/upper-caseで大文字にmapした後、 ;; T以外のものにfilterするtransducerを作る (def xf (comp (map string/upper-case) (filter #(not= % "T")))) ;; sequenceは引数にtransducerを適用したリストを作る ;; 大文字に変換された後、Tでない文字だけにfilterされます。 (apply str (sequence xf "TEST")) ; => "ES"
なぜこのようなアイデアが用意されたかというと、(経緯はいろいろあるんでしょうが、私の認識では)core.asyncの開発途上で必要性が認識されたからです。もともとcore.asyncには、チャネルに入出力するデータに対してmapやfilterを実行するための、専用の関数がたくさん用意されていました。 clojure.core.async/map>
とか。でも、標準のmap関数との違いは、処理対象がコレクションかチャネルか、という違いだけで、実際にやりたいこと(mapしたい、filterしたい)は同じなわけです。だったら、その同じ部分だけを抜き出そうというのは自然な発想です。
というわけで、Clojure 1.7にはTransducerが導入され、mapやfilterといったもともとあった多くの関数が拡張されて、いままでの処理のほかに、transducerを作り出す機能が追加されました。
同時に、core.asyncにあった、専用のmapやfilterといった関数群は、deprecated扱いとなりました。代わりに、チャネルに対してtransducerを設定することができるようになりました。これにより、チャネル入出力=関数の実行、という世界ができあがりました。
現在のcore.asyncは、単なるClojure版goルーチン実装の域を超えて、transducerの導入により、関数実行エンジンとしての機能を備えました。
;; 入力した文字列を大文字化するtransducerを設定したチャネルを生成する (require '[clojure.string :as string] '[clojure.core.async :refer [chan go >! <!]]) (let [ch (chan 1 (map string/upper-case))] ; 大文字化するtransducerをセットしたチャネルを作る ; 小文字を書き込んでみる (go (>! ch "test")) ; 読み込んで出力すると、大文字になっている! (go (println "result:" (<! ch)))) ;;=> result: TEST
core.asyncには、チャネルとチャネルを結合する pipe
という関数があるので、これを使って変換処理をつなげることもできます。
(require '[clojure.core.async :refer [chan go-loop >! <! pipe onto-chan]]) (let [only-odd-ch (chan 1 (filter odd?)) ; 奇数だけを通すチャネル double-ch (chan 1 (map #(* % 2))) ; 2倍にするチャネル ch (chan) piped (-> ch (pipe only-odd-ch) ; ch を only-odd-ch に連結する (pipe double-ch))] ; 前行の連結結果をさらに double-ch に連結する ;; onto-chan関数は内部でgoブロックを使ってコレクションの中身を非同期にチャネルに書き込む (onto-chan ch [1 2 3 4 5 6]) ;; 連結した末尾にあるpipedから、非同期にデータを一つずつ読む (go-loop [] (when-let [data (<! piped)] (println "data:" data) (recur)))) ;; 奇数の1, 3, 5 が2倍になって順番に出力される ; => data: 2 ; => data: 6 ; => data: 10
pipeによってチャネルを結合してtransducerを順次実行していくことができるわけです。
しかしこれだけでは、関数を順次実行しているだけで、直接関数を呼ぶのに比べて、めんどくさくなってるだけではないでしょうか。
実はこれだけでも、関数を直接呼び出すのとは違う利点もあるのですが、そこは一旦置きましょう。core.asyncは並列実行ライブラリです。transducerの実行を並列化しましょう!
pipelineによる関数の並列化
transducerの登場で、データの変換操作を簡単に抽象化することができるようになりました。チャネルの入出力と関数を紐付けることが可能になったわけです。
ここでpipelineが登場です。
pipelineというのは、その名の通り(チャネルとチャネルの)パイプラインを構築する関数です。パイプラインの構築には、入力と出力のチャネルに加えて、実行したい関数、それに並列数を指定できます。
pipeline関数とは、チャネルからチャネルにデータを転送するときに関数を適用するという処理の、「関数を適用する」部分を並列化しよう、というものです。
(pipeline 4 out-ch my-great-transducer in-ch)
この1行で、in-chからout-chへのパイプラインが構築されて、in-chにデータを入れると、out-chから出力されます。その途中に、my-great-transducerによりデータが処理されるのですが、このtransducerの実行は、最大で4並列で処理されます(ちなみに、out-chへはin-chにデータが入ってきた順序で結果が出力されることが保証されているので、順番は壊れません)。チャネルに立て続けに4つデータが入ってくれば、それらは並列で処理されるということです。
チャネル、transducer、pipelineの3つで、core.asyncの役者がそろいました。core.asyncはClojureでgoブロックを実現するライブラリでありつつ、その上に、「transducerという変換関数を並列実行するpipelineを構築して、そのpipeline同士をつなげることで並列プログラムを構築する」という世界ができてるわけです。
この仕組みは、関数によるプログラムモデルにもよい意味での影響を与えます。
たとえば、たくさんのリクエストを受け付けるサーバプログラムで、データ処理プロセスが一つ存在して、それに処理を依頼するようなプログラムモデルを考えてみましょう。サーバプログラムは、たくさんのリクエストを並列に受け付けますから、このエンジンももちろん並列で動いてほしいです。ならば、データ処理を行うpipelineを構築して、その入力チャネルにデータを入れればいいのです! pipelineが、並列処理を実行する軽量プロセスとなるのです。
pipelineは入力がない限り待機してCPUを消費しませんが、入力チャネルに書き込めばいつでも動きます。go-loopで処理を繰り返すのと同じことを、pipelineは行っています。ただ、go-loopは処理を非同期化してくれはしても、並列化はしてくれません。pipelineは、transducerを並列に実行してくれます。
効率的なデータ処理エンジンを構築する
このプログラムはサーバプログラムなので、たくさんのリクエストを外部から受け付けます。秒間100とかもっととか、とにかく並列にたくさんの要求を受け付けます。
一方でサーバのCPU数には限りがあるわけで、すべてのリクエストごとにデータ変換関数を無制限には実行したくありません。一度に実行する変換処理は、例えばCPUコア数までとかに絞りたいわけです。そこで、プログラムの中心に「データ変換エンジン」と呼ぶ、入出力を受け付けるプロセスを用意します。リクエストを受け付けたら、このエンジンにデータを投入すると、並列にデータ変換を実行し、最後にレスポンスを返すものとしましょう。
リクエストを受け付けるサーバ自体の実装は今回のテーマではないので無視することにして、とりあえず read-ch
を読み込むとリクエストが読み込めて、 write-ch
に書き込むとレスポンスが返る、ということにしておきましょう。
用意するものは
- リクエストをパイプラインに流し込む関数(始端処理)
- データを変換する関数
- レスポンスを出力する関数(終端処理)
これだけです。パイプラインには必ず始まりと終わりがあるので、はじまりには何かしらの始端処理が、終わりには何かしらの終端関数があるはずで、今回の場合の始端処理はリクエストをエンジンに渡す(パイプラインに流す)ような何かで、終端処理は、「レスポンスを返す」ということになります。終端処理は、たいていの場合は、パイプラインの最後の出力を読み取って、それを(ファイルとかネットワークとか)どこかに書き出す、という処理を行うような、goブロックです。
エンジンを作る
データを変換する関数の内容自体は今回は重要ではないので、とりあえず great-convert-xf
というすごいtransducer関数があることにします。多分、すごい関数をtransducer化してさらにcompを使って合成したようなものです。これを使って並列パイプラインを作ります。
(require '[clojure.core.async :refer [chan pipeline]]) (def in-ch (chan)) (def out-ch (chan)) (def engine (pipeline 8 out-ch great-convert-xf in-ch))
なんと今回はこれでエンジンは完成ですね。engineと名付けたこのpipelineは、in-chにデータが書き込まれると、great-convert関数を最大8並列で実行してくれます!
もちろん今回は一番大変な great-convert-xf
transducerの実装を省略しているから、これで済んでいるわけですが、関数さえ存在すれば、それを並列エンジン化するのは簡単だということがわかると思います。
始端処理
始端処理が行うべきは、ネットワークからのリクエストが入ってくるread-chを読み取って、エンジンの入力であるin-chに流し込むだけです。 ネットワークから来るデータが、そのままエンジンに流し込めるデータ構造の場合は、とても簡単です。
(pipe read-ch in-ch false)
read-chとin-chを直接つなげちゃえばいいのです! 最後の引数falseは、read-chがcloseされた場合に接続先チャネル(in-ch)をcloseしない、という意味です。これを忘れると、一個のリクエストを処理したところでエンジンの入力チャネルが閉じられてしまうので注意です。
ですが実際には、ネットワークから来るデータは、バイナリだったり、JSON文字列だったりするので、エンジンが処理できるデータ形式に変えてから流し込むことになります。go-loop関数で実現できます。
(require '[clojure.core.async :refer [go-loop <! >!]]) (go-loop [] (when-let [req (<! read-ch)] (when (>! in-ch {:data (convert-request req) :write-ch write-ch}) (recur))))
このgo-loopは、read-chが閉じられる(読み取り結果がnilになる)か、エンジンの入力チャネルが閉じられる(書き込みでfalseが返る)かするまで、無限にループします。ポイントは、これはgoブロックなので、無駄にループしてCPUを消費しないことです。チャネルから読み取れるデータがあれば、core.asyncによってgoブロックに処理スレッドに割り当てられます。そして次のチャネル入出力時に再び休止状態に戻り、ほかのgoブロックに処理が回ります。CPUを効率的に利用できるのです。
エンジンに流し込むデータは、加工済みデータと、レスポンスを出力するためのチャネルの、二つの要素の入ったマップです。
終端処理を作る
終端処理は始端処理の逆ですので、似たような処理となります。エンジンからは、エンジンに流し込んだのと同じ形式のマップとしてデータが出力されるものとします。
(go-loop [] (when-let [{:keys [data write-ch]} (<! out-ch)] (>! write-ch data) (recur)))
この終端処理は、エンジンの出力チャネル(out-ch)が閉じられるまで、無限にループします。始端処理と同じく、ループによってCPUを無駄に消費することはありません。
これで、始端処理→エンジン→終端処理というサイクルができあがりました。エンジンはcore.asyncのpipelineを使うことで並列に実行されます。エンジンが処理する関数がひとつだけの、とてもシンプルな例ですが、core.asyncで並列処理サーバプログラムを作るときの基礎がちゃんと入っています。
pipelineを拡張する
ポイントは、一度この構造ができてしまえば、エンジンの部分は容易に変更できるということです。プログラムの大枠として、始端→エンジン→終端、という構造さえできていればよくて、エンジンの部分をもっと拡張しても、この構造さえ変わらなければ問題ないのです。
そりゃそうだろ、エンジン部分のプログラムが難しいんであって、そこを書いてないのだから、というのはその通りです。しかしポイントは、プログラムの拡張を、core.asyncのパイプラインの拡張という形で行えるというところです。
関数は密結合である!
Clojureの作者であるRich Hickeyが、core.asyncについて説明した プレゼンテーション があります。この動画をみると、core.asyncはただgoルーチンをclojureで実現したものというのとは別の観点もあることがわかります。Richは、このプレゼンテーションの中で、(オブジェクト指向の利点と欠点と対比しつつ)関数プログラムの利点と欠点を簡単に話しています。ここでRichが関数の利点としているのは、関数はロジックを抽象化できるということ(一方、オブジェクトはロジックというよりはそれ自体がマシンである、としてます)、欠点は、関数はどうしても密結合になりがちだ、ということです。
関数呼び出しの連鎖を分解して、途中に分岐(if文とか)を挟み込むのは、結構骨の折れる作業です。ロジックが変わるから事実上検証もやり直しです。
そこで、Richは、関数(データの入力と出力がある)を、キュー(データを入力して出力するという機能しかない単純な構造)と組み合わせることで、関数同士の密結合をほどくことができる、と言っています。
それゆえ、Richは、仮にcore.asyncの非同期機能を一切使わずに、キュー(チャネル)を介して関数と関数を結びつけるだけでも、利点はあると言っています。
データ処理エンジンは、複雑な分岐を含んだ巨大な関数です。これを、pipelineを使って、容易に再接続可能な関数の集まりとして作れるのです。何しろ、pipelineというのは、チャネルという名のキューを介して関数同士を接続するためのものだからです。
パイプラインに分岐を組み込む
パイプラインの入出力はチャネルなので、たくさんのパイプラインを用意して、あるパイプラインの出力チャネルを別のパイプラインの入力チャネルとして指定すれば(あるいは、pipe関数で互いの入力と出力チャネル同士をつなげれば)、パイプラインをつなぎ合わせた巨大なパイプラインを作ることができます。
さらに、core.asyncには、チャネルに入ってくるデータを、別の複数のチャネルに、条件によって振り分ける関数が用意されています。pubとsubです。
ある出力用チャネルにpub関数を適用すると、publicationという特殊なオブジェクトを作れます。publicationは、チャネルに入ってくるデータの種別を識別することができます。さらに、別のチャネルをsub関数を使ってpublicationに登録することができます。この登録時に「このチャネルには、データの種別がAのデータだけを流してください」という指定ができるのです。
たとえば、パイプラインに入ってくるデータが「新規データ」「更新データ」「削除データ」に分かれているケースを考えてみましょう。この三種類それぞれ、行うべき作業は微妙に異なります。こういうとき、pub/subを使って、チャネルを三つに分岐させることができます。
(let [publication (pub ch :type)] (sub publication :new new-data-ch) (sub publication :update update-data-ch) (sub publication :delete delete-data-ch))
イメージとしてはこんな感じです(雑な手書き図ですみません)
ここで、「もともとは新規と削除しかないと聞いてた」→「急に、『実は更新データもありました…』とか言われてしまった」というケースを考えてみます。
core.asyncでは、もともとの「新規」用パイプラインと、「削除」用パイプラインに影響を与えずに、「更新」用パイプラインを追加することができます。単に、「更新」用パイプラインを作って、その入力チャネルを、分岐publicationに追加すればいいのです。
core.asyncは名前からも非同期処理で注目されますが、「密結合した関数呼び出しを疎結合にする」という目的も入っているのです。既存の関数(pipelineにtransducerとして組み込まれている)は変更されないので、この部分の再テストは不要です。新しい関数を用意し、テストし、transducer化し、pipelineを構築する。あとはチャネルとチャネルをどう接続するか、というだけの問題です。チャネル同士の接続を工夫することで、関数の分岐や実行順序を制御できるわけです。しかも、各関数は効率的に並列実行されるのです。
バイパスを作る
異常が発生したケースなど、正常なプログラムの流れを無視して、別の処理にジャンプしたいというケースは結構あります。実際、プログラミング言語の例外とキャッチというのは、正常なプログラムを流れを無視して例外処理へとジャンプする処理な訳で、関数をつなげてプログラムの流れを作るパイプラインであっても、やはり同様のことが必要になるケースはあり得ます。
core.asyncでは、pipelineで実行される関数は並列に実行される(別スレッドで動く)ので、単純に例外処理を行うことができません。その代わりに、exception-handlerと呼ばれる例外処理用の関数を使うことができます。exception-handlerは、引数として例外一つを受け取る関数であれば、なんでも構いません。
pipelineに設定したtransducer内でエラーが発生すると、(あれば)exception-handler関数が呼ばれます。exception-handlerは何をしてもよいですが、exception-handler関数の結果が、transducerの実行結果になります。
;; exception-handlerを設定したpipelineを作る例 (let [ex-handler (fn [ex data] (handle-error ex) nil)] (pipeline 8 out-ch (map my-greate-fn) in-ch false ex-handler))
ここでは使いませんが、実は、チャネルを作成する chan 関数にも、exception-handlerを渡すことができます。チャネルにtransducerをセットした時に、transducer内で発生したエラーを処理するために使います。
通常、エラーとなったデータはパイプラインの先に進めたくありません。そういうときは、exception-handler関数の戻り値をnilにします。core.asyncのチャネルにはnilを値として流すことができない仕様ですので、exception-handlerがnilを返すと、そのデータはパイプラインの先には流れません。 代わりに、exception-handlerに別の出力用チャネルを渡しておき、そこにデータを流すのです。このチャネルの先には、例外を処理するためのpipelineを設定しておきます。
;; バイパス用channelとしてbypass-chがすでにあるものとする ;; このex-handler関数は、受け取った例外をbypath-chに転送する。 (defn ex-handler [ex] (go (>! bypass-ch ex)) nil)
このようなexception-handlerをすべてのpipelineに設定しておけば、例外はすべて、bypass-chを経由して、例外処理用pipelineへと流れていくことになります。エラーのためのバイパスを作ったわけです。
core.asyncにはpub/subによる分岐とは別に、mergeによる連結機能もありますから、バイパスに流しておいて処理した後、最終的には同じ終端処理に接続する、といったことも可能です。
;; 終端処理につながる last-ch というチャネルがあるとする ;; すべてのチャネルを連結したall-data-chを作り、それをlast-chに ;; 連結する (let [all-data-ch (merge [new-ch update-ch delete-ch bypass-ch])] (pipe all-data-ch last-ch))
例外を処理するpipelineまで含めると、このシステムの全体像はこんな感じになります。
この柔軟性が、core.asyncの強みです。関数をtransducerとしてpipelineに組み込むことで並列化し、さらに、チャネルというキューを介して、pipelineを柔軟に結合して、システム全体を作るのです。Clojure自身の「データは基本的に不変である」という性質が、非同期処理の安全性を高めてくれています。入力と出力しかない「関数」と、同じく入力と出力しかない「キュー」を使うことでデータの流れを作り、さらにここに、このtransducerという変換処理を組み合わせると、pipelineという並列化関数を作り上げられるのです。これらを自由に組み替えられるわけです!
まとめ
ちょっとした例ですが、パイプラインをつなげて、分岐を伴うプログラムを作り上げるイメージができたでしょうか。
- goとchannelの二つで、非同期ブロック間のデータのやりとりを実現する
- transducerにより変換処理を抽象化し、合成可能にする
- channelとtransducerを組み合わせて、transducerを並列実行するpipelineを作る
- pipeline同士を、channelを介して分岐したり結合したりしてつなぎ合わせる
- データがパイプラインを終端に向かって流れていくとき、すべての処理は自動的に並列に実行される!
go(非同期ブロック), channel(キュー), transducer(変換処理), pipeline(処理の並列実行)という4つの概念を組み合わせることで、並列プログラムができてしまいました!
それぞれが一つの仕事だけを行うような物を、組み合わせて複雑な処理を作り上げる、しかもそれらを混ぜ込まない(コンプレクトさせない)というのは、Clojureの目指す「Simple Made Easy」の世界観とも合っていますね。core.asyncは、Clojureらしい形で、並列プログラムの作り方を変える仕組みを実現した、Clojureのキラーライブラリの一つです。
core.asyncには、他にも、チャネルに入ってきたデータを複数のチャネルに(同じデータを)転送する multiple や、複数のチャネルをマージしつつ、チャネル単位で流れを一時的に停止できる mix、複数のチャネルから最初にデータが入ってきたものを選択できる alt! など、チャネルをつなぎ合わせるための関数が用意されています。これらを使えば、柔軟にチャネルの接続を動的に切り替えるなどの処理もできます。チャネルの組み合わせと並列化こそがcore.asyncの強さであり、ただのgoブロックではないということが分かるでしょうか。
宣伝
clj-ebisu で、core.asyncを使った時の「あるある」なトラブルと回避方法について、ちょっとしたプレゼンテーションをすることになりました。きてね。
Clojureと「Simple Made Easy」
プログラミング言語というのは、その作者が理想とする世界に合うようにデザインされているものだから(みんな信じないかもだけど、Javaですらそうなのですよ)、Clojureのことを理解するには、作者であるRich Hickeyのプログラミング観を知るのが手っ取り早いでしょう。
Rich Hickeyはさまざまなプレゼンテーションを発表していて、多くはネットで見られます。示唆に富んで皮肉も効いてておもしろいので、ファンも多くて、彼の独特の髪型(往年のロック歌手風)からか、「Rich Hickey’s Greatest Hits」というブログ記事もあったりします(プレゼンテーション動画へのリンク集です)。
ただ、彼のプレゼンテーションは難解な英語も出てきて、私のようなリスニング苦手人間には音声だけで聴くのは難しいです。そういう人は国外でも多いからか、有志が書きおこし(transcript)を公開していたりします。ですので、私は彼のプレゼンを、聴いたのではなく、読んだわけです。ただ動画でなにが起きてるのかわからないと文章だけでは意味が分からないところ(特に現場のジョークとか)があるので、動画も目を通すことをおすすめします。
彼の有名なプレゼンテーションに「Simple Made Easy」というものがあります。具体的なコーディングについてのプレゼンというよりは、シンプルさとは何か?を語ったものです。彼のプレゼンテーションには、そういう、実際のコーディングではなく、プログラミングの土台となる考え方や態度についてのものもあって、なかなか面白いのです。彼の哲学を語ったものとも言え、その考え方は、当然Clojureにも反映されているのだと思います。
Simple Made Easyの内容は、一度、株式会社ユーザベースでプレゼンテーションをする機会があったときに資料にまとめたことがあります。今回は、その資料をもとに、どういう内容のプレゼンテーションだったのか紹介したいと思います。ただ、これは「翻訳」ではなくて私の理解した内容の紹介であることに注意してください。Rich Hickeyが具体的にどう言っているのかは、彼のプレゼンを直接見てください。
シンプルと簡単は違う
世の中でシンプルだと言われるものの中には、実際にはシンプルではなく、単に「簡単」であるものがたくさん混ざってます。このツールを使えばコマンド一発でサーバが構築できてすごいシンプルだ!などなど。もちろん簡単なことはそれ自体価値のあることなのだろうけども(少なくともただ難しいよりは)、シンプルと簡単を混同するのは良くない。それらを混同するから、世の中には、(使うのは)簡単だけど猛烈に複雑なものが現れてくるわけです。
シンプルと簡単は違う。
簡単に使えるけども、ものすごく複雑なものというのは、たくさんあります。お気に入りのIDEはどうですか。JSならば、webpackはどうでしょうか? Spring Bootを使えばJavaで簡単にAPIサーバが作れますが、Spring Bootはシンプルでしょうか?
シンプルと簡単との違いを明確にするために、Richは、ここでいう「簡単」とはどういうものかを紐解いていきます。彼が挙げた、人が「簡単」なものに抱いているイメージは、次のようなものです。
- 慣れている
- すぐに使い始められる
- 似たようなものをすでに知ってて、身近だ
- 今の自分の能力の範疇内だ
「簡単」というのは、だいたいが「ほかと比べて」という比較が入るのです。慣れているにせよ、身近であるにせよ、「今の自分の知ってる何かと比べて」という比較なのです。
であるから、みなが「簡単」なものだけを選択していたら、誰も新しいことを始めることはできません。新しいことは、身近でもないし、慣れてもいないし、たいていは今の能力の範疇外だから。
シンプルとは
一方でシンプルというと、ぱっと「たくさんじゃなくてひとつだけ」だとか「そのツールは機能が少なくてシンプルだ」とか「このプログラムは構成要素が少なくてシンプルだ」とか、なんだか(ものなり機能なりの)数が少ない方がシンプルだ、という話になりがちです。ところが、実際には、ものごとをシンプルにすると、要素の数は多くなっていきます。
ClojureはLISP系の言語ですが、たとえば、伝統的なLISPにおける括弧は、シンプルでしょうか。LISPは括弧だけで構成されているから、シンプルなのでしょうか。RichはLISPの括弧はシンプルではないといいます。なぜならLISPの括弧には、複数の意味があるからです。
LISPでは、括弧は「関数呼び出し」と「ものごとをグルーピングする」という二つの意味があります。let式でペアを表現するためのタプルとしての括弧、関数呼び出しとしての括弧、です。
つまりRichのいう「シンプル」というのは、「ひとつのものに、複数のことがらを混ぜ込まない」ということなのです。LISPの括弧には、ふたつの事柄が混ぜ込まれているので、シンプルではないと。
シンプルというのは、まっすぐの糸のようなもので、シンプルでないものというのは、糸が絡まっている状態です。「関数呼び出し」と「グルーピング」という2本のひもが、絡み合って、括弧となっているのが、LISPの括弧なわけです。
だから、「シンプルさ」というのは「オブジェクトが小さい実装である」とか「オブジェクトの詳細がちゃんと隠蔽されている」とかいう話とも関係がありません。これらはコーディング上では大事なプラクティスであるけど、「シンプルかどうか」という話とは、レイヤーの違う話なのです。
「シンプルでなくなる」という言葉を決めよう
何かを具体的につかむためには、名前をつけるのが大事です。「シンプル」のほうには名前がついてるので、その対義語の方に名前がほしい。もちろん、名詞なら「複雑(complication)」といえばいいかもしれないです。でもできれば、シンプルでないものを作ろうとしてるときに「おい、それは○○してる(シンプルじゃなくしてる)ぞ」と言えれば、シンプルさを保つ役に立ちそうです。
そこでRichは「コンプレクト(complect)」という英語(自動詞)を提案しています。何かがコンプレクトしてるから、そいつは複雑になるというわけです。コンプレクトとは、なにか複数のものが、絡み合い、もう分離できないくらいに結びついてしまうことです。
コンプレクトしてるものを探そう
言葉ができると、これはコンプレクトしてるぞ、ということができるようになります。プログラミングの世界には、身近にあってコンプレクトしているものがたくさんあります。RichはSimple Made Easyの中で、たくさんの「コンプレクトしてる」例を挙げています。
対象 | 何がコンプレクトしている? |
---|---|
状態(state) | 状態を変更するすべてのもの |
オブジェクト | 状態と一意性と値 |
メソッド | 関数と状態と名前空間 |
継承 | 複数の型 |
変数 | 状態と時間 |
アクター | 「何を」と「誰が」 |
switch文/match文 | 「何を」と「誰が」とのペアが複数個混ざっている |
私はこのリストを見たときに、なるほど、変数というのは「状態」と「時間」がコンプレクトしてるのか、なるほど!と思いました。言葉を定義すると、いろんなものが、簡単に表せるようになり、理解しやすくなります。
ものごとを絡み合わせない
コンプレクトしているものは、複数の要素が、もはや切り離せないレベルで結合してしまっています。この、切り離せない、という事実が、複雑さの現れるところなのです。
シンプルなものは違います。シンプルなものは、組み合わせることができます。組み合わせることで、コンプレクトしたものと同じこと実現できるでしょう。さらに、簡単に分離できます。だから合体させたり、分離してカスタマイズしたりが簡単にできます。シンプルなものは、絡み合っていないから、「簡単に」変更できるのです。
シンプルさが、簡単さを作るのです。
同じ城を作るにしても…
この図を見てください。これは実際に「Simple Made Easy」で使われた写真です。左側は毛糸細工で、毛糸を編んだり縫ったりして作る城です。右側はLEGOブロックで作った城です。
毛糸細工は、パターンがわかっている場合、織り機を使えば簡単にできて面白いらしいです。馴染みがないなら、プラモデルとかに置き換えて考えるといいかもしれません。
毛糸の城と、LEGOブロックの城とでは、ツールがある前提に立てば、どちらも簡単に作れるものです。でも、一度作った後に変更しようと思ったら別です。これは重要な視点です。次の文言をじっくり考えてみてください。
テストスイートとリファクタリングツールがあれば、毛糸の城をLEGOブロックの城よりも簡単に変更できるんでしょうか?
強力なリファクタリングツールは複雑なものを安全に改変することをサポートしてくれますが、それは、LEGOブロックのような構造のプログラムと比べてどうなんでしょうか?あるいは、LEGOブロックのような構造のプログラムにリファクタリングツールを使った場合と比べてどうなんでしょうか?
テストスイートとリファクタリングツールは、あくまでも「ガードレール」であって、ガードレールなしでも簡単に変更できる、もともとシンプルなものの代用にはなり得ないんです。
「シンプルであること」は、あなたの選択だ
私たちには、複雑性の文化があります。放っておくと、なんでもこんがらがり、からみあい、コンプレクトしていくのです。そんな世界の中では、シンプルというのは、意図的に選ぶ選択なのです。
もし、シンプルなシステムが持ててないなら、シンプルであることを選択しなかった自分のせいになるのです。
テストや型システム、強力なリファクタリングは、安全性を高めてくれるでしょう。しかし、これらは強力なガードレールではあっても、シンプルさを保証してはくれません。シンプルさと、ものごとがコンプレクトしていくことの問題を解決してはくれません。だから、シンプルさというのは、常に自分の選択なんだ、とRichはプレゼンテーションで主張しています。
私たちはすぐにコンプレクトしてしまうので、常に「コンプレクトレーダー」を動かして監視しなくちゃいけない。 「コンプレクト」というワードを定義したこと自体が、そのレーダーとして役に立つはずです。
プログラムをシンプルにするには抽象化しなければいけない
プログラムやシステム全体をシンプルにするためには、ものごとを、コンプレクトしない形で結合したり分離したりできるようにしなければいけません。あっちとこっちの共通項を決めて、LEGOブロックのように、つなげたりはずしたりできなければいけません。そのためには、物事を抽象化しなければいけません。
抽象化というのは、「複雑さを隠す」ことではありません。
ものごとから、実装の詳細を取り除くことです。
そうすることで、ものごとは組み合わせ可能になって、たくさんのものを、共通の方法で扱えるようになります。LEGOブロックになるのです。
RDBは、「集合」という抽象化を使っています。であるから、テーブルやサブクエリも「集合」として共通の扱いができ、同じようにJOINしたりWHEREでフィルタしたりできます。
Clojureは、言語を貫く共通の抽象データ構造を定義していて、マップであれrecordであれ、あるいは独自に定義した何らかの型であれ、Associativeとして扱うことで、さまざまなものを共通の方法で扱えるようにしています。
データに抽象構造を見いだして、すべてをそこに集約していく必要が出てきます。LEGOブロック化するわけです。それはインタフェースを定義することかもしれないし、もっと大きなシステムであれば、サーバー間の関係をどう捉えるか?まで広がるかもしれません。
シンプルは選択だから、シンプルなシステムがほしいのであれば、システム全体を、コンプレクトしていない単位に分割できるか考えていかねばなりません。
そうしてコンプレクトしていない単位になり、抽象構造で接続されたシステムは、おそらくは「簡単」に変更できるでしょう。
まさしく Simple Made Easy なわけです。
とはいえ、現実の世界で「完全にシンプル」というものの実現は難しかったりもします。Clojureだって、グループ化としての括弧にはベクタなりマップなりを使うことで、丸括弧はおおむね「関数呼び出し」と見なしても大丈夫ですが、丸括弧をリストとして使えないわけではありませんし(でないとマクロを書けないので)。
だから現実にはトレードオフが存在するでしょうし、そのトレードオフこそが、選択なわけです。プログラムやシステムをシンプルにするのは、いつも、私たちの選択なんです。
この「Simple Made Easy」というプレゼンテーションで、RichはClojureの話をほとんど出さないので、具体的にどの部分に影響が、とかは語られてません。しかし、Clojureの並列化ライブラリ core.async を使って作ったプログラムが、「関数」と「チャネル」というそれぞれシンプルな構造を、transducerやpipelineを使って組み合わせる形で作られるところにも、この発想が現れているんだと私は思っています。
シンプルにすることはチャレンジでもあるので、うまくいったものやそうでないものもあるでしょうが、この考え方は、Clojureのいろんな部分に息づいているように思います。
今回は、「Simple Made Easy」というRich Hickeyのプレゼンテーションを通して、Clojureにおけるシンプルさの考え方について書いてみました。
次に何書くかは未定です。。
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を前提として書くのがオススメです。そうすることで、関数は他の関数のインプットになりえますし、他の関数の出力を入力できます(各関数の入力と出力が同じ抽象データ構造だから)。
この抽象化は徹底していて、例えば、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)」という言葉あります。
この一文に賛成の人も反対の人もいるでしょうが、Clojureは明確に「ひとつのデータ構造を扱う100個の関数がある」ような世界の方がいい、という前提で作られているのです。
アドホックな多態(ポリモフィズム)
さて、少ない抽象データ構造とたくさんの関数、という理屈はわかったとして、オブジェクトに結びついてない関数における、多態(ポリモフィズム)はどうなるんでしょうか。用途ごとに異なる関数を用意すればいいってことなんでしょうか。
もちろん、実際の開発には多態は必要です。多態なしで、MySQLとPostgreSQLで異なる動きをする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 に送ったパッチでは、列名をクオートする処理をMySQLとPostgreSQLとで分ける必要に対応しました。対応は簡単で、クオートする関数をマルチメソッドに変更し、引数として渡ってくるDB接続定義の接続URLを使って適切なクオート処理にディスパッチするだけです。関数がマルチメソッドに変わったわけですが、関数を呼び出す側から見ると、やはりただの関数に見えますので、他の部分にはまったく影響しません。
つまり、コードを書いている時に、ある関数を多態にするかどうかというのは後から考えても大丈夫な仕組みなわけです。もちろん、作ってる段階で、設計として「ここの関数は外から拡張できるようにしたいから、マルチメソッドにしておこう」とか「ここはユーザー利便性を考えてプロトコルを定義しておくか」ということはありますが、そうでないところを後からマルチメソッドやプロトコル関数化することも、結構簡単に行えるのです。
だからAd-hoc(場当たり的な)ポリモフィズムと呼ぶわけです。もちろん注意すべきことはあって、対象の処理が関数に分離されてなければ、後で多態関数化することもできませんから、できれば関数は細かく分けておいた方が、後から対処しやすいと思います。そのような注意点さえクリアしておけば、柔軟に後からコード変更可能だ、というのも、Clojureの利点の一つです。
他の文化を無理やり持ち込まないの大事
プログラミング言語には、言語ごとに文化というか、大事にしている考え方があるものです。ClojureにはClojureの大事にしているものがあって、言語仕様自体が、その大事にしているものを前提に設計されているはずです。上に紹介したものも、Clojureという言語に意図的に組み込まれた仕様な訳です。
他の言語から新しい言語に移ってくると、最初は、今まで馴染んでいた言語の文化と、新しい言語のやり方とが、まったく異なっていることに混乱して、自分の馴染んでいる文化と同じにしようとしてしまいがちです。しかし、言語仕様とその言語の文化は強くつながってるものなので、文化を無視して別の文化で書こうとしても苦しいだけです。
そのためには、やはり、その言語が大事にしているものはなんなのか、どういう意図で、どういうことを実現したくてそういう言語デザインになっているのか、を知るのが大事だと思うのです。意図がわかれば、馴染めなかったものにも急に「なるほど、そういうことか」と納得感が得られるものです。抽象データ構造や多態の仕組みなんかも、そのような例の一つです。
ClojureはJavaみたいな型ベースのオブジェクト指向言語とは、かなり違う言語なのですが、一見「これってJavaのインターフェースと同じか」とか「これってLombokで@Valueでクラス定義するようなもの?」とか思えてしまう機能もあったりします。しかしもちろん、それらはイコールではないし、意図してることも異なってることもあります。
他の言語から移ってきた時には、「なんでそういう仕組みになっているのか?その背景はなんなのか?」を把握するのがとても大事だと思います。意図さえわかってしまえば、とても簡潔で書きやすい言語ですから。
抽象データ構造やAd-hoc多態以外にも、Clojureには、シンプルで構造化しやすい言語を作るために、いろんなアイデアが取り込まれていて面白いので、今後も、理解できた範囲で書いていきたいところです。
Clojureのいろんな並行処理の使い分け
この記事はもともとTumblrに書いていた自分のブログ記事を転載したものです。投稿日時も当時の投稿日時を再現してあります。
Clojureには標準でもagent系のsend, send-offに加え、future関数というスレッド起動系関数があります。
core.asyncの登場で、ここにgoマクロとthreadマクロが加わりました。
これらはすべて、背後ではJavaのExecutorsを使ってスレッドプールを作り、一度生成したスレッドの再利用を行いますが、それぞれ使っているスレッドプールが異なります。さらに関数自体の機能も異なるため、どれをつかったらいいのか迷ってしまうことがあります。
自分用に整理したので、メモとしておいておきます。
IOバウンドとCPUバウンド
まず、Clojureのスレッド関連関数の用途は、大きく2種類にわけられます。それが、IOバウンドとCPUバウンドです。
IOバウンドな処理は、実行中の処理がCPUよりもIO処理に強く依存します。DBアクセスとかリモート通信とかですね。別スレッドでこの処理を実行した場合、スレッドは大部分を、IO処理待ち状態で過ごします。
CPUバウンドな処理は、途中にIO待ちのような「待機」が発生せず、CPUをぶん回し続けるような処理です。全データがメモリに載っていて、CPUがフル稼働でそれらを処理するようなケースです。
IOバウンドな処理は大半をIO待ちで過ごすため、CPUを占有しません。一方CPUバウンドな処理は、その名の通り、動いている間中、CPUを使い続けます。
CPUを使い続けるような処理は、CPU(コア)数以上のスレッドを起動してもあまり意味がありません。たくさんのCPU依存処理を起動する場合、全スレッドがタスク処理でCPUを占有しているのがもっとも効率の良い状態で、それ以上起動しても、単にスレッド切り替えコストが無駄になるだけだからです。 CPUバウンドな処理は、スレッド数をコア数に近い数にとどめ、ひとつひとつのタスクは小さくして、たくさんのタスクをどんどんコアで分散して処理していくのが効率がよいことになります。CPUバウンドな処理はCPUを使うしかないのだから効率良く使いたいわけです。
一方、IOバウンドな処理は、その大半は「IO待ち」だったりします。リモートAPIを呼ぶ処理は、大半を「レスポンスが返ってくるのを待つ」ことに費やしています。
ここでたくさんのIOバウンドな処理を、コア数分の固定数スレッドで実行したことを想像してください。スレッドが4つだとして、4つのIO処理を起動すると…すべてのスレッドが使われ、それ以降の処理は待つしかありません。
CPUバウンドな処理であれば、スレッドはCPUをフルに使って一所懸命にタスクを実行していることでしょう。だから待つしかありません。しかしIOバウンドな処理では、4つのスレッドは、おそらく、ただIO待ちをしているだけです。
だから、IOバウンドな処理でスレッド数をコア数近くに限定するのは、あまり意味がないということになります。スレッドがIO待ちをしている間に、ほかの処理が動けるかもしれないのですから。だから、コア数以上のスレッドを起動して、どんどんIO待ちさせ、IOが終わったスレッドから処理を行えばよいのです。
固定数スレッドプールとキャッシュ化スレッドプール
Clojureの関数は、その用途がCPUバウンドかIOバウンドかによって、使用するスレッドプールが異なっています。
agent実行関数sendが使うスレッドプールは固定数であり、JavaのExecutors.newFixedThreadPoolメソッドで作られます。
一方、send-offが使うスレッドプールはキャッシュ化された非制限プールで、Executors.newCachedThreadPoolで作られます。非制限といっても、キャッシュ化スレッドプールは、使われなくなったスレッドを60秒で破棄するので、たくさんのスレッドがゴミとして残ることはありません。
多くのClojure関係の本で、sendはCPUに依存する処理に、send-offはIOに依存する処理に使う、と書かれているのは、このように、背後で使っているスレッドプールがことなるからです。
背後で使われているスレッドプールの種類がわかれば、その関数が、CPUバウンドな処理を想定しているのか、IOバウンドな処理を想定しているのかがわかります。以下は、Clojureのマルチスレッド関数がどのスレッドプールを使っているのかの一覧です。
poolの定義場所 | プールの種類 | スレッドプール生成方法 | スレッド数 | |
---|---|---|---|---|
send | clojure.lang.Agent/pooledExecutor | 固定数 | Executors.newFixedThreadPool | 2+コア数 |
send-off | clojure.lang.Agent/soloExecutor | キャッシュ化 | Executors.newCachedThreadPool | 制限なし |
future / future-call / pmap / pcalls | clojure.lang.Agent/soloExecutor | キャッシュ | Executors.newCachedThreadPool | 制限なし |
go | clojure.core.async.impl.exec.threadpool/the-executor | 固定数 | Executors.newFixedThreadPool | コア数 * 2 + 42 |
thread / thread-call | clojure.core.async/thread-macro-executor | キャッシュ | Executors/newCachedThreadPool | 制限なし |
reducers | clojure.core.reducers/pool | ForkJoinPool | new java.util.concurrent.ForkJoinPool | 自動制御 |
futureのところにはpmapとpcallsも書いていますが、pcallsはpmapを、pmapはfutureを呼び出すので、すべてfutureと同じ扱いです。
まとめてみると、core.asyncの解説で必ず取り上げられるgoマクロは、固定数のスレッドプールを使っていることがわかります。つまり、goマクロはCPUバウンドな処理を前提としているわけです。
goマクロが「コア数 * 2 + 42」というよくわからないスレッド数を使っていることについて、特に42という謎の数値を指定していることについてははっきりしないのですが、+42は後から付け加えられたらしく、メーリングリストのポストなどを追跡すると、前述した、IOバウンドな処理に固定数スレッドプールを使った場合のような、IO待ちで全スレッドが停止して並行処理がスタックしてしまうことをある程度抑止したい、というのが意図のようです。goマクロはあくまでCPUバウンドな処理を扱うものであることは変わらないそうです。
42という数値については「すべての答え」から取ったのでは、という説もありますが、いまだ謎です。「すべての答え」ネタを知らない人はググってください。
goがCPUバウンドであるかわりにthreadマクロが用意されています。
threadマクロはgoマクロとほぼ同じ使い勝手で使えますが、キャッシュ化スレッドプールを使うため、IOバウンドな処理に向いています。goマクロと異なるところは一点だけで、チャネルの操作に <! と >! は使えず、ブロック型のチャネル操作関数 <!! と >!! を使う、ということです。<!!, >!! では呼び出した段階でスレッドがブロックしますが、そもそもthreadを使った場合はネイティブスレッドに処理が割り当てられていて、そのスレッドがブロックするだけなので、メインスレッドは止まらず、問題ありません。
goマクロで起動した並行処理は、単純にひとつのスレッドに丸ごと渡されるわけではなく、コンパイル段階で全処理が式単位に分解され、ステートマシンに変換されます。S式ならではです。そして<!, >!でチャネルへのアクセスごとにスレッドが切り替わる、といった動きをするようです。<!!, >!! をgoブロックで使うと、このスレッド切り替えがうまく動かなくなるので、>! か !< を使います。
彼らはこれをIoC Threadと読んでいますが、いやいやそれはIoCというよりも、昔の協調型マルチタスクと似たものだから「協調型スレッド」と呼ぶべきだという意見もあります。私も強調型だって意見に賛成ですが、たぶんIoCのほうがかっこいいってことなんだと思います)
reducersだけは特殊で、reducersは内部では並行処理をJava 7以降のFork/Join APIに処理を丸投げしています(JVMがJava7未満の場合は互換ライブラリを使っているようです)。Fork/JoinはJavaではとても使いにくいAPIで、Java 8でラムダ式とパラレルストリームが導入されてやっと本気出せるようになったのですが、ClojureではJava 8よりももっと前に、早々に対応していたわけです。よって性質としてはFork/Joinと同等でして、Fork/Joinのドキュメントによると、CPUバウンドな処理を前提にスレッド数を自動制御し、IOバウンドな処理が混ざるとうまく自動制御できないようです。
スレッドプールも、Java 7でFork/Joinとともに導入された、ForkJoinPoolを使っています。このプールは、初期値はCPUコアと同数のスレッドを用意し、ダイナミックにワーカースレッドを追加したり停止したりします。
つまり、reducersはFork/Joinにすべておまかせ、ということです。
そもそもFork/Joinは、要素数がとても多いデータ(10万とか100万とか)を高速並列処理するためのAPIなので、並列化が目的なら、数個程度の並列化ではreducersではなく別の機構をつかったほうがいいです。reducersの機能は並列化だけではないので、そっち目当てならよいですが。
プールの違い
表をよく見るとわかりますが、同じスレッド化プールを使っている関数でも、threadマクロだけは、プールが異なります。send-offとfutureは、ともにClojure標準関数なだけはあって、両方が同じスレッドプールを使っています。これはつまり、send-offで生成されたスレッドは、futureでも再利用できることを意味します。
core.async/thread は、そもそもcore.async自体がClojureの「外部ライブラリ」な位置づけですから、独自に定義したスレッドプールを使っています。よって、futureとthreadとは、互いに生成済みスレッドを再利用できません。ちょっとした差ではありますが、効率的ではないことは知っておいて損はないでしょう。
core.asyncを使う人は、おおむね、スレッド処理はcore.asyncばかり使う傾向があるので、今後はfutureの代わりにthreadを使うことにすれば落着、と行きそうですが、両者は機能にも違いがあるのでなかなかそうは行きません。
機能の違い
- send, send-off (agent系)
- future
- go, thread (core.async系)
この3種類は用途および使い方が違います。
sendとsend-off、goとthreadは、用途は同じですがCPUバウンドかIOバウンドかが異なります。
futureはどちらにも属しません。
sendとsend-offはどちらも、agent操作関数であり、目的はあくまでagentの実行と更新です。そもそもagentは、汎用的な並行処理起動のためにあるものではなく、かなり特殊な用途でつかうものなので、「ただスレッドを起動したい」だけでは使わないほうがいいです。
agentの特徴は、同じagentで起動した処理は「逐次実行される」点です。同じagentに何回もsend, send-offしても、それらが平行で処理されるわけではありません。sendやsend-offはagentのアクション実行キューにアクションを積むだけです(もちろん、複数のagentが存在すれば、それらは平行に動きます)。そもそもagentは「値」を持っていて、sendやsend-offで積んだアクションによって、agentの結果値が順番に変わっていく、というものだからです。
goとthreadはagentに比べてより汎用的な並行処理機構で、goやthreadブロックの処理は、スレッドプールの違いはあれ、すぐにスレッドに割り当てられて平行に動きます。いずれのマクロも、処理完了時の結果値が取り出せるチャネルを返します。とこれだけ書くと、threadはfutureと似ているように思えます。ともにIOバウンドな処理用で、結果値を取得できるオブジェクトを返します。futureを卒業して、core.asyncに「移行」すべきでしょうか?
futureは、処理をキャッシュ化スレッドプールに渡してくれる点でthreadと同じですが、futureはdelayオブジェクトでもある点が大きく異なります。
(let [result1 (future (my-remote-func1 ...)) result2 (future (my-remote-func2 ...))] (my-long-processing-fn) {:age (-> (:base @result1) (+ 20)) :address (str (:address @result1) " " (:address @result2)) :name (:name @result2)})
futureはderef(の省略記号アットマーク)によって非同期処理の実行結果を取得できますが、derefは何回でも使えます。最初のderef時にまだ処理が終わってない場合は処理完了を待機しますが、以降は、キャッシュした結果値を返し続けます。
上記例では、my-remote-func1とmy-remote-func2というリモート呼び出しを平行化するためにfutureを使い、さらにmy-long-processing-fnという長い処理を行う関数を呼びました。my-long-processing-fn実行中も、別スレッドでリモートコールは実行されています。
最後にマップを作るときに、futureの結果値を参照していますが、result1もresult2も、2回参照している点に注目してください。
threadマクロはdelayオブジェクトではなく、チャネルを返します。(<!! ch) によって結果値を取り出せますが、derefと違って、<!!を繰り返し読んでも同じ結果が返ってくるわけではありません。チャネルはキューの一種で、チャネルへの <!! は呼ぶたびに新しい値を返し、値がなくなるとnilを返すので、チャネルを、delayのように繰り返し参照すべきではありません。
(let [ch1 (thread (my-remote-func1 ...)) ch2 (thread (my-remote-func2 ...))] (my-long-processing-fn) (let [result1 (<!! ch1) result2 (<!! ch2)] {:age (-> (:base result1) (+ 20)) :address (str (:address result1) " " (:address result2)) :name (:name result2)}))
チャネルベースのthreadを、futureの代用として使う場合は、letを使ってチャネルからいったん値を取り出さなければいけない点で、使い勝手が異なってきます。
もちろん、ごく僅かな差ですし、go/threadには、複数のgo/threadブロックが共通の(しかもたくさんの)チャネルを介して値をやり取りしつつ並行処理を実行するという本来の目的がありますから、価値はいささかも減じません。ここで言いたいのは、threadはgoのIOバウンド版であって、全並行処理をcore.async化しようとして、futureのかわりにthreadを使おうというのは、アリではありますが、若干短絡的です。
core.asyncのパワーは、goあるいはthreadブロックが複数個起動していて、互いに(チャネルを介して)通信しあう時に発揮されます。もちろん、常に結果チャネルを返す点で汎用的なスレッド起動の仕組みとして使うことも配慮されていますが、上記のような違いを意識しておいたほうがよいでしょう。この例のように、複数のfutureでいくつもの並列処理が起動して、あとでその結果値を使う場合、threadの場合は、長いlet式でいったんチャネルをリードする必要があるかもしれません。
一方で、futureとthreadは使用するスレッドプールが異なるので、併用すると、互いにスレッドを共有してくれません。future同士はスレッドを共有しますし、thread同士も共有しますが、futureとthreadは共有しません。ここに若干のロスが存在します。
よって、用途に合わせてfutureとthreadと使い分けるか、あるいはスレッドプールの効率性を考えて片方に寄せるか(パワーを考えるとthreadの方が強力なので、ふつうはthreadに寄せるでしょう)は、正直、好みの次第です。実を言うと、私はfutureを使うシーンでもthreadを使うことがほとんどです。好みの問題です。
まとめ
- agentは単なる並列処理起動用の機能ではないので、ちょっと考えて使え
- reducersはすごい量のデータを処理でもしない限り、並列化機構だと思うな。
- いまやりたい処理がCPUバウンドかIOバウンドかはちゃんと考えろ
- futureにはちゃんとfutureに向いた処理がある。けどあえてthreadで代用も出来る。その場合、他の並列処理もなるべくcore.asyncを使うようにすれば、スレッドプールのキャッシュ効率は若干良い。
Mac OS X 10.10 (Yosemite)にバージョンアップするとPostgreSQLが動かなくなった場合の対処方法
この記事はもともとTumblrに書いていた自分のブログ記事を転載したものです。投稿日時も当時の投稿日時を再現してあります。
Yosemiteにバージョンアップして大して問題もなく過ごしてたんですが、ひさびさにPostgreSQLを使おうとしたら、なぜか繋がらない。
could not open directory "pg_tblspc": No such file or directory.
とか言われてしまう。
いろいろ調べたところ、どうも、Yosemiteにアップデートする過程で、なんでかしらないが空のディレクトリが削除されるのではないか?というポストを見つけました。
http://stackoverflow.com/questions/25970132/pg-tblspc-missing-after-installation-of-os-x-yosemite
結局、次のようにディレクトリを作成すれば、問題なく起動しました。
mkdir /usr/local/var/postgres/pg_tblspc mkdir /usr/local/var/postgres/pg_twophase mkdir /usr/local/var/postgres/pg_stat_tmp
同じ問題にハマった人向けにメモしておきます。