Clojureにおける同一性について

はじめに

Clojureの勉強のため、テスト駆動開発Clojureで写経しはじめました。
第1部は、他国通貨を扱った問題をTDDで順番に解決していくさまが紹介されます。

使用されている言語はjavaであり、オブジェクト指向でTDDが行われます。
一方Clojure関数型言語の仲間であり、オブジェクト指向でいう継承・メソッドといったものがないため、 実現しかたに考えることがあります。

テストをしていくにあたってClojureの面白いなおもったことがあったので残しておきます。

Javaの場合

まずjavaのテストコードの場合は以下の通り。
ドルクラスを作って、そこから5ドルのインスタンスを生成し、timesメソッドを使って倍化できることを確認するテストである。

public class MoneyTest {
    @Test
    public void testMultiplication() {
        Dollar five = new Dollar(5);
        five.times(2);
        assertEqual(10, five.amount);
        five.times(3);
        assertEqual(15, five.amount);
    }
}

注目すべきは、assertionのとき10や15という数値とfiveの属性であるamount(数値)を比較しており、 オブジェクト同士を比較しているわけではないということである。

Clojureの場合

もちろんClojureの場合も同じようにかける

(deftest multiple-test
  (testing "5ドルを2,3倍すれば10,15ドルになる"
    (let [five (d/->Dollar 5)]
      (is (= 10 (:amount (d/times five 2))))
      (is (= 15 (:amount (d/times five 3)))))))

しかし、数値比較と同じようにオブジェクト同士を比較してもClojureではテストが通る。

(deftest multiple-test
  (testing "5ドルを2,3倍すれば10,15ドルになる"
    (let [five (d/->Dollar 5)]
      (is (= (d/->Dollar 10) (d/times five 2)))
      (is (= (d/->Dollar 15) (d/times five 3))))))

javajavascriptの場合、同じ属性で同一値を持っているオブジェクトであっても 参照値が異なるため単純な比較はできず、deepEqualなどが必要になる。

それに対してClojureはオブジェクト同士を比較できる。 この理由は、Clojureはデータをデフォルトでイミュータブルで扱うことだろうと考えられる。

ある数値に別の数値を掛けたとしても、副作用で互いの数値が別の値に変わることがないように、 それがオブジェクトであってもClojureでは変わらない。だから、=で比較できる。

ではClojureでミュータブルなデータを使用した場合、どうなるのだろうか?

(def zero (atom 0))
(def Zero (atom 0))

(= zero Zero)
;=> false

@ をつけずに比較すると、一致しないことがわかる。

(def five (atom d/->Dollar 5))
(def ten (atom d/->Dollar 10))

(= (d/times @five 2) @ten)
;=> true

@ を使えば比較できる。 結局atom@ で逆参照したとき取得できた値はイミュータブルだからであろう。

デフォルトイミュータブルな世界が為せる面白い挙動なんだろうと感じた。

ClojureDocs=の記述を読めば、 そのとおりだとわかる。

Equality. Returns true if x equals y, false if not. Same as Java x.equals(y) except it also works for nil, and compares numbers and collections in a type-independent manner. Clojure's immutable data structures define equals() (and thus =) as a value, not an identity, comparison.

イミュータブルなデータ構造は、参照比較ではなく値比較してるというわけだ。 なるほど。

テスト駆動開発

テスト駆動開発

CLOJURE for the BRAVE and TRUEでClojure学習 part8

はじめに

Clojure の学習備忘録。
今回はChapter 9: Concurrent and Parallel ProgrammingClojureの並行プログラミングについて学ぶ。並行プログラミングする際利用するスレッドをどうやってClojureで安全に管理するのかというもの。 シングルプロセッサのjs/ts利用者としては、新しい世界が見える感じでなかなか魅力的。

目次

Concurrency and Parallelism Concepts

並行・並列プログラミングは、ハードウェアから、オペレーティングシステム、ライブラリ、自分が書くコードまで あらゆるレベルで厄介なことが多い。この厄介さに立ち向かうまえに、まず並行・並列に関連する概念的な説明を紹介しよう。

Managing Multiple Tasks vs. Executing Tasks Simultaneously

Cucurrency(並行性) とは、同時に一つ以上のタスクを 管理 することを意味する。 タスク というのは、完了待ちとなってるものを指し、ハードウェア・ソフトウェアのいずれかと問わない。 並行性について理解するために、レディ・ガガの歌「Telephone」を参考にして説明しよう。

I cannot text you with a drink in my hand, eh
お酒を飲んでるのでメッセージがうてない。

この歌詞は、ひとつのタスク(お酒をのむ)しかできないので、それ以上のタスクはまったく行えないというわけだ。 しかし、もし彼女がタスクを並行的に処理できるというのであれば、次のように歌うだろう。

I will put down this drink to text you, then put my phone away and continue drinking, eh
あなたにメッセージを書いたら、飲むわ。

このように歌った場合、彼女はお酒を飲む・メッセージを打つという2つのタスクを管理していることになる。しかし、彼女は両方タスクを同時にこなしているわけではない。 同時にではなく、2つのタスクを不連続にスイッチングさせている。スイッチングするときにタスクが完了している必要はない。 タイプしては携帯をおいてコップを手に取り一口飲む。そして、携帯に手に取りまたタイプするというわけだ。

一方 /Parallelism(並列性)/ とは、同時に一つ以上のタスクを 実行 することを意味する。 もしレディ・ガガが、2つのタスクを並列実行するならつぎのように歌う。

I can text you with one hand while I use the other to drink, eh
片手で飲みながら、もう片方の手でタイプできるわ。

並列性は、並行性のサブクラスである。というのも、同時に複数のタスクを 実行 するには、まず複数のタスクを 管理 しないといけないからである。

Clojureには並列実行が簡単にできるツールが用意されている。 レディ・ガガの例でいうと両手で複数のタスクを並列実行したわけだが、コンピュータの世界では複数のプロセッサが並列実行を可能にする。

並列と分散のちがいを理解しておくことは重要である。分散コンピューティングは、並列コンピューティングの特殊な状況であり、 プロセッサが異なるコンピュータに分散されて、タスクがネットワーク越しに各コンピュータに分配される仕組みである。 レディ・ガガの例をつかえば、お酒飲んでるから彼にメッセージを打っておいてとビヨンセに頼むこと同じことである。

Blocking and Asynchronous Tasks

並行プログラミングでもっともよく使われる処理は、 ブロッキング 処理である。 ブロッキングというのは、処理が完了するまで待つということである。ファイルの読み込み、HTTPリクエスト完了待ちといったI/O処理で聞いたことがあるだろう。 これをレディ・ガガの例をつかって説明しよう。

彼女がテキストをおくり、お酒を飲まず携帯を手にして返信があるまでスクリーンをみながら突っ立ているならば、 ブロッキング処理となり、これらのタスクは同期的に処理されていると呼ぶ。

もし携帯を離して飲み物を飲みビープ音やバイブなどの通知があったときに返信内容を確認するのであれば、 非ブロッキング処理であり、非同期に処理していると呼ぶ。

Concurrent Programming and Parallel Programming

並行プログラミングや並列プログラミングはあるタスクを複数のサブタスクに分解して、 並列実行や複数タスクを同時処理で発生するリスクを管理する技術である。

ここからは、これらのプログラミングで発生するリスクについて説明していく。 リスクを把握しClojureがどうやってリスクを回避しているのかを理解するため、Clojureで並行・並列がどうやって実装されているのかを見ていこう。

Clojure Implementation: JVM Threads

今ままで タスク を、コンピュータがどのように定義しているのかを考えず一連の処理として抽象的につかってきた。 たとえば、メッセージを打つというタスクは、飲み物を飲むという処理とは異なる一連の処理として考えてきた。

Clojureでは、タスクはJVMスレッド上で並行的に実行されるものと考えられる。

What’s a Thread?

スレッドは、サブプログラムである。プログラムは複数のスレッドをもち、 各々のスレッドは、スレッド間で共通アクセス可能な状態を利用しながら、それぞれに課せられた命令を処理する。

スレッド管理は、コンピュータの色々レベルで存在する。たとえば、オペレーティングシステムカーネルは、スレッドの作成・管理のシステムコールを提供している。 JVMも独自プラットフォーム非依存のスレッド管理を提供しており、ClojureJVMで動作するのでJVMスレッドを利用する。

スレッドは一連の命令がつなぎ合わさった糸として考えることができる。私は命令をマシュマロだと思ってる(マシュマロおいしいから)。 プロセッサは、これらの命令を順番に実行する。まるでアリゲータが命令を食べている感じでイメージしたのが下図である(実際アリゲータはマシュマロが好きなのだ)。 プログラム実行というのは、一列にたくさん並んだマシュマロをアリゲータが順番に食べていくものようなものである。 下図は、シングルプロセッサ上でのシングルスレッド処理をモデル化したものである。

f:id:poppon555:20200930105941p:plain

スレッドは、新しいスレッドを生み出してタスクを並行的に処理する。 シングルプロセッサ上では、プロセッサは複数のスレッドを不連続にスイッチングする。 ここに並行性の難しい問題がでてくる。 プロセッセは各々のスレッドの命令を順番に実行するが、いつスレッド間をスイッチングするか保障がないからだ。

次は、A,Bという2つのスレッドと、それらがどのように実行されるかをタイムラインで図示したものである。 Bスレッドの命令はグレーにしている。

f:id:poppon555:20200930110012p:plain

注意すべきは、この実行順序はひとつの可能性でしかないということである。たとえば、A1, A2, A3, B1, A4, B2, B3という順番で処理されることもある。 つまりプログラムは非決定論的ということになる。実行順番が不明で順番が異なれば結果が変わるのであれば、事前にどのような処理結果になるのかはわからない。

以上がシングルプロセス前提の並行性について説明してきたが、マルチコアの場合、各スレッド処理が各コアに割り当てられ、同時に実行される。 各コアは、下図の通り、各スレッドの命令を順番に実行していく。

f:id:poppon555:20200930110024p:plain

シングルコアのときと同様、全体の実行順序がどのようになるか保障はないので、プログラムは非決定論的になる。 2つの目のスレッドを導入するとプログラムは非決定論的になり、これにより3種類の問題に陥ることがある。

The Three Goblins: Reference Cells, Mutual Exclusion, and Dwarven Berserkers

並行プログラミングで陥るこれらの問題について理解するために、つぎの表をみてほしい。

f:id:poppon555:20200930110028p:plain

プロセッサが、A1, A2, A3, B1, B2という順番で実行した場合、 X の値は期待通り 2 になるが、 もしA1, A2, B1, A3, B2という順番で実行された場合、 X の値は、 1 になってしまう。

これが1つ目の問題であり、 参照問題 である。 2つのスレッドが同じ場所を読み書きするが、実行順序によってその値が変わることで発生してしまう。

2つ目の問題は、 相互排他 と呼ばれる。2つのスレッドが同じファイルに文字を書き込むところ想像してほしい。 書き込み処理になんらかの排他制御がない場合、実行順序が狂って、最終的にはめちゃくちゃの文章が構成されてしまう。 次の2つの内容があるとしよう。

By the power invested in me by the state of California, I now pronounce you man and wife

Thunder, lightning, wind, and rain, a delicious sandwich, I summon again

これを相互排他制御なしで、実行すると次のような結果になるかもしれない。

By the power invested in me by Thunder, lightning, wind, and rain, the state of California, I now pronounce you a delicious man sandwich, and wife I summon again

3つめの問題は、デッドロックである。

サイトでは3人バーサーカーを例に説明するがイマイチなので Wikipediaにある哲学者の食事をみてほしい。

ポイントは複数のスレッドが互いに完了するのを待ち続けることで、 いつまで立っても次にすすまない状況になることである。

Futures, Delays, and Promises

Futures, delays, promisesは、並行プログラミングを簡単に扱えるツールである。 各ツールの使い方と並行性の参照問題・相互排他問題の防ぎ方について学習する。 シンプルだが並行プログラミングの要求に応えられる非常に便利なものだと理解できるだろう。

直列のコードで書くならば、次の3つのイベントが必要になる。

  1. タスク定義
  2. タスク実行
  3. タスク実行結果の要求

例として、シンプルな仮想のAPIコールタスクを定義しよう。

(web-api/get :dwareven-beard-waxes)

Clojureがこのタスク定義に到達すると、すぐさま実行しAPIコールが完了するまで結果を待ち続ける。 並行プログラミングを学習すれば、このような一連の時間的なつながりが不要になってくる。 Futures, delays, promisesでタスク定義、実行、結果の取得を分割することができるからだ。

Futures

future をつかえば、定義したタスクを別スレッドで実行させ、実行結果は即時に求めない。 future マクロでfutureを作成することができるので、早速REPLでやってみよう。

(future (Thread/sleep 4000)
        (println "I'll print after 4 seconds"))
(println "I'll print immediately")

Thread/sleep で現在のスレッドで指定したミリ秒数待機する。もしREPLで Thread/sleep を使用すると、待機時間までブロックしてるので、他の評価ができない。 しかし future を使えば、futureで囲った Thread/sleep を含む式を新しいスレッドで実行するので、REPLスレッドは待機することない。つまりブロックされない。

futureをつかえば別スレッドでタスクを実行したままにすることもできるが、その実行結果を利用したい場合もあるだろう。 future 関数は実行結果にアクセスできる参照値を返してくれる。この参照は、クリーニング屋がくれるチケットのようなものである。 チケットを渡してクリーニング依頼した服をうけとることができるが、クリーニングが完了していないならば、それまで待たないといけない。 これと同じで参照してでfutureの結果をうけとるよう依頼することができるが、まだ結果の処理が完了していないならば、待たないといけない。

futureの結果を取得することは、futureの 逆参照 と呼ばれ、 deref 関数あるいは @ リーダマクロを使う。 futureの結果には、futureフォームの最後に評価された式の値がはいる。futureフォームは一回だけ処理されて、その結果はキャッシュされる。

(let [result (future (println "this prints once")
                     (+ 1 1))]
  (println "deref: " (deref result))
  (println "@: " @result))
; => this prints once
; => deref:  2
; => @:  2

注目すべきは、futureを2回逆参照してるが、 this prints once は一回しか出力されないことである。 つまり、futureフォームは1回しか実行されず結果の2はキャッシュされているわけだ。

futureを逆参照すれば、futureの実行が完了するまでブロックすることになる。

(let [result (future (Thread/sleep 3000)
                     (+ 1 1))]
  (println "The result is: " @result)
  (println "It will be at least 3 seconds before I print"))
; => The result is:  2
; => It will be at least 3 seconds before I print

futureは待機時間にリミットを設けることができる。そのためには、 deref にリミットまでの待機時間とオーバしたときの値を指定する。

(deref (future (Thread/sleep 1000) 0) 10 5)
; => 5

これは10ミリ秒以内futureが値を返せないならば、 5 を返すように deref を使っている。

最後に、 realize? をつかってfutureの処理が完了しているかどうかを確認できる。

(realized? (future (Thread/sleep 1000)))
; => false

(let [f (future)]
  @f
  (realized? f))
; => true

futureは並行プログラミングを可能にするシンプルな方法である。 futureを利用すれば、プログラムをより効率的にできるように別スレッドで実行できるようになる。 また別スレッドの実行結果をいつうけとるかコントロールすることができる。

futureを逆参照するとき、その結果をすぐにもとめ取得できるまで評価はストップされる。 これで相互排他問題を扱うのに役立つことがわかるだろう。一方、futureの結果を無視することもできる たとえば、futureをつかってログファイルに非同期に書き込むことができる。この場合、逆参照する必要はないからである。

Delays

Delays をつかえば、実行やその結果をすぐに取得する必要なくタスク定義することができる。 delay を使って実現できる

(def jackson-5-delay
  (delay (let [message "Just call my name and I'll be there"]
    (println "First deref:" message)
      message)))

この例では、なにも出力されない。というのは、まだ let フォームを評価しないようにしているからである。 これを評価しその結果を逆参照するには、 force をつかう。

(force jackson-5-delay)
; => First deref: Just call my name and I'll be there
; => "Just call my name and I'll be there"

futureのように、delayは一回実行するとその結果がキャッシュされる。 forceの後に逆参照すると、コンソール出力されずjackson 5 messageが返ってくる。

@jackson-5-delay
; => "Just call my name and I'll be there"

delayは、関連するいくつかのfutureのうち1つ目が完了したときはじめて実行させるときに効果的になる。 たとえば、いくつかの画像をサイトにアップロードして、1つのアップロードが完了したらユーザに通知する機能を実現するには、次のようになる。

(def gimli-headshots ["serious.jpg" "fun.jpg" "playful.jpg"])

(defn email-user
  [email-address]
  (println "Sending headshot notification to" email-address))

(defn upload-document
  "Needs to be implemented"
  [headshot]
  true)

(let [notify (delay (email-user "and-my-axe@gmail.com"))]
  (doseq [headshot gimli-headshots]
    (future (upload-document headshot)
            (force notify))))

アップロードする画像をベクタで定義し、さらに email-user, upload-document という2つの疑似関数を定義している。 そして、 let でnotifyにdelayをバインドする。delay内の (email-user "and-my-axe@gmail.com") はdelay作成時は評価されない。 評価されるのは、 doseq で作られたfutureのいずれかがはじめて (force notify) を評価したときになる。 (force notify) は3回評価されることになるが、delayの内容は1回しか評価されない。

このテクニックは、並行性プログラミングの相互排他という問題に役に立つだろう。 この問題はあるタイミングで特定のリソースにアクセスできるのは1つのスレッドだけにすることである。 例の場合、delayはメールサーバリソースへのアクセスを防いでいる。 delay内処理は一回しか実行されないので、残り2つのスレッドが同じメールを送ることもなく、今後も同じdelayからメールを送ることはない。 これは非常に強力な制限になるが、この例の場合完璧に動作する。

Promises

Promise はタスクがどのような結果を返すべきか、あるいはタスクをいつ実行すべきかを決めることなく定義することができる。 promise をつかってpromiseを生成し、 delivery をつかってpromiseに結果を割り当てることができる。 割り当てた結果には、逆参照によって取得することができる。

(def my-promise (promise))
(delivery my-promise (+ 1 2))
@my-promise
; => 3

promiseを作成してそれに値を割り当ててる。そして、promiseを逆参照して割り当てた値を取得している。 逆参照は、プログラマが期待した結果を取得する方法であり、 割り当て前に my-promsie を逆参照しようとすると、futureやdelaysのように、割り当てられるまでプログラムはブロックされる。 promiseに割り当てできるのは1回だけである。

promiseを使い方のひとつとして、コレクションデータからはじめに条件を満たした要素をみつけるという使い方がある。 たとえば、オウムに言葉を覚えさせるための餌をあつめているとして、餌はなめらかさレーティングが97以上のヤクのバターが必要だとする。 そして条件を満たすヤクのバターをサイトからみつけだすスクリプトを書かないといけないとする。

次のサンプルコードは、ヤクのバター、APIのモック関数、バターが条件を満たしたものであるかどうか確認するテストする関数を定義してる。

(def yak-butter-international
  {:store "Yak Butter International"
    :price 90
    :smoothness 90})

(def butter-than-nothing
  {:store "Butter Than Nothing"
   :price 150
   :smoothness 83})

;; This is the butter that meets our requirements
(def baby-got-yak
  {:store "Baby Got Yak"
   :price 94
   :smoothness 99})

(defn mock-api-call
  [result]
  (Thread/sleep 1000)
  result)

(defn satisfactory?
  "If the butter meets our criteria, return the butter, else return false"
  [butter]
  (and (<= (:price butter) 100)
       (>= (:smoothness butter) 97)
       butter))

APIコールは本物のように結果を返すまで1秒間待機させている。

同期処理の場合どのくらい時間がかかるのかを調べるために、 some でコクレクションの各要素に対して satisfactory? 関数を適用してはじめに条件を満たしたもの返し、そうでないものはnilを返させる。 同期的に処理されるので、結果を取得するまでに1秒以上かかる。

(time (some (comp satisfactory? mock-api-call)
      [yak-butter-international butter-than-nothing baby-got-yak]))
; => "Elapsed time: 3007.27871 msecs"
; => {:store "Baby Got Yak", :price 94, :smoothness 99}

comp で関数合成させ、 time で評価にかかった時間を出力している。 promiseとfutureをつかえば別スレッドで各要素が条件をみたすかどうかチェックさせることができる。 実行するマシンがマルチコアならば、実行時間を1秒程度まで減らすことができるだろう。

(time
  (let [butter-promise (promise)]
    (doseq [butter [yak-butter-international butter-than-nothing baby-got-yak]]
      (future (if-let [satisfactory-butter (satisfactory? (mock-api-call butter))]
                (deliver butter-promise satisfactory-butter))))
    (println "And the winner is:" @butter-promise)))
; => "Elapsed time: 1006.319212 msecs"
; => And the winner is: {:store Baby Got Yak, :price 94, :smoothness 99}

ここでは、 butter-promise という名前のpromiseを作成して、それにアクセス可能な3つのfutureを作成している。 各futureタスクはサイトを評価して条件を満たした場合その結果をpromiseに割り当てる。 最後に butter-promise を逆参照する。このときプログラムはデータが割り当てられるまでブロックされる。 サイトの評価は並行で行われるので、3秒ではなく1秒程度で完了する。 結果の取得と結果の計算方法を分離することで、並行的に計算させ適切なタイミングで結果を保存できる。

このやり方で参照問題を防げることができる。promiseは一回しか書き込めないので、非決定論的な読み書きを防げるわけだ。

ヤクのバターを満たすものがひとつもなかったら、どうなるのだろうかと思うかもしれない。 その場合、逆参照で永遠にブロックされたままになり、スレッドをしばり続けることになる。 これを防ぐために、タイムアウトを入れることができる。

(let [p (promise)]
  (deref p 100 "timed out"))

ここでは p というpromiseを作成してそれを逆参照しようとしている。 deref に100ミリ秒を指定することで、 その時間までなにも値が取得できない場合、タイムアウトtimed out が使用される。

最後に紹介したいのは、promiseで、Javascriptのように、コールバック関数を登録することができる。 Javascriptのコールバックは、別で定義したコードの処理が終了すれば実行されるように定義できる非同期処理である。

(let [feregi-wisdom-promise (promise)]
  (future (println "Here's some Feregi wisdom" @feregi-wisdom-promise))
  (Thread/sleep 100)
  (deliver feregi-wisdom-promise "Whisper your way to success."))
; Here's some Feregi wisdom Whisper your way to success.

ここでは、futureを作成すると、逆参照で値が取得できるまでスレッドはブロック状態になる。 100ミリ秒経過すると、 feregi-wisdom-promise に値が割り当てられ、future内の println が実行される。

Futures, delay, promisesは、並行プログラミング管理するシンプルで優れたツールである。 次節は、並行プログラミングをうまく扱えるより面白い方法についてみていこう。

Rolling Your Own Queue

ここまで学んできたことは、futures, delays, promisesをつかって並行プログラミングを安全にするシンプルなやり方について見てきた。 この節ではfutureやpromisをマクロと組み合わせて少し複雑なことをやってみる。 不要に感じるかもしれないが便利さを示すことはできるだろう。

並行プログラミングの3つの問題に共通する特徴は、どのタスクも変数やプリンタといった共有のリソースに順序関係なく並行アクセスすることである。 もし共有リソースにあるタイミングでアクセスできるのをひとつのタスクだけにするには、共有リソースにアクセスする部分を直列実行するキューに割り当てるのがよい。

このキューマクロを実装するために、キューを発明してくれたイギリスに敬意を示そう。 キューをつかって、イギリスの慣習的な挨拶"Ello, gov’na! Pip pip! Cheerio!"を正しい順番に行えるようにする。 sleepを処理をよく使うので、まずそれをマクロにしておく。

(defmacro wait
  "Sleep `timeout` seconds before evaluating body"
  [timeout & body]
  `(do (Thread/sleep ~timeout) ~@body))

これは、引数に与えたフォームを実行するまえに Thread/sleep を呼び出し、それらを do でラップしている。 次のコードは、タスクを並行処理させる部分と直列処理させる部分に分割している。

(let [saying3 (promise)]
  (future (deliver saying3 (wait 100 "Cheerio!")))
  @(let [saying2 (promise)]
     (future (deliver saying2 (wait 400 "Pip pip!")))
      @(let [saying1 (promise)]                                ; 1. letの返り値を逆参照する@
        (future (deliver saying1 (wait 200 "'Ello, gov'na!")))
        (println @saying1)
        saying1)
     (println @saying2)
     saying2)
  (println @saying3)
  saying3)

全体の流れを説明すると、各タスク(この場合挨拶の出力)のpromiseと、それらに対応するfutureをつくって並行計算した結果を割り当てる。 どのfutureも全promiseが逆参照されるよりも前に作成され、直列実行される。 "'Ello, gov'na!"saying1 がはじめに、次に saying2, 最後に saying3 の順に出力される。 let ブロックで saying1 を返し、1.の箇所で let を逆参照することで saying1 が完了したあとに saying2 に移動する。 そして saying2 から saying3 も同じように処理が進む。

let ブロックを逆参照することに違和感を感じるかもしれないが、マクロ使用時もコードを抽象化できる。 上記サンプルのようなコードを書くのはメンタルがやられてしまうので、きっとマクロでうまくやりたいと思うだろう。 理想的には、次のようなマクロが機能してほしい。

(-> (enqueue saying (wait 200 "'Ello, gov'na!") (println @saying))
    (enqueue saying (wait 400 "Pip pip!") (println @saying))
    (enqueue saying (wait 100 "Cheerio!") (println @saying)))

このマクロでは第2引数に作成したpromiseにつける名前を指定する。 そして、そのpromiseに値を割り当て方を定義するタスクを第3引数に、 最後にpromiseでどんな処理をするのか定義するタスクを第4引数に指定する。 さらにこのマクロは別の enqueue を第1引数にうけとり、スレッディングマクロが機能している。 このマクロ定義を次に示す。

(defmacro enqueue
   ([q concurrent-promise-name concurrent serialized]
    `(let [~concurrent-promise-name (promise)]
      (future (deliver ~concurrent-promise-name ~concurrent))
       (deref ~q)
      ~serialized
      ~concurrent-promise-name))
   ([concurrent-promise-name concurrent serialized]
   `(enqueue (future) ~concurrent-promise-name ~concurrent ~serialized)))

このマクロはデフォルト値を使うため2つのアリティを用意している。 はじめのアリティでは q パラメータがあり、2つ目のアリティにはなく、そのデフォルト値として (future) をセットしている。 マクロは、promiseを作成して、future内でそれに値を割り当て、 q のフォームを逆参照し、直列にコードを評価し、最後にpromiseを返す。 q が指定されなくても、例外がなげられないようにマクロはfutureを返してくれる。

futureを返さない場合、逆参照してqから値がとれるまでブロックし続けて、最後には例外が発生する。 それを防ぐためにfutureを利用しているということになる。

試しにマクロ展開してみると次のようにになる。

(let* [saying (clojure.core/promise)]
  (clojure.core/future (clojure.core/deliver saying (wait 100 "Cheerio!")))
  (clojure.core/deref (enqueue
                        (enqueue saying (wait 200 "'Ello, gov'na!") (println (clojure.core/deref saying)))
                        saying
                        (wait 400 "Pip pip!")
                        (println (clojure.core/deref saying))))
  (println (clojure.core/deref saying))
  saying)

では、 enqueue マクロがかけたので、実際それで時間が短縮できたかみてみよう。

(time @(-> (enqueue saying (wait 200 "'Ello, gov'na!") (println @saying))
           (enqueue saying (wait 400 "Pip pip!") (println @saying))
           (enqueue saying (wait 100 "Cheerio!") (println @saying))))
; => 'Ello, gov'na!
; => Pip pip!
; => Cheerio!
; => "Elapsed time: 401.635 msecs"

適切な順序で挨拶が実行されており、経過時間から並行処理をただしく行えていることがわかるだろう。

Summary

現代のマルチプロセッサのハードウェアで動くプログラムを書くならば、並行・並列プログラミングの技術を学ぶことは重要である。 プログラムで一つ以上のタスクを処理することを並行と呼び、Clojureでこれを実現するためにはタスクを別々のThreadに依頼して実現する。 コンピュータが同時に複数のスレッドを実行するマルチプロセッサであればプログラムは並列に処理される。

並行プログラミングは、参照問題、相互排他、デッドロックの3つの並行性に絡むリスクを管理するための技術である。 Clojureは並行性をあつかう3つの基本的なツール、futures, delays, promisesを提供し、いずれもタスク定義、タスク実行、タスク結果取得というイベントを分けて設計できる。 Futuresを使えば、タスクを定義後即座に実行し、その結果を取得するかどうかは任される。そしてFuturesはその結果をキャッシュする。 Delaysは必要なときまで実行されないタスクを定義し、これもその結果をキャッシュできる。 Promisesはタスクが将来どのような結果を返すかを知らなくても結果を取得できる。そしてpromiseに値を割り当てるのは1回だけである。

CLOJURE for the BRAVE and TRUEでClojure学習 part7

はじめに

Clojure の学習備忘録。今回はChapter 8: Writing Macrosでマクロの書き方について学ぶ。
これでようやく1章の全Chapter完了である(^o^)

前章がマクロをメカニズムを紹介するのに対して、今回はマクロのより実践的な話。

専門用語をどう訳すが適切なのかわからず進めているので、誤訳あるかもしれませんが、あしからず。

目次

Macros Are Essential

マクロは魅力的なものであるが、コードをド派手にする難解なツールと考えるべきではない。 実際、マクロは、少数のコアな関数やスペシャルフォームから多くの組み込み機能を提供してくれるもの。 when を例にみてみよう。

(when boolean-expression
  expression-1
  expression-2
  expression-3
  ...
  expression-x)

他の多くの言語では、特殊なキーワードを使ってしか条件式を作成できず独自の条件演算子を作る方法もないので、 whenif のようなスペシャルフォームかと思うかもしれないが実はそうではない。 when はマクロである。

マクロ展開すれば、 whenif, do で実装されていることがわかる。

(macroexpand '(when boolean-expression
                expression-1
                expression-2
                expression-3))
; => (if boolean-expression
;       (do expression-1
;           expression-2
;           expression-3))

Anatomy of a Macro

マクロ定義は関数定義とよく似ており、マクロ名や、オプションの説明、引数、処理内容を記述する。 マクロはほぼ必ずリストを返すが、これはClojureが評価できるようにデータ構造を変換するのがマクロの機能であることからわかるだろう。 マクロ定義では、関数や、マクロ、スペシャルフォームが使用でき、関数やスペシャルフォームと同じようにマクロを呼び出すことができる。

もうお馴染みの infix マクロを例にみてみよう。

(defmacro infix
  "Use this macro when you pine for the notation of your childhood"
  [infixed]
  (list (second infixed) (first infixed) (last infixed)))

このマクロは中間記法で書かれたリストを正しい並びに再編成する。

(infix (1 + 1))
; => 2

関数とマクロが異なる点として、関数の場合引数は評価されてから関数内に渡されるのに対して、マクロの場合評価されずにマクロ内に渡されるという違いがある。 これを上記の例でみてみよう。 (1 + 1) を評価しようとすれば、エラーをうけとるであろう。しかし、マクロで呼び出してるので (1 + 1) は評価されずに infix に渡される。 そして、マクロは Clojureが評価できるように first, second, last を使ってリストを再編成している。 マクロ展開をつかえば、、次の通り再編成されていることがわかる。

(macroexpand '(infix (1 + 1)))
; => (+ 1 1)

また、関数のようにマクロ定義のときに分割代入(destructuring)を使うことができる。

(defmacro infix-2
  [[operand1 op operand2]]
  (list op operand1 operand2))

分割代入された引数は、渡されたシーケンス引数の順番に対応してバインドされる。 infix-2 でも引数にシーケンスを受け取り、はじめの値は operand1 という名前に、 2番めの値は、 op という名前に、3つ目の値は、 operand2 という名前にというように 順番に分割代入されていく

またマクロでは、arityも使うことができる。 and, or といった基本的な演算子も実はこれを利用したマクロになっている。 and マクロをみてみよう。

(defmacro and
  "Evaluates exprs one at a time, from left to right. If a form
  returns logical false (nil or false), and returns that value and
  doesn't evaluate any of the other expressions, otherwise it returns
  the value of the last expr. (and) returns true."
  {:added "1.0"}
  ([] true)
  ([x] x)
  ([x & next]
   `(let [and# ~x]
      (if and# (and ~@next) and#))))

これ理解するには、 `, ~@ といったシンボル(後で学習する)を理解しないといけないが、重要なのは3つのマクロ内容があるということである。 0アリティの場合常に true を返し、1アリティの場合は,引数それ自身を返し、nアリティの場合再帰的に自身を呼び出している。 関数と同じようにマクロにも再帰が利用でき、残引数(& next)が使える。

Building Lists for Evaluation

マクロを書くためには、Clojureが評価できるリストを構築しないといけないが、これには通常とは異なる考え方が必要になってくる。 そのひとつとして、クォート式をつかって最終的に構築されるリストが評価されないようにしないといけない。 もっと一般的にいえば, シンボル の違いに対してより深い注意が必要になってくる。

Distinguishing Symbols and Values

たとえば、式を引数に受け取り、その評価結果をコンソール出力させ、さらにその結果を返すマクロをつくりたいとしよう。 マクロに次のようなことをしてほしいわけだ。

(let [result expression]
  (println result)
  result)

評価のため list 関数を使って、そのままこれを次のようにマクロにするかもしれない。

(defmacro my-print-whoopsie
 [expression]
 (list let [result expression]
       (list println result)
       result))

だがこの試みは失敗し、 Can't take the value of a macro: #'clojure.core/let という例外を受け取る。 なぜだろうか?

理由は、letをシンボルのまま返してほしかったのに、 let の参照先を取得しようとしてるからである。 ほかにも、 result, println もシンボルではなくそれの参照先を取得しようとしている。 よって、次のように書けばうまくいく。

(defmacro my-print
  [expression]
  (list 'let ['result expression]
        (list 'println 'result)
        'result))

シンボルのまま扱いたい場合、シングルクォートを先頭につける。これでシンボルを評価させずそのまま扱うことができる。 そして評価させないようクォートをつかうことが、マクロを書くことの重要な考え方となる。 もう少し細かくみていこう。

Simple Quoting

マクロを書くためにはシンボルの評価をさけるためクォートを使用するが、ここでクォートについて簡単におさらいして、マクロ内でどのように使用されるのかを確認しよう。

まずはじめに、シンプルな関数をクォートなしで使うと次の通り。

(+ 1 2)
; => 3

これにクォートつけると、評価前のデータ構造そのまま取得できる

(quote (+ 1 2))
; => (+ 1 2)

このリスト内の + はシンボルである。もしこのシンボルを評価すると、加算関数が得られる

+
; => #function[clojure.core/+]

一方これをクォートすると、加算シンボルが得られる

(quote +)
; => +

バインドされていないシンボルを評価すると例外が発生する

sweating-to-the-oldies
; => Unable to resolve symbol: sweating-to-the-oldies in this context

一方クォートすると、たとえバインドされていなくても、シンボルそれ自身を返す。

(quote sweating-to-the-oldies)
; => sweating-to-the-oldies

シングルクォートは、 (quote x) のリーダーマクロである。

'(+ 1 2)
; => 3

'dr-jekyll-and-richard-simmons
; => dr-jekyll-and-richard-simmons

実際 when マクロでもこれを確認できる。

(defmacro when
  "Evaluates test. If logical true, evaluates body in an implicit do."
  {:added "1.0"}
  [test & body]
  (list 'if test (cons 'do body)))

注目すべきは、マクロ定義で、 if, do がクォートされていることである。 これによりシンボルをシンボルのままとして扱うことができる。 実際にマクロ展開してみてみよう。

(macroexpand '(when (the-cows-come :home)
                (call me :pappy)
                (slap me :silly)))
; => (if (the-cows-come :home) (do (call me :pappy) (slap me :silly)))

もうひとつ別の組み込みマクロ unless をみてみよう。

(defmacro unless
  "Invert 'if'"
  [test & branches]
  (conj (reverse branches) test 'if))

ここでも if はクォートされるので評価されず最終的に次のようなリストを返す。

(macroexpand '(unless (done-been slapped? me)
                      (slap me :silly)
                      (say "I reckon that'll learn me")))
; => (if (done-been slapped? me) (say "I reckon that'll learn me") (slap me :silly))

(conj '(1 2) 3 4) ; => (4 3 1 2) であることを忘れず。

マクロを書くならばこのようにクォートを利用することが多いが、syntax quoteというもっと強力なツールもある。

Syntax Quoting

Syntax Quote(以下準クォートと呼ぶ)は、通常のクォートと同様、データ構造を評価せずに返すが、大きくことなる2つの点がある。 ひとつめは、準クォートは名前空間を含んだフルパスのシンボルを返す。実際に比較してみよう。

名前空間を含めずにクォートをつかったときは、名前空間は含まない。

'+
; => +

名前空間を含めると、出力も名前空間が含まれる。

'clojure.core/+
; => clojure.core/+

準クォートをつかえば、必ずフルパスのシンボルになる。

`+
; => clojure.core/+

リストをクォートすれば、再帰的に働く。

'(+ 1 2)
; => (+ 1 2)

準クォートを使うと、全要素にその効果が働く

`(+ 1 2)

準クォートが名前空間を含むのは、名前衝突を回避するためである。

準クォートのもうひとつの異なる点というのは、チルダ ~ をつかってクォートを解除できることだ。 準クォートされたフォーム内でチルダが登場すると、評価しない、フルパスにするという機能が失われる。

`(+ 1 ~(inc 1))
; => (clojure.core/+ 1 2)

チルダのあとに (inc 1) があるので、クォートされるのではなく、評価される。 チルダをつけなかったらフルパスのシンボルにして返す。

`(+ 1 (inc 1))
; => (clojure.core/+ 1 (clojure.core/inc 1))

文字列補間を知っているならば、準クォート・解除も同じようなものと考えられる。 どちらも、構造化された中にいくつかの変数をくみこんだテンプレートを作成する。 Rubyならば、文字列結合をつかって、 "Churn your butter, Jebedian!" を作成できる。

name = "Jebedian"
"Churn your butter, " + name + "!"

これを文字列補間を使って書けば、次のようにかける

"Churn your butter, #{name}!"

文字列補間のほうが、見やすいだろう。 これと同じで準クォート・解除で書いたほうが見やすくなる。

(list '+ 1 (inc 1))
; => (+ 1 2)

`(+ 1 ~(inc 1))
; => (clojure.core/+ 1 2)

準クォートを使ったほうが簡潔であり、また最終的に生成されるフォームに近いので理解しやすいのだ。

Using Syntax Qutoing in a Macro

準クォートのメカニズムを理解できたので、 code-critic マクロについて見てこう。 このマクロを準クォートを使ってより簡潔なものに書き直す。

(defmacro code-critic
  "Phrases are courtesy Hermes Conrad from Futurama"
  [bad good]
  (list 'do
        (list 'println
              "Great squid of Madrid, this is bad code:"
              (list 'quote bad))
        (list 'println
              "Sweet goriilla of Manila, this is good code:"
              (list 'quote good))))

(code-critic (1 + 1) (+ 1 1))
; => Great squid of Madrid, this is bad code: (1 + 1)
; => Sweet goriilla of Manila, this is good code: (+ 1 1)    

これを準クォートをつかって書き直すと、非常に見やすくなる。

(defmacro code-critic
  [bad good]
  `(do (println "Great squid of Madrid, this is bad code:"
                (quote ~bad))
       (println "Sweet goriilla of Manila, this is good code:"
                (quote ~good))))

ここでは、 good, bad シンボル以外をすべてクォートしてる。 はじめのコードでは個々の要素ごとにクォートしたりリスト関数をつかって明示的にその要素として記述するのに対して、 準クォートをつかった場合、 do 式をクォートして評価したい2つのシンボルだけクォート解除するだけでよい。

以上をまとめると、マクロは任意の未評価のデータ構造を引数に受けとり、Clojureが評価できるデータ構造を返すものである。 マクロ定義するとき、関数と同様、引数の分割代入や、 let を使用することができる。さらに、アリティや再帰的にマクロを呼び出すこともできる。

マクロはリストを返す。リスト構築するためには、 list 関数または準クォートが使用できるが後者をつかったほうが 簡潔なコードになる。そして、もし複数のフォームを返したい場合は、 do で囲ってやればよい。

Refacoring a Macro and Unquote Splicing

前節で扱った code-critic にはまだ改良の余地がある。 println が2回呼ばれていることである。これをきれいにしていこう。 まずはじめに、この println リストを作成するヘルパー関数を作成する。

(defn criticize-code
  [criticism code]
  `(println ~criticism (quote ~code)))

(defmacro code-critic
  [bad good]
  `(do ~(criticize-code "Cursed bacteria of Liberia, this is bad code: " bad)
       ~(criticize-code "Sweet sacred boa of Western and Eastern Samoa, this is good code:" good)))

注目すべきは、ヘルパー関数の criticize-code の戻り値が、準クォートのリストということである。 これでマクロが適切なリストを返してくれるようになる。

まだ改良の余地はあり、同じ関数を複数回呼び出している。これを回避するために、 map をつかって改善してみる。

(defmacro code-critic
  [bad good]
  `(do ~(map #(apply criticize-code %)
        [["Great squid of Madrid, this is bad code:" bad]
         ["Sweet gorila of Manila, this is good code:" good]])))

良さそうに見えるので、実行してみよう。

(code-critic (1 + 1) (+ 1 1))
; => (NullPointerException) at user/eval6154

残念だがうまくいかない。なぜだろうか? 問題は、 map が返す値がリストであること、この場合、 println 式をリストで囲ったものを返すことにある。 println の呼び出し結果を得たいのだが、それをリストの中にいれて評価しようとしてしまうのである。

次のようなコードが実行されてしまっているのである。

(do 
  ((clojure.core/println "criticism" '(1 + 1))
   (clojure.core/println "criticism" '(+ 1 1))))

println はリスト内にあるので評価されてしまう。

(do 
  (nil
   (clojure.core/println "criticism" '(+ 1 1))))

最終的に次のようになってしまう。

(do 
  (nil nil))

こういう仕組みで例外が発生してしまった。 printlnnil に評価されて、最終的に (nil nil) になり、 NullPointerException が発生する。

どういうことかわかりにくが、要するに返す値がリストになってることが問題。 printlnを括弧で囲わないでほしいのに、囲われてるので評価されてアウトというわけだ。 *1 => (nil) => error という流れになってる。

clojure (macroexpand '(code-critic (1 + 1) (+ 1 1)))

この問題を解決してくれるのが、unquote splicing(以下スプライシングと呼ぶ)である。 スプライシングは、 ~@ を使う。通常のクォート解除は次の通りになる。

`(+ ~(list 1 2 3))
; => (clojure.core/+ (1 2 3))

一方、スプライシングを使用すると、次のようになる。

`(+ ~@(list 1 2 3))
; => (clojure.core/+ 1 2 3)

スプライシングを使用すると、シーケンスデータの括弧がなくなり、準クォート内に配置される。 これをさきほどのマクロに使用すると以下の通り。

(defmacro code-critic
  [good bad]
  `(do ~@(map #(apply criticize-code %)
              [["Sweet lion on Zion, this is bad code:" bad]
               ["Great cow of Moscow, this is good code:" good]])))
(code-critic (1 + 1) (+ 1 1))

Things to Watch Out For

ここでは、マクロ使用時につまずくポイントとその回避方法について説明する。

variable Capture

Variable Captureはマクロ利用側が知らないうちに既存のバインディングを上書いてしまう現象である。 次のコードは、マクロ内の let で混乱した結果を招くものである。

(def message "Good job!")
(defmacro with-mischief
  [& stuff-to-do]
  (concat (list 'let ['message "Oh, big deal!"])
           stuff-to-do))

(with-mischief
  (println "Here's how I feel about that thing you did:" message))
; => Here's how I feel about that thing you did: Oh, big deal!

println がシンボル message を参照するとき、 "Good Job!" がバインドされていると思うかもしれない。 しかし、 with-mischief マクロが message に新たなバインディングを作成してしまうので期待通りにはいかない。

準クォートを使うと、例外が発生する。

(def message "Good job!")
(defmacro with-mischief
  [& stuff-to-do]
  `(let [message "Oh, big deal!"]
        ~@stuff-to-do))

(with-mischief
  (println "Here's how I feel about that thing you did:" message))

この例外はありがたい。というのも、準クォートはマクロ内のVariable Captureの発生を防いでくれてるのからである。 もしマクロ内で let を使いたいならば、 gensym を使えばよい。 gensym は呼び出すたびにユニークなシンボルを生成してくれる。

(gensym)
; => G__6145

(gensym)
; => G__6148

(gensym) #+ENDSRC

G__6145G__6148

シンボルにプレフィックスをつけることもできる。

(gensym 'message)
; => message6151

(gensym 'message)
; => message6154

gensym をつかってマクロを書き直すと、次のとおりになる。

(def message "Good job!")
(defmacro with-mischief
  [& stuff-to-do]
  (let [macro-message (gensym 'message)]
    `(let [~macro-message "Oh, big deal!"]
        ~@stuff-to-do
        (println "I stil need to say:" ~macro-message))))

(with-mischief
  (println "Here's how I feel about that thing you did:" message))

gensym で新しいユニークなシンボルを生成して、それを macro-message にバインドする。 準クォート内の let では、 macro-message はクォート解除されて、gensymで生成されたシンボルとなる。 このシンボルは stuff-to-do とは異なるシンボルになるため、variable captureを回避できる。

こういったケースはよくあるので、 auto-gensym というもっと便利なものが使える

`(blarg blarg#)
; => blarg blarg__6168__auto__

`(let [name# "Larry Potter"] name#)
; => (clojure.core/let [name__6171__auto__ "Larry Potter"] name__6171__auto__)

順クォート内のシンボルにハッシュマーク(#)を付け加えることで、auto-gensymが作成される。 このように gensym やauto-gensymはマクロを書いてるときに陥るvariable captureを回避するときに必ず使用される。

Double Evaluation

variable captureの他にも double evaluation(二回評価) というトラップもマクロを書くときにある。 これは、マクロの引数のフォームが複数評価されてしまうものだ。

(defmacro report
  [to-try]
  `(if ~to-try
       (println (quote ~to-try) "was successful:" ~to-try)
       (println (quote ~to-try) "was not successful:" ~to-try)))

(report (do (Thread/sleep 1000) (+ 1 1)))

このマクロは、引数の真偽判定に応じて、成功・失敗どちらかの出力を行うものである。 (Thread/sleep 1000) は2回評価されてしまうので、2秒待たされてしまう。 1回目はifの直後、2回目は println が呼ばれたときである。 これが起きるのは、マクロ展開で (do (Thread/sleep 1000) (+ 1 1)) が複数現れてしまうからで、次のようなコードが実行されている。

(if (do (Thread/sleep 1000) (+ 1 1))
  (println '(do (Thread/sleep 1000) (+ 1 1))
           "was successful:"
           (do (Thread/sleep 1000) (+ 1 1)))

  (println '(do (Thread/sleep 1000) (+ 1 1))
           "was not successful:"
           (do (Thread/sleep 1000) (+ 1 1))))

この問題を回避するには、次のようになる。

(defmacro report
  [to-try]
  `(let [result# ~to-try]
      (println result#)
      (if result#
      (println (quote ~to-try) "was successful:" result#)
      (println (quote ~to-try) "was not successful:" result#))))

(report (do (Thread/sleep 1000) (+ 1 1)))

to-trylet 内に配置することで、1回だけ評価されるようにして、その結果をauto-gensymで生成されたシンボル(result#)にバインドする。 これで2回評価されるこはなくなるわけだ。

Macros All the Way Down

マクロ展開が評価の前に行われることで、ハマってしまう問題もある。 たとえば、 report マクロを 単独で複数回呼び出せば問題ないが、 doseq をつかって繰り返し呼び出すときうまくいかないことがある。

(report (= 1 1))
; => (= 1 1) was successful: true

(report (= 1 2))
; => (= 1 2) was successful: false

doseq をつかえば次の通り、期待通りにいかない。

(doseq [code ['(= 1 1) '(= 1 2)]]
  (report code))
; => code was successful: (= 1 1)
; => code was successful: (= 1 2) おかしい

reportマクロを単体で呼び出したときはうまく機能するが、 doseq で繰り返したときは機能していない。 この理由は、 doseq の各イテレーションごとのマクロ展開をみてみるとわかる。

(if
 code
 (clojure.core/println 'code "was successful:" code)
 (clojure.core/println 'code "was not successful:" code))

report マクロの各イテレートの code シンボルは評価されない。 report のマクロ展開時には、評価結果にアクセスできないのだ。

これを解決するためには、次のようなマクロを書けばよい。

(defmacro doseq-macro
  [macroname & args]
  `(do 
      ~@(map (fn [arg] (list macroname arg)) args)))

(doseq-macro report (= 1 1) (= 1 2))
; => (= 1 1) was successful: true
; => (= 1 2) was not successful: false

Brews fro the Brave and True

ここでは、 if-valid というバリデーション機能をもったマクロを書いていく。 自分でマクロを書くのにとってもよい訓練となるだろう。

Validation Functions

シンプルに考えたいので、名前とメールアドレスのバリデーションについて考える。 我々のお店では、次のような注文明細を受け取るようにしたい。

(def order-details
  {:name "Mitchard Blimmons"
   :email "mitchard.blimmonsgmai.com"})

このハッシュマップのメールアドレスは@がないので無効であるが、これをバリデーションでつかまえたいのである。 理想的には、次のようなコードが書きたい。

(validate order-details order-details-validations)
; => {:email ["Your email address doesn't look like an email address."]}

検証対象のデータ(order-details)と、バリデーション定義(order-detail-validations)を引数にとる validate 関数を呼び出せるようにしたいのである。 関数適用結果として、無効なフィールドをkeyに、無効の理由をベクタとしてvalueにもつハッシュマップを受け取りたい。

早速 order-details-validations から実装していこう。

(def order-details-validations
  {:name
    ["Please enter a name" not-empty]
   :email
    ["Please enter an email address" not-empty

     "Your email address doesn't look like an email address"
     #(or (empty? %) (re-seq #"@" %))]})

検証したいkeyに、エラーメッセージと検証関数のペアをベクタが紐付いたハッシュマップである。 たとえば、 :name キーは、ひとつの検証関数(not-empty)をもち、 検証結果がエラーならば、 Please enter a name というエラーメッセージを受け取る

次に validate 関数を実装しよう。 validate 関数は2つの関数で構成され、 ひとつは該当フィールドに検証関数を適用する関数で、 もうひとつは、 {:email ["Your email address doesn't look like an email address."]} のように検証結果のエラーメッセージをひとつにまとめる役割を持つ関数である。

次の error-message-for は前者を実装したものである。

(defn error-messages-for
  "Return a seq of error messsage"
  [to-validate message-validator-pairs]
  (map first (filter #(not ((second %) to-validate))
                    (partition 2 message-validator-pairs))))

第一引数の to-validate は検証したい値である。第二引数の message-validator-pairs は偶数個の要素をもつシーケンスデータである。 (paritition 2 message-validator-pairs) でペアに分割され、第一要素がエラーメッセージ、第二要素が関数となる(order-details-validations と同じ並びになる)。

error-message-for 関数は、エラーメッセージと検証関数のペアごとに、 to-validate を検証してtrueになった場合ものを絞り込む。 そして、 map first をつかってエラーメッセージをのみを返す。

(error-messages-for "" ["Please enter a name" not-empty])
; => ("Please enter a name")

では、つぎにエラーメッセージをハッシュマップにまとめる関数を実装して仕上げよう。

(defn validate
 [to-validate validations]
 (reduce (fn [errors validation]
           (let [[fieldname validation-check-groups] validation
                 value (get to-validate fieldname)
                 error-message (error-message-for value validation-check-groups)]
             (if (empty? error-message)
              errors
             (assoc errors fieldname error-message))))
         {}
         validations))

(validate order-details order-details-validations)
; => {:email ("Your email address doesn't look like an email address")}

if-valid

完成したバリデーション関数は、大抵次のようにつかうだろう。

(let [errors (validate order-details order-details-validations)]
  (if (empty? errors)
    (println :success)
    (println :failure errors)))

共通してつかわれるところは、

  1. レコードを検証して、その結果を errors にバインドする
  2. エラーがあるかどうかチェックする
  3. もしエラーがなければ、それに応じた処理が実行される。ここでは、 (println :success) になる。
  4. エラーがあれば、それに応じた処理が実行される。ここでは、 (println :failure errors) になる。

これを本番コードに導入してると、同じようなコードを繰り返し書いてることに気がついて、抽象化が必要だと気づくようになった。 具体的には、 validate 関数を適用して、結果をあるシンボルにバインドして、バインド結果からエラーがあるかどうかを確認する部分である。 抽象化のために、次のようなコードを書くかもしれない。

(defn if-valid
  [record validations success-code failure-code]
  (let [errors (validate record validations)]
    (if (empty? errors)
      success-code
      failure-code)))

これはうまく動かない。というのも、 success-code, failure-code が評価されてしまうからである。 しかし、評価のコントールできるマクロをつかえば、うまく動作させられる。次のようにマクロを使えるようにしよう。

(if-valid order-details order-details-validations errors
  (render :success)
  (render :faliure errors))

このマクロは、繰り返しの部分をうまく回避し、実現したいことを簡潔に表現してくれる。 早速実装してみよう。

(defmacro if-valid
  "Handle validation more concisely"
  [to-validate validations error-name & then-else]
  `(let [~error-name (validate ~to-validate ~validations)]
     (if (empty? ~error-name)
       ~@then-else)))

このマクロは、 to-validate, validations, errors-name, then-else の4つの引数をもつ。 errors-name を使うのは新しい戦略である。 then-else 内でvalidate関数の結果を使用したいので、errors-nameを用意して検証結果バインドさせている。 マクロ展開すると、次のようになる。

(macroexpand
  '(if-valid order-details order-details-validations my-error-name
       (println :success)
       (println :failure my-error-name)))

; (let*
;   [my-error-name (user/validate order-details order-details-validations)]
;   (if (clojure.core/empty? my-error-name)
;     (println :success)
;     (println :failure my-error-name)))

準クォートで let/validate/if を使ったフォームを抽象化して、 さらにスプライシングif 分岐で使って、残引数の then-else を適切に評価できるようにしている。

Summary

本章では、マクロの書き方について学習してきた。マクロは関数定義とよく似ており、引数、docstring、処理内容がある。 また分割代入、残引数、再帰も使える。マクロはリストを返す。単純なマクロならば、 list, seq を使うときもあるが、 多くの場合、 ` 準クォートで書くことになるだろう。

マクロを書くときには、シンボルと値の違いを意識することが重要である。 マクロ展開されたあとに評価されるので、評価結果にマクロ内ではアクセスできない。 重複評価やvariable captureもトラップになるが、 let 式とgensymsを賢く使用すれば回避できる。

マクロはより自由にプログラムできる楽しいツールである。プログラマが評価をコントロールできるため、 他の言語では不可能な表現を可能にしてくれる。Clojureの学習のなかで、マクロは危険、つかうべきでないといった意見を聞くこともあるだろう。 しかし、少なくともはじめは、これらの意見に耳を傾けてはいけない。率先して使って楽しみなさい。 使ってみないとマクロの最適な箇所はわからないからである。そのうちに反対派の意見もわかってくるようになるからだ。

*1:println …

CLOJURE for the BRAVE and TRUEでClojure学習 part6

はじめに

久しぶりの投稿になったがClojure の学習備忘録。 今回はChapter 7: Clojure Alchemy: Reading, Evaluation, and Macrosで以下を学ぶ

  • Clojureの評価モデル
  • Readerについて
  • Evaluatorについて

評価モデルこそ、Lispが他の言語と大きく異なる特徴的な部分だと理解できる。そしてマクロがどのように振る舞うのかというメカニズムについて理解深まる面白いパートである。

またスペシャルフォームが、なぜスペシャルと呼ばれるのかを理解できたことはうれしい章である。

目次

Clojure Alchemy: Reading, Evaluation, and Macros

賢者の石といえば、錬金術で最も有名なアイテムで、鉛から金を変化できるものだ。 Clojureには、賢者の石のように都合よく変更できるツール、マクロがある。

マクロを使えば、どんな表現でもClojureが実行できるもの変更してくれるので、 必要に応じて言語自体を拡張させることができる。

実際マクロの力をみてみよう。

(defmacro backwards
  [form]
  (reverse form))

(backwards (" backwards" " am" " I" str))

(" backwards" " am" " I" str)Clojureの文法に違反してるので評価できないのだが、 backwards マクロをつかえば、Clojureは正しく評価してくれる。 賢者の石が鉛から金を生成できるように、 backwards で新しい文法を作り出すこと、つまり好きなようにClojureを拡張できる。

本章では、マクロがかけるための概念的な部分について、 Clojureの評価モデル, reader, evaluator, macro expander について説明する。

An Overview of Clojure’s Evaluation Model

他のLisp言語と同様Clojureも、他の多くの言語と異なる評価モデルをもっており、 ソースコードread し、Clojureのデータ構造を作り出すフェーズがある。

その後データ構造は 評価 (evaluate)される。 Clojureはデータ構造を走査して、関数適用だったり、データ構造の種別に応じた変数探索といったアクションを行う。 たとえば、 (+ 1 2) という文字列が読み込まれると(read)、その結果は、第一要素の + シンボルのあとに、数値の1,2がつづくリストと分析される。 リストが評価(evaluate)されると、 + に一致する関数を見つけ出し、1,2が適用される。

このようなソースコードと評価の関係をもつ言語は、同図像性をもつ(ホモイコニック)と呼ばれる。 ホモイコニック言語では、コードはプログラマが操作できるデータ構造として推論される。

プログラミング言語には、コンパイラインタプリタがあり、それはユニコードで構成されたコードをマシン語に変換してくれる。 この変換で、 abstract syntax tree(AST) が構築され、あるデータ構造として表現される。 /evaluator/は、このASTをインプットとして受け取り、その木構造を操作してマシンコードなどをアウトプットとして生成してくれる。

多くの言語では、ASTのデータ構造にプログラミング言語からアクセスすることはできない。 プログラミング言語領域とコンパイラ領域は完全に分かたれ、合流することはありえない。 次の図は、非Lisp言語のコンパイルプロセスを示したものである。

f:id:poppon555:20200827135625j:plain

一方、ClojureLisp言語の場合は違う。 アクセス不可のASTを評価するのではなく、Lisp言語は素のデータ構造を評価する。 Clojure木構造を評価するが、木構造Clojureのリストや値で構成されている。

リストは、木構造を構成するに理想的な表現である。第一要素はルートとして、その後に続く要素はそのブランチとして扱われる。 ネストした木構造をつくりたければ、次の図の通り、ネストしたリストをつくればよい。

f:id:poppon555:20200827135722j:plain

まずはじめに、Clojurereader は 文字列の (+ 1 (* 6 7)) をネストしたリストに変換する。 そして、Clojureevaluator はそれを入力として受け取って、その評価結果を出力する。

f:id:poppon555:20200827135655j:plain

しかし、/evaluator/ はどこからのインプットなのかに関心をもたないので、 インプットが reader である必要はない。 そのため、 eval を使えば、プログラマが書いたコードを evaluator に直接送ることがができる。

(def addition-list (list + 1 2))
(eval addition-list)
; => 3

このとおり、コードからClojureのリストを評価できた。 詳細の説明については追々していくとして、ここでは簡単な説明に留めると、 まずClojureはリストを評価して、 addition-list が参照するリストをみつだす。 そして、 + シンボルに対応する関数をみつけ、 1, 2 を引数としてその関数に適用する。 これは、プログラマのデータ構造であれ、/evaluator/ のデータ構造であれ同じである。 その結果、Clojureの力を最大限を発揮させることができるようになる。

(eval (concat addition-list [10]))
; => 13

(eval (list 'def 'lucky-number (concat addition-list [10])))
; => #'user/lucky-number

lucky-number
; => 13

このとおり直接 evaluator にコードを送ると、実行可能な形式に関数やデータが修正される。

Clojureはホモイコニックな言語である。つまり、コードはリスト形式で記述されるが、それがもうAST表現になっている。 自分が書いたコードが、 evaluator によって評価されるデータ構造と同じ表現であるため、 プログラムがどのように修正されるのか推論するのがたやすくなる。

マクロはこの変換をプログラマから簡単に行えるようにしたものである。 マクロについてより詳細に理解できるようにするため、残りはClojurereader, evaluator について説明する。

The Reader

リーダは、ファイルやREPLに記述されたコードをClojureのデータ構造に変換する。 プログラマが書いたユニコードを文字列を、Clojureの世界のリストや、ベクタ、マップ、シンボルなどに変換してくれる。 ここでは、リーダを直接操作しながら、より簡潔なコードを可能にしてくれる リーダマクロ について学習しよう。

Reading

REPLを起動して、以下の文字列を入力してみよう。

(str "To understand what recursion is," " you must first understand recursion.")

REPLに入力したものはただのユニコードの文字列なのだが、Clojureのデータ構造として判断される。 データ構造を文字列で表記したものは、 reader form と呼ばれる。 この例では、 str とその他2つの文字列からなるフォームを要素に持つリストを表現したフォームということになる。

この文字列がリーダに渡ると、対応するClojureのデータ構造が作られ、さらにそれが評価されると次の通り出力される。

"To understand what recursion is, you must first understand recursion."

読み込み(reading)と評価(evaluation)は、異なるプロセスなので、独立させて実行させることができる。 reader プロセスを直接操作するには、 read-string をつかう。 read-string に文字列を渡すと、リーダと同じようにデータ構造を返す。

(read-string "(+ 1 2)") ; 文字列のリストをわたす
; => (+ 1 2)            ; Clojureのリストに変換される

(list? (read-string "(+ 1 2)"))
; => true  変換したデータ構造がリストであること確認できる

(conj (read-string "(+ 1 2)") :zagglewag) ; Clojureのリストなのでconj関数も適用できる
; => (:zagglewag + 1 2)

そしてreaderの出力結果を評価することもできる。

(eval (read-string "(+ 1 2)"))
; => 3

ここまで紹介してきたものは、リーダフォームとデータ構造が一対一の対応関係があるものになる。 もう少しこの対応関係を紹介する。

  • () はリストのリーダフォーム
  • str はシンボルのリーダフォーム
  • [1 2] は2つの数値リーダフォームからなるベクタリーダフォーム
  • {:sound "hoot"} はキーリーダフォームと文字列リーダーフォームからなるマップリーダフォーム

繰り返しになるが、リーダフォームとは、Clojureのデータ構造を文字列表記したもの

しかしながら、リーダはもうすこし複雑な挙動をするときがある。

(#(+ 1 %) 3)
; => 4

これをリーダに読み込ませてみよう。

(read-string "(#(+ 1 %) 3)")
; => '((fn* (p1__6174#) (+ 1 p1__6174#)) 3)

これが一対一しない場合である。 (#(+ 1 %) 3) を読み込んむとリストを出力するが、 それは、 fn* シンボルと、シンボルを要素に持つベクタ、さらに3要素をもつリストをもっているわけだ。 どういうことだろうか?

Reader Macro

これの答えは、 reader macro によって (#(+ 1 %) 3) は変換されたということになる。 リーダマクロは、テキストをデータ構造に変換する一連のルールをもっている。 リーダマクロのおかげで、省略リーダフォームが展開されて、コードを簡潔にかける。 省略リーダフォームは、 macro character と設計されており、 ', #, @ が代表例である。

たとえば、シングルクォートは、次のように展開される。

(read-string "'(a b c)")
; => (quote (a b c))

deref リーダマクロも同じで、 @ のときに展開される。

(read-string "@var")
; => (clojure.core/deref var)

コメントアウト部分は省略して展開してくれる。

(read-string "; ignore!\n(+ 1 2)")
; => (+ 1 2)

以上が、リーダについてになる。次は、評価(evaluator)についてみていこう。

The Evaluator

f:id:poppon555:20200827135209j:plain

Clojureevaluator は、引数にデータ構造をうけとり、データ構造の種別に応じたルールを適用して、結果を返す関数と考えてよい。 シンボルを評価するときは、Clojureはシンボルの参照先をみつけだす。リストを評価するときは、リストの第一要素をみて関数、マクロ、スペシャルフォームらを呼び出す。 文字列や、数値、キーワードといった他の値は、それ自身として評価される。

たとえば、 (+ 1 2) とREPLに入力したときを考える。 上図に示したデータ構造が、 evaluator の送られる。

リストが送られたので、 evaluator はその第一要素の評価からスタートする。第一要素は+シンボルなので、対応する関数をみつけて次の処理に進む。 第一要素は関数だとわかったので、次にそのオペランドを評価しにいく。オペランドの1, 2は、リストやシンボルではないので、そのまま評価される。 最後に1,2をオペランドとして+関数に適用し、その結果を返す。

ここからは、 evaluator がそれぞれのデータ構造をどのように評価していくのかを詳しく紹介していく。

These Things Evaluate to Themselves

リストやシンボルではないデータ構造を評価するときは、データ構造それ自体が出力される。

true
; => true

false
; => false

{}
; => {}

:huzzah
; => :huzzah

空のリストも同様である。

()
; => ()

Symbols

プログラマの重要タスクの一つは、値と名前の関連づけであり、chapter3では, def, let や関数定義で学習したことである。 シンボル を使えば、関数、マクロ、データ、その他なんであれ名前をつけることがができ、その名前で評価することできる。 シンボルを評価するとき、Clojure名前空間からそれと関連付けられた値を見つけ出す。 最終的にシンボルは、 , スペシャルフォーム のいずれかとして判定される。 スペシャルフォームとは、Clojureに組み込まれた基本的な演算子のことである。

一般的にClojureがシンボルを評価するときは、次の通りになる。

  1. シンボルがスペシャルフォームかどうかを判定する。そうでないならば、
  2. ローカルバインドされたものかどうか判定する。そうでないならば、
  3. def で定義されて名前空間に存在するかどうか判定する。そうでないならば、
  4. 例外をなげる

まずスペシャルフォームの場合についてみてみよう。 if のようなスペシャルフォームは必ず演算子として使用されるため、リストの第一要素に現れる。

(if true :a :b)
; => :a

この例でも、ifはスペシャルフォームであり、演算子として使用されていることがわかる。もしスペシャルフォームをリストなしに使用すると例外が返される。

if
; => Unable to resolve symbol: if in this context

次は、ローカルバインディングについてみてみよう。/ローカルバインディング/ とは、 def を使わないでシンボルと値を関連付けさせることである。 例では、 let を使用してシンボル x に5をバインドしてる。 x を評価すると、シンボルxは、5として現れる。

(let [x 5]
  (+ x 3))
; => 8

名前空間上にxと15を対応付ければ、次の通りになる

(def x 15)
(+ x 3)
; => 18

次の例では、xは15にマッピングされているが、xは let で5にローカルバインディングされてもいる。 この場合、 let が優先される。

(def x 15)
(let [x 5]
  (+ x 3))
; => 8

ネストした場合、直近にあるバインディングが利用される。

(let [x 5]
  (let [x 6]
    (+ x 3)))
; => 9

関数もローカルバインディングを行っており、渡されたパラメータを関数内の引数にバインディングしてる。 次の例では、 exclaim は関数にマッピングされてる。関数内の exclamation パラメータは、関数の引数として渡されたものにバインドされる。

(defn exclaim
  [exclamation]
  (str exclamation "!"))

(exclaim "Hadoken")
; => "Hadoken!"

最後に、 map, inc について見てみよう。

(map inc [1 2 3])
; => (2 3 4)

Clojureがこのコードを評価するとき、まず map シンボルを評価し、その参照先関数を見つ出し、引数をそれに適用する。 map シンボルはmap関数を参照するが、関数それ自体ではないことに注意しよう。 文字列 fried saled がデータ構造であるのと同様、 map シンボルもデータ構造なのであり、関数それ自体ではないのだ。

(read-string ("+"))
; => +

(type (read-string "+"))
; => clojure.lang.Symbol

(list (read-string "+") 1 2)
; => (+ 1 2)

この例でも、プラスのシンボル + をデータ構造として扱ってのであり、参照する関数として扱ってるわけではない。 評価してはじめて、Clojureは参照する関数を見つけ出し、適用する。

(eval (list (read-string "+") 1 2))
; => 3

Lists

空リストを評価すれば、空のリストが返る

(eval (read-string "()"))

空でなければ、リストの第一要素を呼び出そうとするが、第一要素の性質によって異なってくる。

Function Calls

関数呼び出しならば、全オペランドが評価された後、関数の引数として渡される。 たとえば、 + シンボルの場合をみてみよう。

(+ 1 2)
; => 3

Clojureはリストの先頭が関数だとわかれば、続いて残りの要素を評価する。 オペランドの1,2は、シンボルやリストでないのでそれ自身として評価されて、そしてそれを関数に適用する。

ネストした関数呼び出しの場合はどうだろうか。

(+ 1 (+ 2 3))
; => 6

第2引数がリストになっているが、Clojureの先程と同じプロセスを踏む。 + シンボルを見つけては、その引数を評価する。 (+ 2 3) を評価するときは、第一要素を関数として判定し、残りの要素も評価する。このように再帰的に評価される。

Sepecial Forms

スペシャルフォームは、関数で実装できない機能をもつのでスペシャルと呼ばれる。

(if true 1 2)

この例では、シンボル if で始まるリストを評価している。 if シンボルはスペシャルフォームをつくるので、 true, 1, 2オペランドとして評価する。

スペシャルフォームの評価は、関数評価と異なる。たとえば、関数を呼び出したとき、全オペランドが評価される。 しかし if を使用するときは、特定のオペランドを評価してほしい場合となる。条件の真偽に応じて、特定のオペランドのみ評価させることができるのだ。

quote スペシャルフォームについてもみてみよう。

'(a b c)

これはリーダマクロによって、以下のように表現される。

(quote (a b c))

今まで見てきたとおり普通、 a シンボルがリストの第一要素なので、それを呼び出そうとする。 しかし、 quote スペシャルフォームは、「次のデータ構造を通常通り評価するのではなく、データ構造そのものを返してほしい]と評価方法をかえる。 その結果、シンボル a, b, c からなるリストを受け取る。

def, let, loop, fn, do, recur も同様で、関数と同じやり方で評価しない。 たとえば、 def, let を評価するときも、それの参照先をみつけたりしない。代わりに、シンボルと値の関連付けを作成してくれる。 このように、リーダからデータ構造が送られてくると、シンボルを走査してリストの先頭要素の関数やスペシャルフォームを呼び出す。 またマクロをリスト先頭要素にすると、また新しい評価方法が得られる。

Macros

マクロを理解するには、実際にコードをみてみよう。たとえば、Clojureで、 (+ 1 1) ではなく (1 + 1)中置記法で評価してほしいとする。 これはマクロではない。むしろ、ただ中置記法でかいたら評価できるようによしなに変更してほしいということを示したいだけである。

(read-string "(1 + 1)")
; => (1 + 1)

このリストを評価すれば例外をなげる

(eval (read-string "(1 + 1)"))
; => ClassCastException java.lang.Long cannot be cast to clojure.lang.IFn

しかし read-string が返すリストを評価できるよう適切に変換すれば、うまくいく。

(let [infix (read-string "(1 + 1)")]
  (list (second infix) (first infix) (last infix)))
; => (+ 1 1)     

これを評価すればもちもん2が出力される。

(eval
  (let [infix (read-string "(1 + 1)")]
  (list (second infix) (first infix) (last infix))))
; => 2     

うまくいったが不格好だ。そこでマクロなのである。マクロを使えば、Clojureが評価する前にリストを操作することができる。 マクロは関数に似ており、引数と返り値をもつ。また関数と同様、データ構造として機能する。 マクロがユニークなのは、評価プロセスに適するように振る舞えることである。 マクロは、 readerevaluator の間に位置するので、リーダが読み取ったデータ構造を evaluator にわたす前に変換することができる。

例をみてみよう。

(defmacro ignore-last-operand
  [function-call]
  (butlast function-call))

(ignore-last-operand (+ 1 2 10)) ;1
; => 3

(ignore-last-operand (+ 1 2 (println "look at me!!!")))
; => 3

1で、 ignore-last-operand マクロが引数として受け取るのは、評価後の13ではなく、 (+ 1 2 10) リストそれ自体である。 この振る舞いは、関数のと異なるものである。というのも、関数は呼び出す前に、全引数を評価し、ある引数は評価したり、評価しなかったりすることは不可能だからである。 それに対して、マクロの場合、オペランドは評価されない。具体的言うと、シンボルの参照先を解決せず、シンボルをそのまま受け取る。それはリストでも同じである。 つまり、リストの第一要素が、関数や、スペシャルフォーム、マクロとしてではなく、未評価のリストデータ構造として受け取る。

もうひとつ関数と異なるのは、関数の戻り値のデータ構造は評価されないが、マクロの場合評価される。 マクロの返り値が計算されるプロセスは、 マクロ展開(macro expansion) と呼ばれ、 macroexpand をつかえば、マクロが返す評価される前のデータ構造を確認することができる。

(macroexpand '(ignore-last-operand (+ 1 2 10)))
; => (+ 1 2)

(macroexpand '(ignore-last-operand (+ 1 2 (println "look at me!!!"))))
; => (+ 1 2)

ご覧の通り、どちらの結果も (+ 1 2) というリストになる。このリストが評価されると、3が出力されるわけだ。

(defmacro infix
  [infixed]
  (list (second infixed) 
        (first infixed) 
        (last infixed)))

(infix (1 + 2))
; => 3     

f:id:poppon555:20200827135554j:plain

マクロ展開のプロセスを絵にすると、上図のとおりである。

以上が評価プロセスにフィットさせるようなマクロの振る舞いである。 マクロのありがたみというのは、 (1 + 2) といったデータ構造をClojureが評価可能な形 (+ 1 2) に変更してくれることになる。 これが意味することは、プログラマClojure自身を拡張させるさせることができること、すなわちマクロが 文法抽象(syntactic abstraction) を可能にしているということである。

Syntactic Abstraction and the -> Macro

Clojureのコードは、関数呼び出しがネストする場合がよくあり、たとえば、次のようなコードもそうである。

(defn read-resource
  "Read a resource into a string"
  [path]
  (read-string (slurp (clojure.java.io/resource path))))

関数の中身を理解するには、括弧の一番深いところ、 (clojure.java.io/resorce path) をみつけて、 右から左へと関数の戻り値が次の関数の引数に送られていく流れを追わないといけない。 この右から左にコードを読んでいくという流れは、非Lispプログラマの読み方と逆になっている。 もっともClojureの書き方になれれば問題ではないが、左から右へ、上から下へコードを読んでいきたいなら、 -> マクロが使える。 このマクロは、スレッディングマクロ、あるいはスタビーマクロと呼ばれ、先程の処理を次のように書き直せる。

(defn read-source
  [path]
  (-> path
      clojure.java.io/resorce
      slurp
      read-string))

これなら最深括弧から外側の括弧へではなく、上から下に読むことができるわけだ。 まず、 pathio/resource の引数として渡され、その関数適用結果が slurp の引数として渡り、最後にslurpの出力結果が read-string の引数に渡っていく。

どちらのコードでも処理は同じになるが、スレッディングマクロを使ったほうが、上から下へという普段から慣れた書き方なので理解しやすい。 また -> マクロで括弧も省略されてるので、ビジュアル的にもみやすくもなっている。 これが 文法抽象 の力であり、Clojureの組み込み文法とは異なる書いたものを、人間が理解できる書き方で記述できるようにしてくれるのだ。

CLOJURE for the BRAVE and TRUEでClojure学習 part5

はじめに

Clojure の学習備忘録。
今回はChatper 6: Organizing Your Project: A Librarian’s Taleで以下を学ぶ

前半が、名前空間についての説明で、
後半は、lein new app で作成されたプロジェクトで、名前空間(nsマクロ)の使い方が紹介される。

また本章では、Clojureの体系的なシステムを図書館システムのように機能していると説明される。

目次

Your Project as a Library

現実世界の図書館には、本や雑誌、DVDなどが保管されている。
そこでは住所システムが用いられ、住所がわかればそこに足を運んで、目的の品を取得できる。

もちろん、目的の品がどこにあるのかを人間が知っている必要はない。 代わりに図書館が住所と図書のタイトルを関連付けて、それを検索できるツールを提供してくれる。

Clojureでも同じようなことをやってくれるので、Clojureも順序づけられた広大な棚に格納されたオブジェクトのように考えられる。 プログラマはオブジェクトがどこの棚に格納されているかをしる必要はない。 代わりに、目的のものを取得できるように目印をClojureに与えることができる。

そのため、Clojureは目印と棚の住所との関連づけを管理する必要があり、それが名前空間で実現可能となる。 名前空間は、シンボルと棚の住所への参照先の対応付けを人間が理解できるようにしてくれるわけだ。

技術的には、名前空間clojure.lang.Namespace という種類のオブジェクトであり、他のデータ構造と同じようにあつかうことができる。 たとえば、 *ns* で現在の名前空間を参照することができ、 (ns-name *ns*) でその名前を取得できる。

(ns-name *ns*)
; => user

たとえば、REPLを開始すると、 user という名前空間にいる。プロンプトには、 user=> のように現在の名前空間が表示される。

名前空間は、ハードウェアの制約を無視すれば、いくつでも作成できる。Clojureのプログラムでは、常にどこかの名前空間に所属する。

シンボルに関していうと、今まで名前空間を意識することなくシンボルを使ってきた。たとえば、 (map inc [1,2]) と書いたとき map, inc は両方ともシンボルである。これら map などのシンボルをClojureに与えてやると、現在の名前空間内で対応する変数をみつけ、 変数に書かれてある棚の住所を取得して、棚から目的のものを(この場合、 map が参照してる関数を)取得してくれる。 もしシンボルの参照先ではなく、シンボルそれ自体を使用したいのであれば、クォートしなければならない。 Clojureのフォームをクォートすれば、そのフォームは評価されず、データとして扱われる。 次は、REPL上でクォート化したフォームを評価したサンプルである。

inc
; => #function[clojure.core/inc]
; incが参照してる関数のテキスト表記が得られる。

'inc
; => inc
; シンボル名incが得られる

(map inc [1 2])
; => (2 3)

'(map inc [1 2])
; => (map inc [1 2])
; map, inc, ベクタが評価されずそのまま出力される

Strong Objects with def

Clojureでオブジェクトを棚に保管するもっともよく使うツールは、 def である。 他にも defn もあるが、これも内部で def が使用されている。

(def great-books ["East of Eden" "The Glass Bead Game"])
; => #'user/great-books

great-books
; => ["East of Eden" "The Glass Bead Game"]

このコードは、Clojureに次のように命じている

  1. 現在の名前空間にある対応関係において great-books と変数の関連付けを更新しなさい
  2. 保管スペースがある棚を見つけなさい
  3. その棚に、 ["East of Eden" "The Glass Bead Game"] を保管しなさい
  4. その棚のアドレス(住所)を変数に書き込みなさい
  5. 書き込んだ変数(この場合, #user/great-books)を返しなさい

この処理は、変数の閉じ込めと呼ばれ、 ns-intern名前空間の対応関係にアクセスすることできる。

(ns-interns *ns*)
; => {great-books #'user/great-books}

get 関数で、特定の変数を取得できる

(get (ns-interns *ns*) 'great-books)
; => #'user/great-books

(ns-map *ns*) を使っても同様のことができる。

#'user/great-books は変数のリーダーフォームであるが、リーダフォームについては別のchapterで説明するとして、 名前空間 user にあるシンボル great-books にひもづく変数を指している。 deref で変数の参照先を取得できる。

(deref #'user/great-books)
; => ["East of Eden" "The Glass Bead Game"]

このフォームの意味するところは、

  1. 変数から棚の番地を取得して、
  2. そこまで足を運んで、
  3. 目的地にあるものを取得して、
  4. 私にくれ

ということをClojureに命じているのである。

ただつぎのように使うのが一般的だ。

great-books
; => ["East of Eden" "The Glass Bead Game"]

では、もう一度同じシンボルに対して def を使用するとどうなるだろうか?

(def great-books ["The Power of Bees" "Journey to Upstairs"])
great-books
; => ["The Power of Bees" "Journey to Upstairs"]

変数は、新しいベクタの住所で上書きされてしまう。棚の住所がかき消されて新しい住所が書き込まれてしまったわけである。 もはやはじめにセットしたベクタにはアクセスできなくなってしまう。これが名前衝突と呼ばれるものであり、混乱をまねく原因となる。

もっともこれ自体は、javascriptRubyなどの言語でも起こりうることではあるが、何が問題かといえば、 無意識に自分で書いたコードが上書きされてしまうため、サードパーティのライブラリにも自分のコードが入っていないという保証がなくなってしまう。 これを回避するためにClojureでは、名前空間を利用する。

Creating and Switch to Namespaces

Clojureには名前空間を作成する3つツールがある。 create-ns 関数, in-ns 関数、そして ns マクロである。 実際の開発で使うのは ns になるが、その説明よりも先に他の2つを先に説明する。

create-ns はシンボルを受け取って、同名の名前空間が存在しないならば、新たにシンボル名の名前空間を作成して返す。

(create-ns 'cheese.taxonomy)
; => #namespace[cheese.taxonomy]

ns-name で、名前空間名を取得できる

(ns-name (create-ns 'cheese.taxonomy))
; => cheese.taxonomy

実際には、 create-ns は使わない。というのも、名前空間を作成するだけで、そこに移動しないのは不便だからである。 in-ns がもっと一般的で,新しい名前空間を作成し、移動までしてくれる。

(in-ns 'cheese.analysis)
; => #namespace[cheese.analysis]

実行後REPLプロンプトは、 cheese.analysis> と表示されるように、実際に名前空間を移動したことを示している。 ここで def を使えば、 cheese.analysis という名前空間上にオブジェクトを保管することができる。

しかし、他の名前空間にあるデータや関数を使いたいときどうすればよいのだろうか? そのためには、 名前空間/シンボル のようにフルパスでシンボルを使えばよい。

(in-ns 'cheese.taxonomy) ; cheese.taxonomyを作成して移動
(def cheddars ["mild" "medium" "strong" "sharp" "extra sharp"])
(in-ns 'cheese.analysis) ; cheese.analysisを作成して移動
cheddars
; => Unable to resolve symbol: cheddars in this context

この通り別の名前空間で定義された cheddars をそのままでは呼び出すことできないが、次であれば可能になる。

cheese.taxonomy/cheddars
; => ["mild" "medium" "strong" "sharp" "extra sharp"]

このようにフルパスシンボルでならアクセスできるが、毎回入力するとなるとめんどくさい。 そのためClojureでは、 refer, alias というツールを用意してくれてる。

refer

refer は他の名前空間にあるオブジェクトを細かいレベルでコントールすることができる。

(in-ns 'cheese-taxonomy)
(def cheddars ["mild" "medium" "strong" "sharp" "extra sharp"])
(def bries ["Wisconsin" "Somerset" "Brie de Meaux" "Brie de Melun"])
(in-ns 'cheese.analysis)
(clojure.core/refer 'cheese.taxonomy)
bries
; => ["Wisconsin" "Somerset" "Brie de Meaux" "Brie de Melun"] ; 別の名前空間の変数にアクセスできる

cheddars
; => ["mild" "medium" "strong" "sharp" "extra sharp"]

refer をつかえば、フルパスで指定しなくても別の名前空間のシンボルにアクセスできる。 これが可能となっているのは、現在の名前空間のシンボル/オブジェクト対応表が更新されているからである。 次のコードを見てみよう。

(clojure.core/get (clojure.core/ns-map clojure.core/*ns*) 'bries)
; => #'cheese.taxonomy/bries

(clojure.core/get (clojure.core/ns-map clojure.core/*ns*) 'cheddars)
; => #'cheese.taxonomy/cheddars

つまり次のようなことをやってくれているわけだ。

  1. cheese.taxonomy 上で ns-interns を呼ぶ
  2. それを現在の名前空間ns-map とマージする
  3. マージした結果を新しい ns-map として現在の名前空間に作成する

refer を使うときに、フィルター用に :only, :exclude, :rename が使える。 名前から分かる通り、 :only, :exclude は現在の名前空間にマージするものを制限することができる。 :rename は別名で現在の名前空間にマージできるようになる。

:only は次のように使える

(clojure.core/refer 'cheese.taxonomy :only ['bries])
bries
; => ["Wisconsin" "Somerset" "Brie de Meaux" "Brie de Melun"]

cheddars
; => => RuntimeException: Unable to resolve symbol: cheddars

:exclude は次のように使える

(clojure.core/refer 'cheese.taxonomy :exclude ['bries])
bries
; => RuntimeException: Unable to resolve symbol: bries
cheddars
; => ["mild" "medium" "strong" "sharp" "extra sharp"]

:rename は次のように使える

(clojure.core/refer 'cheese.taxonomy :rename {'bries 'yummy-bries})
bries
; => RuntimeException: Unable to resolve symbol: bries
yummy-bries
; => ["Wisconsin" "Somerset" "Brie de Meaux" "Brie de Melun"]

同一名前空間からしか使用できない関数をつくりたいときもあるだろう。 そんなときは、 defn- を使用してprivateな関数をつくればよい。

(in-ns 'cheese.analysis)
;; Notice the dash after "defn"
(defn- private-function
  "Just an example function that does nothing"
  [])
(in-ns 'cheese.taxonomy)
(clojure.core/refer-clojure)
(cheese.analysis/private-function)
(refer 'cheese.analysis :only ['private-function])

たとえ refer を使っても、別の名前空間のprivate関数を利用できない。

alias

refer と比べると、 alias はもっとシンプルである。名前空間に短縮(ショートカット)名をつけるだけである。

(clojure.core/alias 'taxonomy 'cheese.taxonomy)
taxonomy/bries
; => ["Wisconsin" "Somerset" "Brie de Meaux" "Brie de Melun"]

cheese.taxonomy ではななく、 より短い言葉 taxonomy でシンボルにアクセスできるようになる。 おかげでREPL開発が捗る。

しかし、REPL上で全プログラムを書きあげてしまうことはないだろう。 よって次のセクションでは、現実のプロジェクトで必要になることを説明しよう。

Real Project Organization

ようやく準備が整ったので、ここからは実際のプロジェクトで今まで学習してきたことを生かしていこう。 ファイルパスと名前空間の対応関係について、 require, user を使ったファイルのロード、 そして ns を使った名前空間のセットアップ方法について説明する。

The Relationship Between File Paths and Namespace Names

2羽の鳥によって大切なチーズが盗まれてしまった。 このやっかいなチーズ泥棒を捕まえるために盗まれてしまった場所をマッピングする プロジェクトを作ってみる。

lein new app the-divine-cheese-code

ディレクトリ構造はいかのように作られるだろう。

.
├── CHANGELOG.md
├── LICENSE
├── README.md
├── doc
│   └── intro.md
├── project.clj
├── resources
├── src
│   └── the_divine_cheese_code
│       └── core.clj
└── test
    └── the_divine_cheese_code
        └── core_test.clj

src/the_divien_cheese_code/core.cljを開こう。一行目は次の通り記述されてるだろう。

(ns the-divine-cheese-code.core
  (:gen-class))

今まで名前空間を作成には in-ns を使ってきたが、 ns名前空間の作成・管理でもっとも使われるので これについて説明していく。一方 (:gen-class) はchapter12で説明する。

今の名前空間the-divine-cheese-code.core である。 Clojureでは、名前空間とファイルのパス名に次のような一対一の関係がある。

それでは the-divine-cheese-code.visualization.svg を作ってみよう。

mkdir src/the_divien_cheese_code/visualization
src/the_divine_cheese_code/visualization/svg.clj

次に、 require, use について見てみる。

Requiring and Using Namespaces

名前空間 the-divine-cheese-code.core から 名前空間 the-divine-cheese-code.visualization.svgsvgマークアップ関数を使いたいときは、 require を使えば良い。だがその前に、 svg.clj を実装しよう。

(ns the-divine-cheese-code.visualization.svg)

(defn latlng->point
  "Convert lat/lng map to comma-separated string"
  [latlng]
  (str (:lat latlng) "," (:lng latlng)))

(defn points
  [locations]
  (clojure.string/join " " (map latlng->point locations)))

latling->point, points を定義した。これらは、シーケンス化された緯度経度情報を文字列に変換する関数である。 この関数を、 core.clj から使用したい場合、 require を使う。 require名前空間のシンボルを引数に取るので、 (require 'the-divine-cheese-code.visualization.svg) のように記述すれば、 Clojureは該当のファイルを名を読み込んで評価してくれる。 評価されると、 the-divine-cheese-code.visualization.svg という名前空間が作成されて、そこに前述の関数が定義される。 適切なパスにcljを格納しても自動でClojure読み込んでくれるわけではなく、プログラマが明示的にClojureに教えてあげるないといけない。

さらに、フルパスで指定しなくてすむように refer を使う。 これで別の名前空間の関数を呼べるようになったわけなので、 heists シーケンスを core.clj に加えて確認しよう。

(ns the-divine-cheese-code.core)
;; SVGコードを評価する
(require 'the-divine-cheese-code.visualization.svg)
;; フルパス指定しなくて済ませるためにreferを使う
(refer 'the-divine-cheese-code.visualization.svg)

(def heists [{:location "Cologne, Germany"
              :cheese-name "Archbishop Hildebold's Cheese Pretzel"
              :lat 50.95
              :lng 6.97}
             {:location "Zurich, Switzerland"
              :cheese-name "The Standard Emmental"
              :lat 47.37
              :lng 8.55}
             {:location "Marseille, France"
              :cheese-name "Le Fromage de Cosquer"
              :lat 43.30
              :lng 5.37}
             {:location "Zurich, Switzerland"
              :cheese-name "The Lesser Emmental"
              :lat 47.37
              :lng 8.55}
             {:location "Vatican City"
              :cheese-name "The Cheese of Turin"
              :lat 41.90
              :lng 12.45}])

(defn -main
  [& args]
  (println (points heists)))    

lein run で実行した結果は、次のとおりになる。 main 関数では heistspoint 関数に適用している。

50.95,6.97 47.37,8.55 43.3,5.37 47.37,8.55 41.9,12.45

require を使うときに, :as, alias も合わせるとエイリアスが使用できる。

(require '[the-divine-cheese-code.visualization.svg :as svg])

これは、次と同じ意味である。

(require 'the-divine-cheese-code.visualization.svg)
(alias 'svg 'the-divine-cheese-code.visualization.svg)

別名をつけた(エイリアスした)名前空間では、次のように呼び出せる。

(svg/points heists)
; => "50.95,6.97 47.37,8.55 43.3,5.37 47.37,8.55 41.9,12.45"

use ならば、 require, refer 両方を使用せずとも別の名前空間にアクセスできる。 本番コードで使用するのはよろしくないが、REPL上で実験するときには、早くて便利である。

(use 'the-divine-cheese-code.visualization.svg)

これは次の同じことである。

(require 'the-divine-cheese-code.visualization.svg)
(refer 'the-divine-cheese-code.visualization.svg)
(alias 'svg 'the-divine-cheese-code.visualization.svg)

またエイリアスを使うとつぎのようにもかける

(use '[the-divine-cheese-code.visualization.svg :as svg])
(= svg/points points)
; => true

(= svg/latlng->point latlng->point)
; => true

use を使えば points でアクセスできるので、一見冗長な使い方に思えるかもしれない。 しかし、 use にオプションの :only, :exclude, :as, :rename を使えば便利になる。

(require 'the-divine-cheese-code.visualization.svg)
(refer 'the-divine-cheese-code.visualization.svg :as :only ['points])

これを use を使って書くと次のようになる。

(use '[the-divine-cheese-code.visualization.svg :as svg :only [points]])
(refer 'the-divine-cheese-code.visualization.svg :as :only ['points])
(= svg/points points)
; => true

;; エイリアス利用してアクセス可
svg/latlng->point

;; これはアウト
latlng->point
; アクセスできないので例外発生

The ns Macro

ようやく ns マクロを説明できる段階にきた。今までつかったきた in-ns, refer , alias, require は REPL上ではよく使用されるが、実際の開発では ns マクロのほうがもっと便利な機能を用意されているので使われる。 このセクションでは require, use, in-ns, alias, refer が組み込れた便利な ns の使い方をみていこう。

ns の便利なところの1つ目は、デフォルトで名前空間 clojure.core を参照してくれることである。 このおかげで、 clojure.core/println ではなく println で呼び出すことができる。

clojure.core から何を参照するかは refer と同じオプションが使える :refer-clojure を使ってコントロールできる。

(ns the-divine-cheese-code.core
  (:refer-clojure :exclude [println]))

この場合、 -main では、 clojure.core/println で呼び出さないといけなくなる。 ns 内の (:refer-clojure) フォームは、 reference(参照) と呼ばれ、上のコードは次と同じことである。

(in-ns 'the-divine-cheese-code.core)
(refer 'clojure.core :exclude ['println])

ns 内では6種類の参照がつかえる。

  • (:refer-clojure)
  • (:require)
  • (:use)
  • (:import)
  • (:load)
  • (:gen-class)

(:import), (:gen-class) は12章で説明し、 (:load) はめったに使用されないので紹介しない。

(:require)require 関数と同じような機能をもつ。

(ns the-divine-cheese-code.core
  (:require the-divine-cheese-code.visualization.svg))

これは次と等しい。

(in-ns 'the-divine-cheese-code.core)
(require 'the-divine-cheese-code.visualization.svg)

注意することは、 ns の場合、 ' を使わなくてよいことである。 そして、 require では次の通り alias が使える。

(ns the-divine-cheese-code.core
  (:require [the-divine-cheese-code.visualization.svg :as svg]))

これは次と等しい

(in-ns 'the-divine-cheese-code.core)
(require ['the-divine-cheese-code.visualization.svg :as 'svg])

複数のライブラリを参照したい場合は、次のように書く。

(ns the-divine-cheese-code.core
  (:require [the-divine-cheese-code.visualization.svg :as svg]
            [clojure.java.browse :as browse]))

これは次と等しい

(in-ns 'the-divine-cheese-code.core)
(require ['the-divine-cheese-code.visualization.svg :as 'svg])
(require ['clojure.java.browse :as 'browse])

(:require)require の違いは、前者の場合, 参照したいものを絞ることができる。

(ns the-divine-cheese-code.core
  (:require [the-divine-cheese-code.visualization.svg :refer [points]]))

これは次と等しい

(in-ns 'the-divine-cheese-code.core)
(require 'the-divine-cheese-code.visualization.svg)
(refer 'the-divine-cheese-code.visualization.svg :only ['points])   

:all キーワードを使えば、全シンボルが参照できる

(ns the-divine-cheese-code.core
  (:require [the-divine-cheese-code.visualization.svg :refer :all]))    

これは次と等しい

(in-ns 'the-divine-cheese-code.core)
(require 'the-divine-cheese-code.visualization.svg)
(refer 'the-divine-cheese-code.visualization.svg)    

コードを読み込み、別名をつけて、参照するのが好まれた使い方なので、 :use の使用はおすすめしないが、この書き方に出くわすこともあるので使い方を知っておいたほうがよい。

(ns the-divine-cheese-code.core
  (:use clojure.java.browse))

これは次と等しい

(in-ns 'the-divine-cheese-code.core)
(use 'clojure.java.browse)

また次のコードは、

(ns the-divine-cheese-code.core
(:use [clojure.java browse io]))

こちらと同じことである。

(in-ns 'the-divine-cheese-code.core)
(use 'clojure.java.browse)
(use 'clojure.java.io)

注意すべきは、 :use はベクタを引数にとる。ベクタの第一要素を base として残りの要素を参照する。

To Catch a Burglar

ns マクロの説明が済んだので、盗まれたチーズ問題の解決にもどろう。

盗まれた緯度経度の座標情報からSVGイメージを作成したい。 だが、2つの理由で座標情報をそのまま使用できない。 ひとつめは、緯度は、南から北にむかって数値が上がるのに対して、SVGのy座標は、数値があがると上から下に下がってしまうからである。 つまり、上下逆さにして緯度情報を利用する必要があるのだ。 2つ目は、描かれるものが非常に小さいことで、ズームしてあげる必要がある。

それをコードにすると次の通りである。

(ns the-divine-cheese-code.visualization.svg
  (:require [clojure.string :as s])
  (:require :exclude [min max]))

(defn comparator-over-maps ;1
  [comparision-fn ks]
  (fn [maps]
       (zipmap ks          ;2
               (map        ;3
                 (fn [k] (apply comparision-fn (map k maps)))
                  ks)))

(defn min (comparator-over-maps clojure.core/min [:lat :lng])) ;4
(defn max (comparator-over-maps clojure.core/max [:lat :lng]))

1は少々トリッキーなのだが、関数を返す関数である。 返された関数は、引数で指定された comparision-fn から ks をkeyにもつvalueを比較してくれる関数である。

4の comparator-over-maps で作った min, max 関数は、左上と右下の座標を求めるためのものである。

(min [{:a 1 :b 3}, {:a 5 :b 0}])
; => {:a 1 :b 0}

min 関数内で使用してる zipmap は、2つのシーケンスを引数にとり、新しいシーケンスを返す関数である。 はじめのシーケンスが、ハッシュマップのkeyとなり、もう一方のシーケンスがそのvalueとなる。

(zipmap [:a :b] [1 2])
; => {:a 1 :b 2}

したがって、2では、 ks がzipmapが返すハッシュマップのkeyとなり、3のmapの結果がそのvalueとなる。

続きのコードを書こう

(defn translate-to-00
  [locations]
  (let [mincoords (min locations)]
    (map #(merge-with - % mincoords) locations)))

(defn scale
  [width height locations]
  (let [maxrecords (max locations)
        ratio {:lat (/ height (:lat maxrecords))
               :lng (/ width (:lng maxrecords))}]
    (map #(merge-with * % ratio) locations)))

translate-to-00 は、 min で盗まれた座標の最小のものを取得して、そこを原点とした相対座標を返す。 merge-with は次のように機能する関数である。

(merge-with - {:lat 50 :lng 10} {:lat 5 :lng 5})
; => {:lat 45 :lng 5}

scale 関数は、最大座標をもとに倍率を求めて、残りの座標にも展開して返す関数である。

svg.clj の残りのコードは次の通り。

(defn latlng->point
  "Convert lat/lng map to comma-separated string" 
  [latlng]
  (str (:lat latlng) "," (:lng latlng)))

(defn points
  "Given a seq of lat/lng maps, return string of points joined by space"
  [locations]
  (s/join " " (map latlng->point locations)))

(defn line
  [points]
  (str "<polyline points=\"" points "\" />"))

(defn transform
  "Just chains other functions"
  [width height locations]
  (->> locations
       translate-to-00
       (scale width height)))

(defn xml
  "svg 'template', which also flips the coordinate system"
  [width height locations]
  (str "<svg height=\"" height "\" width=\"" width "\">"
       ;; These two <g> tags change the coordinate system so that
       ;; 0,0 is in the lower-left corner, instead of SVG's default
       ;; upper-left corner
       "<g transform=\"translate(0," height ")\">"
       "<g transform=\"rotate(-90)\">"
       (-> (transform width height locations)
            points
            line)
       "</g></g>"
       "</svg>"))

ここで登場した関数は簡単なもので、 {:lat x :lng y} マップを受け取って、SVGで表現できるように変換してくれる。

latlng->point 関数は、SVGマークアップで表現できる文字列の座標情報に変換してくれるものである。 points 関数は、 lat/lng シーケンスマップをカンマ区切りの座標情報に変換してくれる。 line 関数は、与えられた座標からSVGマークアップで表現できる直線情報を返してくれる。 transform 関数は、座標シーケンスを平行移動させ、倍率をあげる。 xml 関数は、最終的なSVGマークアップ表記を作成してくれる。

core.clj も仕上げよう

(ns the-divine-cheese-code.core
  (:require [clojure.java.browse :as browse]
            [the-divine-cheese-code.visualization.svg :refer [xml]])
  (:gen-class))

(def heists [{:location "Cologne, Germany"
              :cheese-name "Archbishop Hildebold's Cheese Pretzel"
              :lat 50.95
              :lng 6.97}
             {:location "Zurich, Switzerland"
              :cheese-name "The Standard Emmental"
              :lat 47.37
              :lng 8.55}
             {:location "Marseille, France"
              :cheese-name "Le Fromage de Cosquer"
              :lat 43.30
              :lng 5.37}
             {:location "Zurich, Switzerland"
              :cheese-name "The Lesser Emmental"
              :lat 47.37
              :lng 8.55}
             {:location "Vatican City"
              :cheese-name "The Cheese of Turin"
              :lat 41.90
              :lng 12.45}])

(defn url
  [filename]
  (str "file:///"
       (System/getProperty "user.dir")
       "/"
       filename))

(defn template
  [contents]
  (str "<style>polyline { fill:none; stroke:#5881d8; stroke-width:3}</style>"
       contents))

(defn -main
  [& args]
  (let [filename "map.html"]
    (->> heists
         (xml 50 100)
         template
         (spit filename))
    (browse/browse-url (url filename))))    

複雑なコードはない。 -mainxml, template からマークアップを完成させて、 spit でファイルに吐き出し、 browse/browse-url でブラウズしてる。

さいごに

実際には、nsマクロの使い方を知っておけばいいのが、
それがどんな役割をしてくれるのを分解して説明してくれる章でした。

CLOJURE for the BRAVE and TRUEでClojure学習 part4

はじめに

Clojure の学習備忘録。 今回は、Chapter 5: Functional Programming では以下を学習する

  • pure functionとはなんであるか?
  • pure functionがなぜ便利なのか?

以下pure functionを純粋関数と呼ぶ。

目次

Pure Functions: What and Why

println, rand を除けば、今まで使ってきた関数はすべて純粋関数である。 純粋関数は、以下2つの特性をもつ。 

  • 渡された引数が同じならば、必ず同じ結果を返す これは 参照透過性 と呼ばれる
  • サイド・エフェクト(副作用)を生まない。 関数の外にある変数やファイルといった関数スコープ外にあるアクセス可能な対象を変更しないこと。

この2つの特性は、関数の独立化を可能にし、他のシステムに影響を与えないため、より論理的なコーディングができる。 「この関数を呼び出したら、他に影響しないだろうか」といったことで悩む必要はなくなるわけだ。

Pure Functions Are Referentially Transparent

純粋関数かどうかは、渡される引数と、イミュータブルな返り値という2点においてのみ決まる。

参照透過性の例は次の通り。

(+ 1 3)
: => 3

イミュータブルな値にのみ依存してるならば、その関数は参照透過性をもつといえる。 文字列 , Daniel-san もイミュータブルなので、次の関数も参照透過性をもつと呼べる。

(defn wisdom
  [words]
  (str words ", Daniel-san"))

(wisdom "Always bathe on Fridays")
; => "Always bathe on Fridays, Daniel-san"     

一方参照透過性を満たさないは次の通り。 実行するたびに異なる結果を返すため、乱数を利用する関数はみな参照透過性をもつことはない。

(defn year-end-evaluation
  []
  (if (> (rand) 0.5)
    "You get a raise!"
    "Better luck next year!"))     

ファイルを読み込む関数もまた参照透過性をもたない。というのは、ファイルの内容は常に一定だと保証されないからである。 よって analyze-file は参照透過性がないが、 analysis は参照透過性をもつ関数になる。

(defn analyze-file
  [filename]
  (analysis (slurp filename)))

(defn analysis
  [text]
  (str "Character count: " (count text)))     

Pure Functions Have No Side Effect

副作用は、名前と値の関連付けを変更するものであり、javascriptを例にみてみよう。

var haplessObject = {
  emotion: "Carefree!"
};

var evilMutator = function(object){
  object.emotion = "So emo :'(";
}

evilMutator(haplessObject);
haplessObject.emotion;
// => "So emo :'("

もっとも副作用がなければ、ファイルへの書き出しや画面のRGB変更などは実現できない。 しかし、それでも副作用は変数名やkey名の参照先が何なのかをわからなくさせるため潜在的な危険を残してしまう。 参照先がなぜ・どのように変更されたのか追うのが困難になって、デバッグもむずかしくなったりする。副作用のある関数は、呼び出したときに外部の変数を変更しないかだけではなく、副作用関数に依存しているすべての関数でも同じく注意をはらないといけない。 一方副作用のない関数ならば、インプットとアウトプットの関係のみを考えばよく、それ以外に変更されること心配しなくてよい。

というわけで、副作用のある関数は限定的に使用するのがよい。 Clojureは、コアとなるデータ構造がイミュータブルなので、副作用のない関数を作成しやすい。

Living with Immutable Data Structures

イミュータブルなデータ構造というのは、副作用がないということである。 しかし副作用なしでどうやってコードをかけばよいのだろうか?

Recursion Instead of for/while

javascriptで次のようなコードを書いたことはあるだろう。

var wrestlers = getAlligatorWrestlers();
var totalBites = 0;
var l = wrestlers.length;

for(var i=0; i < l; i++){
  totalBites += wrestlers[i].timesBitten;
}

ループの外にある変数(totalBites)だけでなく、ループ内の変数iにも副作用がある。 このような副作用は対して問題にならないが、Clojureはそれも許さない。

Clojureでループ処理を書くにはどうすればよいのか? map, reduce を使用するやり方もあるが、 ここでは再帰を利用した関数型プログラミングのやり方を紹介する。

次の例をみてほしい。 Clojureでは再代入の演算子がないので、新しいスコープを作らない限り新しい値を変数にバインドできない。

(def great-baby-name "Rosanthony")
great-baby-name
; => "Rosanthony"

(let [great-baby-name "Bloodthunder"]
  great-baby-name)
; => "Bloodthunder"

great-baby-name
; => "Rosanthony"     

はじめにグローバル空間に変数名 great-baby-nameResonthony をバインドする。 次に、 let で新しいスコープをつくり、そこで great-baby-nameBloodthunder をバインドする。 let 式の評価が終わると、グローバル空間にもどって great-baby-nameRosanthony として再び評価される。

このようにスコープが異なると同じ変数名に別の値をバインドできる性質は、再帰に利用され、次の例は再帰的な問題解決である。

(defn sum
  ([vals] (sum vals 0))        ; 1
  ([vals accumlating-total]
    (if (empty? vals)          ; 2 
      accumlating-total
      (sum (rest vals) (+ (first vals) accumlating-total)))))

sum関数は2つの引数をうけとり、ひとつはコレクション(vals)で、もうひとつは蓄積用変数(accumlating-total)で、 Arityのオーバロードを利用して、accumlating-totalの初期値は0にしてる(1)。

どんな再帰的な解決も処理を継続するかどうかのチェックがあるように、sum関数も引数のチェックをおこない、 vals が空かどうかチェックする。 もし空ならば(2)、コレクションの全要素の処理が完了したので、 accumlating-total を返す。 vals が空でないと、まだ未処理の要素が残ってるので、再帰的にsumを呼び出す。 そのときの第一引数は、 (rest vals) とvalsの残りの要素が渡され、第二引数には、 (+ (first vals) accumulating-total) となり、 valsの第一要素を蓄積用変数に追加したものが渡される。これでreduceと同じように合計数を計算できるわけだ。

ただパフォーマンスの都合上, 実際のプログラムでは次のように recur を使用したほうがよい。 これはClojureが、末尾再帰をサポートしてないからということであるが、末尾再帰については説明を割愛する。

(defn sum
  ([vals]
     (sum vals 0))
  ([vals accumulating-total]
     (if (empty? vals)
       accumulating-total
       (recur (rest vals) (+ (first vals) accumulating-total)))))

Function Compsition Instead of Attribute Mutation

オブジェクトの状態を完全なものに仕上げるとき、ミューテーション(状態変更)を行うことがある。 たとえばRubyで、 GlamourShotCaption オブジェクトが入力値のスペース除去・ "lol"の大文字化する例を考えよう。

class GlamourShotCaption
  attr_reader :text
  def initialize(text)
    @text = text
    clean!
  end

  private
  def clean!
    text.trim!
    text.gsub!(/lol/, "LOL")
  end
end

best = GlamourShotCaption.new("My boa constrictor is so sassy lol!  ")
best.text
; => "My boa constrictor is so sassy LOL!"     

Glamourshotcaption クラスは、cleanメソッドで処理をカプセル化する。 Glamourshotcaption オブジェクトをつくると、内部変数に文字列を割り当てて、このミューテーション(内部状態変更)処理が実行される。

Clojureの場合、次のようになる。

(require '[clojure.string :as s])
(defn clean
  [text]
  (s/replace (s/trim text) #"lol" "LOL"))

(clean "My boa constrictor is so sassy lol!   ")
; => "My boa constrictor is so sassy LOL!"

Clojureでは状態変更はまったく行われない。 clean 関数は、イミュータブルな値を受けとると、純粋関数の s/trim でスペースをトリミングし、 さらにその結果を 純粋関数 s/replace に渡して、大文字化した結果を返す

このように関数を組み合わせるやり方は、関数合成とよばれる。 先程の再帰が同じ関数を引数を変えて何度も呼び出したのに対して、関数合成は異なる関数を組み合わせる。 一般的に関数型プログラミングでは、このようにシンプルな関数を組み合わせてより複雑な処理を構築する。

Cool Things to Do With Pure Functions

データから新しいデータを生み出すのと同じように、関数から新しい関数を生み出すことができる。 前chapterで登場した partial もそれであるが、ここではさらに comp, memorize を紹介する。

comp

前節でみたように、入力と出力の関係にだけに注目すればよいので、純粋な関数同士の合成は常に安全になる。 関数合成はよく使われるので、Clojureでは comp 関数で合成関数をつくれる。

((comp inc *) 2 3)    
; => 7

ここでは、 inc, * から匿名合成関数をつくっている。この関数に引数2,3を渡すと、 2 * 3が行われた後、それに+1した結果を返す。 数学的な用語で説明すると、f1, f2, …, fnから合成関数gをつくったとき g(x1, x2, …, xn)は、f1(f2(fn(x1, x2, …, xn)))と同義である。

注意すべきは、 最初に適用される * は引数をいくつでも受け取れるが、 それ以外の関数は1つしか引数が取れないということである。

次の例では、 comp を使ってロールプレーイングゲームのキャラクタの属性を抜き出せる。

(def character
  {:name "Smooches McCutes"
   :attributes {:intelligence 10
                :strength 4
                :dexterity 5}})
(def c-int (comp :intelligence :attributes))
(def c-str (comp :strength :attributes))
(def c-dex (comp :dexterity :attributes))

(c-int character)
; => 10

(c-str character)
; => 4

(c-dex character)
; => 5

匿名関数を使っても同じことができるが、 comp を使ったほうがコード量が少なくエレガントである。 これら3つの関数はすべて引数が1つであるが、合成したい関数のなかに2つ以上の引数をもつものがあるときどのようにすればよいだろうか? そういうときは匿名関数を使える。

(defn spell-slots
  [char]
  (int (inc (/ (c-int char) 2))))

(spell-slots character)
; => 6

このspell-slotsは、はじめ2で割り、それに1を足し、最後に小数をなくすため整数にする関数である。 割り算は引数を2つもっているので、 comp を使って同じ処理をさせるには次のようにする。

(def spell-slots-comp (comp int inc #(/ % 2) c-int))

2で割るために、匿名関数で包むわけだ。

memoize

純粋関数を使う時にもうひとつ便利なのがメモイズであり、特定の関数の呼び出し結果を記録することができる。 メモイズは、関数の引数と戻り値を保存することができる、2回目同じ引数で関数を呼び出したとき、すぐさま結果を受け取れる。 処理に時間かかる関数を使うときには便利である。たとえば、処理に1秒かかる関数がメモイズされていない場合は次の通り。

(defn sleepy-identity
  "Returns the given value after 1 second"
  [x]
  (Thread/sleep 1000)
  x)
(sleep-identity "Mr. Fantasitico")
; => "Mr. Fantasitico" after 1 second

(sleep-identity "Mr. Fantasitico")
; => "Mr. Fantasitico" after 1 second

2回目の呼び出しも1秒かかる。しかし、メモイズを利用すれば1秒かかるのは、1回目呼び出しだけになる。 それ以降の呼び出しは、即座に結果を返す。

(def memo-sleepy-identity (memorize sleepy-identity))
(meom-sleepy-identity  "Mr. Fantasitico")
; => 1秒後に"Mr. Fantasitico"を返す

(meom-sleepy-identity  "Mr. Fantasitico")
; => すぐに"Mr. Fantasitico"を返す

さいごに

はじめに書いた内容を再度整理すると、

  • pure functionとはなんであるか?
    同じ引数ならば同じ結果を返し、副作用がない関数のこと。

  • pure functionがなぜ便利なのか?
    クラスのメソッドならば別の箇所で利用しにくいが、関数ならば簡単に転用できる
    関数合成を使えば、シンプルな関数を組合わせてより複雑な処理を実現できる

これはClojureに限った話ではないなとあらためておもう。 jsでもループに再帰関数を利用できるわけなので、他の言語を利用するときも可能な限り純粋関数で書くようにしよう。 個人的には、純粋関数で書くことのなによりもメリットは単体テストが書きやすいことだと思う。

CLOJURE for the BRAVE and TRUEでClojure学習 part3

はじめに

Clojure の学習備忘録。 今回は、Chapter 4: Core Functions in Depthの残り箇所。

今回のあつかうテーマは、以下の通り。

  • 遅延シーケンス
  • 無限シーケンス
  • コレクション関数(conj, into)
  • 代表的な高階関数(apply, partial, complement)

遅延シーケンスといっても、実際に要素にアクセスするまで全く計算しないではなく、 パフォーマンス上先行するいくつか要素も計算してしまうというのは面白い。

Lazy Seqs

mapは他のシーケンス関数と同様、引数のコレクションにseqを適用するだけではなく、遅延シーケンス を返す。
遅延シーケンスとは、要素に実際にアクセスするまで計算を行わないシーケンスのことである。 それに対してシーケンスの全要素が計算されているものは、リアライズシーケンスと呼ぶ。 必要になるタイミングまで計算を遅延させることは、より効率的なプログラミングを可能にし、無限シーケンスが構築できるようになる。

Demonstrating Lazy Seq Effciency

遅延シーケンスについて理解するために、次の例を考えよう。あなたはヴァンパイアを特定するタスクに参加しており、1 億人の容疑者の中からヴァンパイアを特定しないといけない。ヴァンパイアデータベースのサブセットはスタブ化して、次の通りである。

(def vampire-database
  {0 {:makes-blood-puns? false, :has-pulse? true  :name "McFishwich"}
    1 {:makes-blood-puns? false, :has-pulse? true  :name "McMackson"}
    2 {:makes-blood-puns? true,  :has-pulse? false :name "Damon Salvatore"}
    3 {:makes-blood-puns? true,  :has-pulse? true  :name "Mickey Mouse"}})

(defn vampire-related-details
  [social-security-number]
  (Thread/sleep 1000)
  (get vampire-database social-security-number))

(defn vampire?
  [record]
  (and (:makes-blood-puns? record)
        (not (:has-pulse? record))
        record))

(defn identify-vampire
  [social-security-numbers]
  (first (filter vampire?
                  (map vampire-related-details social-security-numbers))))

vampire-related-details は 1 秒でデータベースからレコードを見つける関数である。 vampire? はレコードがヴァンパイアかどうかを判定する関数。 identify-vampireマイナンバーとデータベースのレコードをマッピングして、はじめに一致したレコードを返す。

これらの関数の処理時間を計測するには、 time を使用すればよい。

(time (vampire-related-details 0))
; => "Elapsed time: 1001.373258 msecs"
; => {:makes-blood-puns? false, :has-pulse? true, :name "McFishwich"}

1 行目には、関数処理にかかった時間が出力される。
2 行目は、関数の戻り値である、データベースのレコードが出力される。

遅延が考慮されていない map ならば、全要素に対して vampire-related-details 関数を適用し、その後、 filter を適用することになる。1 億人の容疑者からヴァンパイアを見つけるならば、全要素なめるのに 1 億秒つまり 12 日必要となり、街の半分の人が死んでしまう。しかし Clojuremap は遅延を考慮されているので、そんな心配はいらない。

(time (def mapped-details (map vampire-related-details (range 0 1000000))))
; =>"Elapsed time: 0.053236 msecs"
; => #'user/mapped-details

この例では、 range によって 0 から 999,999 までの遅延シーケンスを生成し、 map が返す遅延シーケンスを mapped-details にバインドしている。しかし、 range が返すシーケンスの全要素に対して vampire-related-details はまだ適用されていないので、処理時間は 12 日もかからず、一瞬で終わる。

遅延シーケンスは、一つはシーケンスの要素をリアライズ方法が記述されたレシピと、今までリアライズされた要素の2つから構成される。 map を使用してもどの要素もまだリアライズされていないが、要素を生成するレシピはある。リアライズされていない要素にアクセスすれば、その度遅延シーケンスはレシピにあるとおり要素を料理して返すことになる。

上の例の場合、 map-detailsリアライズされていないが、map-details の要素にアクセスすれば、要素に対してレシピを適用することになり、検索時間に 1 秒かかるだろう。

(time (first mapped-details))
; => Elapsed time: 32070.326583 msecs"
; => {:makes-blood-puns? false, :has-pulse? true, :name "McFishwich"}

処理に 32 秒かかっている。1 億秒よりかはずっと速くなったが、期待しているよりも 31 秒も多くかかってしまっている。ひとつの要素にアクセスするだけなので 1 秒で済むはずなのになぜだろうか?

それは、Clojure が遅延シーケンスをチャンクに分割するからなのである。1 要素をリアライズするときに先行していくつかの要素もリアライズしている。例の場合だと、1 要素アクセスしても続く 31 要素もリアライズしたので 32 秒かかったのである。 これはパフォーマンス的な理由で行われている。

遅延シーケンスの要素の評価は、初回アクセスのときだけである。よって、もう一回 mapped-details のはじめの要素にアクセスしてもほとんど時間はかからない。

(time (first (mapped-details)))
; => "Elapsed time: 0.022 msecs"
; => {:name "McFishwich", :makes-blood-puns? false, :has-pulse? true}

遅延シーケンスについて整理できたので、はじめに書いたコードが時間かからないことも理解できるだろう

(time (identify-vampire (range 0 1000000)))
; => "Elapsed time: 32019.912 msecs"
; => {:name "Damon Salvatore", :makes-blood-puns? true, :has-pulse? false}

Infinite Sequences

遅延シーケンスの特徴を利用して、無限シーケンスも作成できる。今までシーケンスは有限であったが、Clojure では無限シーケンスを作成できる関数を用意されてる。 repeat はその一つであり、渡された引数の値からなる無限シーケンスを構築する

(concat (take 8 (repeat "na")) ["Batman!"])
; => ("na" "na" "na" "na" "na" "na" "na" "na" "Batman!")

ここでは、文字列 "na" を要素にもつ無限シーケンスを構築している。

repeatedly をつかえば、もっと柔軟な無限シーケンスを構築できる

(take 3 (repeatedly (fn [] (rand-int 10))))
; => (1 4 0)

repeatedly でできた無限シーケンスの要素は、匿名関数 (fn [] (rand-int 10)) の適用結果になる。REPL を実行するたびに、取得要素はランダムに変化する。

遅延シーケンスをリアライズする first や take といった関数は、シーケンスの次の要素がなんであるかを知らないし、取得した要素の次があるならば、ただそれを取得するだけである。

(defn even-numbers
  ([] (even-numbers 0))
  ([n] (cons n (lazy-seq (even-numbers (+ n 2))))))

(take 10 (even-numbers))
; => (0 2 4 6 8 10 12 14 16 18)

ここでは、再帰を利用した無限シーケンスを作成してる。cons は第一引数の要素と第二引数のリストから新たなリストを作成している。

(cons 0 '(2 4 6))
; => (0 2 4 6)

even-numbers では、今の要素と次の要素の計算手順が記された遅延シーケンスをコンス操作の引数に渡すことで 無限シーケンスを構成している。

The Collection Abstraction

シーケンスの抽象化は、構成要素各々に対するオペレーションに関するものに対して、コレクションの抽象化は、データ構造全体に関するものになる。たとえば、 count, empty?, every? とった関数は、個々の要素ではなく、その全体を対する操作になる。

(empty? [])
; => true

(empty? ["no"])
: => false

ここでは代表的で、よくごっちゃになりがちな関数 into, conj をとりあげる。

into

今までみたきたように、シーケンス関数の戻り値はオリジナルのデータ構造ではなく、seq である。それをオリジナルなデータ構造に変換したいとき into がつかえる。

(map identity {:sunlight-reaction "Glitter!"})
; => ([:sunlight-reaction "Glitter!"])

(into {} (map identity {:sunlight-reaction "Glitter!"}))
; => {:sunlight-reaction "Glitter!"}

map 関数は hashmap を seq に変換するが、 into で hashmap に変換し直している。hashmap 以外のデータ構造にも変換できる。

ベクタに変換する

(map identity [:garlic :sesame-oil :fried-eggs])
; => (:garlic :sesame-oil :fried-eggs)

(into [] (map identity [:garlic :sesame-oil :fried-eggs]))
; => [:garlic :sesame-oil :fried-eggs]

セットに変換する

(map identity [:garlic-clove :garlic-clove])
; => (:garlic-clove :garlic-clove)

(into #{} (map identity [:garlic-clove :garlic-clove]))
; => #{:garlic-clove}

into の第一引数が空である必要はない。空ではないと、追記させることができる。

(into {:favorite-emotion "gloomy"} [[:sunlight-reaction "Glitter!"]])
; => {:favorite-emotion "gloomy" :sunlight-reaction "Glitter!"}

(into ["cherry"] '("pine" "spruce"))
; => ["cherry" "pine" "spruce"]

conj

conj 関数もコレクションに要素を追加するが、into と少し異なる。

(conj [0] [1])
; => [0 [1]]

[1] のまま追加されてしまう。

(into [0] [1])
; => [0 1]

一方 into の場合、 1 で追加される conj で同じ結果を得たい場合は、次の通り。

(conj [0] 1)
; => [0 1]

conjスカラー値で受け取るのに対して、 into はコレクションを引数にとるという違いがある。

Function Functions

関数を引数にとったり、戻り値として返す代表的な高階関数として、 apply, partial をとりあげる。

apply

apply は、引数に関数とシーケンスをうけとり、シーケンスを関数の引数として適用するたとえば、 受け取った引数で最大の値を返す max 関数で考えてみる。

(max 0 1 2)

しかし、引数にベクタを受け取った場合、期待どおりにいかない。

(max [0 1 2])
; => [0 1 2]

max に渡すときは、ベクタとしてまとめて渡してはいけず、比較したい値をそれぞれ引数としてわたさないといけない。そして、ベクタとして渡しても動作させるには、 apply が使える。

(apply max [0 1 2])

partial

partial は引数に、一個の関数(f)と 0 個以上の引数(arg)をとり、新しい関数を返す。partial が返す関数を引数(new arg)つきで呼び出すと、partial に渡した f に arg と new arg を渡して適用される。

(def add10 (partial + 10))
(add10 3)
; => 13
(add10 5)
; => 15

(def add-missing-elements
  (partial conj ["water" "earth" "air"]))

(add-missing-elements "unobtainium" "adamantium")
; => ["water" "earth" "air" "unobtainium" "adamantium"]

add10 を呼び出すと、 (+ 10) に与えられた引数を追加して (+ 10 3) が実行される。(+ 10) のように+関数の第一引数を 10 で固定させた関数を作るのが、 partial である。

コンテキストは異なるが関数とその引数の組み合わせが同じものを繰り返し使用するとき、partial は効果的である。

(defn lousy-logger
  [log-level message]
  (condp = log-level
    :warn (clojure.string/lower-case message)
    :emergency (clojure.string/upper-case message)))

(def warn (partial lousy-logger :warn))

(warn "Red light ahead")
; => "red light ahead"

(warn "Red light ahead") を呼び出すことは、 (lousy-logger :warn "Red light ahead") を呼び出すことと同じである。

complement

前節で、1 億人からヴァンパイアを特定する identify-vampire 関数を作成したが、逆に人間を特定するにはどうすればよいだろうか?

(defn identify-humans
  [social-security-numbers]
  (filter #((not (vampire? %)))
          (map vampire-related-details social-security-numbers)))

filter 関数の第一引数は、 #(not (vampire? %)) となっているが、 この述語関数の戻り値を反転させるのが、 complement 関数である。

(defn not-vampire? (complement vampire?))
(defn identify-humans
  [social-security-numbers]
  (filter not-vampire?
          (map vampire-related-details social-security-numbers)))