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マクロの使い方を知っておけばいいのが、
それがどんな役割をしてくれるのを分解して説明してくれる章でした。