紙箱

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

Clojure + core.async による非同期&並列プロセスの世界

core.asyncによる非同期プログラミング

core.asyncClojure用の、事実上標準の非同期プログラミングのライブラリです。

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で並列処理サーバプログラムを作るときの基礎がちゃんと入っています。

f:id:t_yano:20171029031328j:plain

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))

イメージとしてはこんな感じです(雑な手書き図ですみません)

f:id:t_yano:20171029031238j:plain

ここで、「もともとは新規と削除しかないと聞いてた」→「急に、『実は更新データもありました…』とか言われてしまった」というケースを考えてみます。

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まで含めると、このシステムの全体像はこんな感じになります。

f:id:t_yano:20171029031435j:plain

この柔軟性が、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を使った時の「あるある」なトラブルと回避方法について、ちょっとしたプレゼンテーションをすることになりました。きてね。

connpass.com

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は、ここでいう「簡単」とはどういうものかを紐解いていきます。彼が挙げた、人が「簡単」なものに抱いているイメージは、次のようなものです。

  • 慣れている
  • すぐに使い始められる
  • 似たようなものをすでに知ってて、身近だ
  • 今の自分の能力の範疇内だ

「簡単」というのは、だいたいが「ほかと比べて」という比較が入るのです。慣れているにせよ、身近であるにせよ、「今の自分の知ってる何かと比べて」という比較なのです。

であるから、みなが「簡単」なものだけを選択していたら、誰も新しいことを始めることはできません。新しいことは、身近でもないし、慣れてもいないし、たいていは今の能力の範疇外だから。

シンプルとは

一方でシンプルというと、ぱっと「たくさんじゃなくてひとつだけ」だとか「そのツールは機能が少なくてシンプルだ」とか「このプログラムは構成要素が少なくてシンプルだ」とか、なんだか(ものなり機能なりの)数が少ない方がシンプルだ、という話になりがちです。ところが、実際には、ものごとをシンプルにすると、要素の数は多くなっていきます。

ClojureLISP系の言語ですが、たとえば、伝統的な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ブロックで作った城です。

f:id:t_yano:20170515114854p:plain

毛糸細工は、パターンがわかっている場合、織り機を使えば簡単にできて面白いらしいです。馴染みがないなら、プラモデルとかに置き換えて考えるといいかもしれません。

毛糸の城と、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を前提として書くのがオススメです。そうすることで、関数は他の関数のインプットになりえますし、他の関数の出力を入力できます(各関数の入力と出力が同じ抽象データ構造だから)。

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には、シンプルで構造化しやすい言語を作るために、いろんなアイデアが取り込まれていて面白いので、今後も、理解できた範囲で書いていきたいところです。

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の定義場所プールの種類スレッドプール生成方法スレッド数
sendclojure.lang.Agent/pooledExecutor固定数Executors.newFixedThreadPool2+コア数
send-offclojure.lang.Agent/soloExecutorキャッシュ化Executors.newCachedThreadPool制限なし
future / future-call / pmap / pcallsclojure.lang.Agent/soloExecutorキャッシュExecutors.newCachedThreadPool制限なし
goclojure.core.async.impl.exec.threadpool/the-executor固定数Executors.newFixedThreadPoolコア数 * 2 + 42
thread / thread-callclojure.core.async/thread-macro-executorキャッシュExecutors/newCachedThreadPool制限なし
reducersclojure.core.reducers/poolForkJoinPoolnew 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ブロックで使うと、このスレッド切り替えがうまく動かなくなるので、&gt! か !< を使います。
彼らはこれを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

同じ問題にハマった人向けにメモしておきます。

Atlassian BambooでCI環境を構築する

この記事はもともとTumblrに書いていた自分のブログ記事を転載したものです。投稿日時も当時の投稿日時を再現してあります。

やらないといけないと思いながらも、ずぼらでずっと後回しにしてた、CIサーバ構築作業を最近やりました。というのも、JIRAを契約するついでにお試しに登録していた、Atlassian Bambooのお試し期間が過ぎてしまったから。お試しすらしていない…

とりあえず解約するにしても試してからにしようってことで本腰入れて使ってみることにしました。でも、ネットで検索してもCircleCIいいよってのが多く、Bambooは「どう設定していいのかぱっとみわからなかった」みたいなのが多い印象。

たしかに、ぱっと見どう使っていいのかなんか分かりにくい…

でも、分かってしまえば仕組みは簡単でした。

Bambooって結局なんなのか

Amazon EC2を実行サーバとして利用する、CI環境です。Bamboo上にソースリポジトリと「Builder」や「Tester」の設定をしておくと、ソースをリポジトリにpushすると、Bambooが指定したEC2インスタンスを自動起動して、ソースチェックアウトからビルド&テストしてくれます。EC2インスタンスは自動停止します(これについては後述)

ポイントは

  • AmazonのAMIを指定するとそのサーバを起動して使うので、ビルド&テスト環境を自由に構築できる(自分でEC2上に環境構築して、そのインスタンスからイメージをつくればいいので)。自由度は高い
  • Bambooのコストとは別にEC2のコストがかかる
  • ビルトインで提供されているBuilderとかを使える状況であれば、出来合いのAmazonインスタンスを使えば即使える

って感じですか。 私はClojure+Leiningen+Midjeという環境が必要なので、サーバ設定を自前でできるのはありがたい。使ってみようという気が湧きました。

ただ、ドキュメントが猛烈に分かりにくいように思います。何ヶ所かつまって、検索すると、Atlassian自身のフォーラムに同じ質問が(英語で)上がってて解決したり、みたいなことがけっこうありました。

ワナ

私が一日無駄にしたワナが、Bamboo OnDemand(ダウンロード版)では、Atlassian提供のAmazon EC2イメージを使ってサーバ構築しないと、Bambooが接続するためのエージェントが起動しないってのがよくわかってなくて、それが分からなかったのが

tokyoリージョンにはイメージが提供されてない

ってことがわからなかったからです。なんとかビルドできる環境作っても、エージェント起動開始でずっとPending…ってなって困ってたのですが、そもそもエージェント・プログラムがサーバに入ってないのだからあたりまえです。

でもねえ、「Elastic Bamboo Global Settings」ってところに、いきなりリージョン選べってのがあるんですよ。そりゃTokyo選びませんか?

Global Settings画面 global settings

Bamboo OnDemandを使う場合は、ここは(ちゃんと動く環境ができるまでは)US East (Northern Virginia)固定でOK。デフォルトがたぶんそうなってるので、かえなきゃいいわけです。

すべて動くようになってから、EC2のイメージを(EC2の機能で)Tokyoリージョンにコピーして、Bamboo側もTokyoリージョンに変更、ということは可能ですが、Bambooインスタンスに接続するのはBamboo自身であって、自分でも顧客でもないのだから、インスタンスがTokyoで起動する意味もないわけで、ずっとUS Eastでもいいはず。

US Eastであれば、Bambooの「Image Configurations」画面に、すぐに使用可能なイメージがずらっと表示されます。Tokyoに変えていると、ここが空っぽです(空っぽなので、ほんとはここに既成イメージが表示されるなんてことが想像できない)

image configurations

Elastic Bambooの設定画面でAWS access key idを設定済みであれば、Startボタンを押すと、EC2でインスタンスが起動します。あとはEC2インスタンスにSSHでログインして、ビルド環境を構築した後、EC2側でインスタンスからイメージを作成し、そのイメージを、Image Configurationsに追加してやれば、独自のビルドサーバが作成できます。

ただ、Java+Maven+JUnitとかRubyとかPHPとかは、実はビルドやテストを実行できるだけの設定がすでに既成イメージにされているので、上記から適当にひとつ選んでもいいかもしれません。ただLargeイメージを使う意味がない場合は、既成イメージのAMI idを使って、インスタンスタイプだけをMicroに変更したような定義を一個作った方が、財布に優しいと思います。

ともかく、US Eastリージョンで既成イメージをベースに環境構築する(あるいはそのまま使う)ってことだけ忘れなければ、そんなにはまることなくさっさと動かし始められると思います。ただEC2につないでるだけですから…

EC2インスタンスの自動停止

Bambooの設定に、リモートサーバのエージェントが何分以上アイドルになったらインスタンスを止めるかって設定があって、デフォルトが10分なんですが、10分経っても落ちません。実は、サーバの自動停止にはもうひとつ条件があって、なぜかドキュメントに書いてなくて、AtlassianフォーラムのQ&Aで見つけたのですが…

  • EC2の支払いが最小になるように、EC2の最小課金時間(通常は1時間?)の間は起動し続ける

ということのようです。

EC2のインスタンスは、起動サーバ単位で1時間いくら、なんですが、この1時間というのは「1時間未満はすべて1時間とみなす」という条件がついてます。よって、1分で停止しても1時間分取られます。

Bambooは、ソースがプッシュされるたびに毎度サーバを起動&停止するのではなく、1時間以内であれば、同じインスタンスを使い回そうとします。アイドル時間が10分たっても、1時間以内にまた別のビルド要求が来るかもしれないので、ギリギリまでは起動し続けます。そのまま要求がこないと、最小課金時間が過ぎるちょっと前に(私が見てた限りでは55分くらいで)自動停止します。 なかなかがんばってるなーと思いました。

Capabilityの設定

Bambooに設定できる各種コマンドは「Capability」と呼んで、インスタンス設定画面の既成インスタンス一覧にある「View Capabilities」を押すと、そのイメージにあるコマンド等を設定できます。

Ant, Grails, Maven, Node.js, PHPUnit, JDK, Mercurial, Gitは始めから入っています。あと、設定画面には出ていないけど、サーバにログインしてみると、/optの下に既にJDK8やMaven 3.2が入ってたりもするので、それらは、サーバをいじらずに、Capabilityに定義を追加するだけで、使い始められます。

私はClojureソースをビルドするためにLeiningenをサーバに入れたので、次のように定義を足しました。

leiningen

このようにCapabilityを足すと、この後のタスク設定なんかで、「Leiningen」を選択できるわけです。

ステージとジョブ、タスク

ビルドプランの設定はまあ直感的だと思います。 ステージ・ジョブ・タスクの区別が分かりにくいかなーと思いました。

  • ひとつのプランは複数のステージを持てる
  • ステージは順番に実行される
  • ひとつのステージは複数のジョブを持てる
  • ジョブは 並列に 実行される
  • ジョブは複数のタスクを持てる。
  • タスクは順番に実行される

という構造で、ジョブが並列に実行されるってことだけ分かってればいいかと。

以下の画像では、

  • 「Build Project」という名前のステージが
  • 「Build Source」というひとつのジョブを持っていて、そのジョブが
  • 「Source Code Checkout」「Maven 3.x」「Command」という3つのタスクを持っている

という構造です。

plan

タスクは、Bambooにもともといくつか用意されていて(「Source Code Checkout」とか)、それらを並べるだけでいいです。サーバイメージに自分でインストールしたコマンドについては、「Command」タスクを使って、Executableを指定できます。

こんな感じです。先にCapabilityを設定しているので、コンボボックスでLeiningenを選択できるようになっています。

task settings

上記イメージのように、JVM系コマンドの場合、JAVA_HOMEとPATHをせっていしなくちゃいけません。Bambooエージェント自体はJava 6で動いている(サーバ設定いじると変えられますが、エージェントが起動しなくなります)ので、タスクの設定で、使用したいJVMを指定してください。 上記画像ではJava 8を設定しています。ちなみに、Java 8は既成イメージの始めから入ってました(なぜかCapabilityには入ってないので、自分で足しましたが)

ビルドしたいソースが、チェックアウトしたソースのサブディレクトリの場合は、「Working sub directory」にリポジトリトップからの相対パスを書きます。私の場合、javaソースは「java」というサブディレクトリに、Clojureソースは「clojure」というサブディレクトリに入れているので、「clojure」を指定しています。

言葉で書くと長ったらしいですが、設定自体はさくさく終わります。

あとはビルドプランを一発手動実行して、動くようならOK。リポジトリにソースがプッシュされたら、勝手にビルドが走ります。

Bambooについて日本語で書いてるブログがほとんど見つからなかったので、書いてみました。

シェルスクリプトでtmuxのウインドウをまとめて開く

この記事はもともとTumblrに書いていた自分のブログ記事を転載したものです。投稿日時も当時の投稿日時を再現してあります。

開発中、常に5つくらいのウインドウをtmuxで開いて作業しています。開くウインドウは毎回同じなのですが、毎回毎回5つウインドウを作って、リネームして、というのが面倒です。

たぶんコマンドから制御できるんだろうと思い調べたらやっぱりあったので、メモとして書いておきます。

DIR="/path/to/your/development/dir"
tmux new-session -d  -s myproj -n project1 -c "$DIR/project1"
tmux new-window -n project2 -c "$DIR/project2"
tmux new-window -n project3 -c "$DIR/project3"
tmux new-window -n project4 -c "$DIR/project4"
tmux new-window -n project5 -c "$DIR/project5"
tmux attach -t myproj

これで、tmux.shを起動したら5つのウインドウがセッティングされた状態でtmuxが開きます。