プロジェクトを作らないClojure

最近Go言語で遊んでます。文法がシンプルなのも気に入ってますが、適当な場所に`hello.go`を作成して、`go build hello.go`とするだけで実行ファイルが出来上がる、という手軽さも中々心地よいです。「簡単に取り掛かることができる」というのも楽しくプログラミングするうえで重要な要素なんだなと実感しました。

一方のClojureは、まずプロジェクトを作って次に`project.clj`を書いて…と作業にかかるまでに一手間です。もっと手軽に書けたら良いのに…と調べていたら、Bootが使えそうでした。


# Bootを使ったスクリプト

Clojureのビルドツールに[Boot](http://boot-clj.com/)というものがあります[^1]。Leiningenのようにプロジェクトを作って開発することもできますが、ホームページの機能紹介には

> **Write quick Clojure scripts**
> without a project context using shebang support. 

とあり、プロジェクトがなくても使えるようです。

早速試してみましょう。まず、**適当な場所で**次のファイルを作成します。

[^1]: Bootのホームページでは、Bootを指して「It's not a build tool - it's build tooling. 」と言っています。ニュアンスの違いが分かるような、分からないような…

```
> touch hello.clj
```

ファイルの中身は以下のとおりとします。

```clojure:hello.clj
#!/usr/bin/env boot

(ns boot.user
  (:require [clojure.string :as str]))

(defn -main []
  (println (str/upper-case "hello world!")))
```

一行目にShebangを書いていること以外は普通のClojureのコードですね。実行方法は以下のとおりです。

```
> chmod +x hello.clj
> ./hello.clj
HELLO WORLD!
```

なお、Bootスクリプトは`boot.user`ネームスペースで実行されますが`ns`で明示する必要はありません。`-main`も省略可能です。この方がスクリプト言語っぽく見える気もします。

```clojure:boot_scrape.clj（nsと-main省略版）
#!/usr/bin/env boot

(require '[clojure.string :as str])

(println (str/upper-case "hello world!"))
```


# ライブラリの使用

ライブラリを使用する場合はBootの`set-env!`関数で依存関係を記述します。以下は、[enliveのチュートリアル](https://github.com/swannodette/enlive-tutorial)を参考にした、Hacker Newsの記事タイトルを取得するスクリプトです。

```clojure:boot_scrape.clj
#!/usr/bin/env boot

(set-env! :dependencies '[[enlive "1.1.6"]])

(ns boot.user
  (:require [net.cgrand.enlive-html :as html]))

(def ^:dynamic *base-url* "https://news.ycombinator.com/")

(defn fetch-url [url]
  (html/html-resource (java.net.URL. url)))

(defn hn-headlines-and-points []
  (map html/text
       (html/select (fetch-url *base-url*)
                    #{[:a.storylink] [:span.score]})))

(defn print-headlines-and-points [p]
  (doseq [line (map (fn [[h s]] (str h " (" s ")"))
                    (partition 2 (hn-headlines-and-points)))]
    (println line)))

(defn -main []
  (print-headlines-and-points))
```

Shebangに続く`(set-env! :dependencies '[[enlive "1.1.6"]])`で依存関係を記述しました。これで`:require`のフォームで通常通りライブラリを読み込めるようになります。また、依存するライブラリはスクリプトの起動時に自動で取得してくれるので、先ほどと同様に実行できます。

```
> chmod +x boot_scrape.clj
> ./scrape_boot.clj
The 3D Printing Revolution That Wasn’t (79 points)
Announcing Bookdown: Authoring Books and Technical Documents with R Markdown (23 points)
Amazon States Language – A JSON-based language to describe state machines (223 points)
・・・（以下略）・・・
```

簡単ですね。


# ファイル分割

プログラムがある程度大きくなってきたらファイルを分割したくなると思います。`-main`関数以外を別ファイルに移動してみましょう。

```
> mkdir lib
> touch lib/hn_scrape.clj
```

```clojure:lib/hn_scrape.clj
(ns hn-scrape
  (:require [net.cgrand.enlive-html :as html]))

(def ^:dynamic *base-url* "https://news.ycombinator.com/")

(defn fetch-url [url]
  (html/html-resource (java.net.URL. url)))

(defn hn-headlines-and-points []
  (map html/text
       (html/select (fetch-url *base-url*)
                    #{[:a.storylink] [:span.score]})))

(defn print-headlines-and-points []
  (doseq [line (map (fn [[h s]] (str h " (" s ")"))
                    (partition 2 (hn-headlines-and-points)))]
    (println line)))
```

```clojure:boot_scrape.clj
#!/usr/bin/env boot

(set-env! :dependencies '[[enlive "1.1.6"]]
          :source-paths #{"lib"})

(ns boot.user
  (:require [hn-scrape :refer [print-headlines-and-points]]))

(defn -main []
  (print-headlines-and-points))
```

ここでは新たに`lib`というディレクトリを作成し、外に出した関数はその中の`hn_scrape.clj`というファイルに記述しました。名前空間は`hn-scrape`です。`boot_scrape.clj`の方では、`set-env!`に`:source-paths`を追記して、読み込みたいファイルのあるディレクトリを指定します。そうすることで、`:require`は`:source-paths`から`hn-scrape`名前空間を見つけてくれます。実行方法は先ほどまでと同様で`./boot_scrape.clj`とするだけです。



# コマンドライン引数

コマンドラインから引数を与えることもできます。コマンドラインからの引数は`-main`関数の`args`に入っています。例えばこんな感じです。

```clojure:arg_sample.clj
#!/usr/bin/env boot

(defn -main [& args]
  (println args))
```

```
> chmod +x arg_sample.clj
> ./arg_sample.clj hoge fuga piyo
(hoge fuga piyo)
```

これを使っても良いのですが、Bootには引数をいい感じに扱ってくれるマクロ`defclifn`があります。利用例を見てみます。

```clojure:arg_sample.clj
#!/usr/bin/env boot

(require '[boot.cli :refer [defclifn]])

(defclifn -main
  "This is a sample script."
  [n number VALUE int "the number"]
  (println number))
```

```
> ./arg_sample.clj -n 10
10
> ./arg_sample.clj --number 10
10
> ./arg_sample.clj -h
This is a sample script.

Options:
  -h, --help          Print this help info.
  -n, --number VALUE  VALUE sets the number.
>
> ./arg_sample.clj -n hoge
        clojure.lang.ExceptionInfo: option :minimum-score must be of type int
・・・（以下略）・・・
```

`-main`関数の引数に`n number VALUE int "the number"`とありますが、この5つで1セットです。最初の`n`はオプションの指定方法です。2つめの`number`は引数で与えた値が束縛されます（`--number`というオプションにもなります）。3つめの`VALUE`は「この-nオプションは値をもつよ。フラグじゃないよ。」ということを示します（逆に、フラグの動作をするオプションを定義する場合は省略します）。

特に自分で定義しなくても`-h`（と`--help`）オプションが追加され、型チェックもしてくれます。この型チェックに使われるのが4つめの`int`で、5つめは`-h`オプションのヘルプドキュメントになります[^2]。

[^2]: これらの引数に関する詳しい情報は、Boot Wikiの[Task Options DSL](https://github.com/boot-clj/boot/wiki/Task-Options-DSL)を参照してください。

それでは、コマンドライン引数を使って、先ほどの`boot_scrape.clj`を与えたスコア以上の記事のみ抽出するように修正してみます。

```clojure:lib/hn_scrape.clj
(ns hn-scrape
  (:require [net.cgrand.enlive-html :as html]))

(def ^:dynamic *base-url* "https://news.ycombinator.com/")

(defn fetch-url [url]
  (html/html-resource (java.net.URL. url)))

(defn hn-headlines-and-points []
  (map html/text
       (html/select (fetch-url *base-url*)
                    #{[:a.storylink] [:span.score]})))

(defn score-gt? [[_ s] minimum-point]
  (let [this-point (Integer/parseInt (first (clojure.string/split s #" ")))]
    (> this-point minimum-point)))

(defn print-headlines-and-points [minimum-point]
  (doseq [line (->> (partition 2 (hn-headlines-and-points))
                    (filter #(score-gt? % minimum-point))
                    (map (fn [[h s]] (str h " (" s ")"))))]
    (println line)))
```

```clojure:boot_scrape.clj
#!/usr/bin/env boot

(set-env! :dependencies '[[enlive "1.1.6"]]
          :source-paths #{"lib"})

(ns boot.user
  (:require [hn-scrape :refer [print-headlines-and-points]]
            [boot.cli :refer [defclifn]]))

(defclifn -main
  "This is a sample script."
  [m minimum-score VALUE int "Minimum score to list"]
  (if-not minimum-score
    (do (boot.util/fail "The -m/--minimum-score option is required!") (*usage*))
    (print-headlines-and-points minimum-score)))
```

```
> ./boot_scrape.clj -m 100
Amazon States Language – A JSON-based language to describe state machines (223 points)
Don’t prematurely obsess on a single “big problem” or “big theory” (102 points)
Reflections on Rusting Trust (231 points)
・・・（以下略）・・・
```

`lib/hn_scrape.clj`の修正は本質じゃないので省きますが、`boot_scrape.clj`の方には`if-not`から始まる引数チェックを加えました。`(*usage*)`関数は先ほどのヘルプと同じ動きをします。

# 終わりに

とりあえず以上を押さえておけば僕の用途では問題なさそうです。Bootを使ってこれからも楽しくClojureを書きたいと思います！