Clojureのもう一つの開発環境Bootについて

ウェブ技術 デジタルトランスフォーメーション技術 人工知能技術 自然言語処理技術 セマンティックウェブ技術 深層学習技術 オンライン学習&強化学習技術 チャットボットと質疑応答技術 ユーザーインターフェース技術 知識情報処理技術 推論技術  Clojure プログラミング

今回はClojureのLeiningenと並んで近年よく使われているもう一つの開発環境Bootについて述べる。

Bootとは

まずBootとはとして、Bootのgit pageより「Bootは、ClojureビルドフレームワークとアドホックClojureスクリプト評価器です。Bootは、プロジェクトのコンテキストで実行されるClojureで書かれたスクリプトからClojureプロジェクトを構築するために必要なすべてのツールを含む実行時環境を提供します。

アプリケーションのビルドプロセスは、常に複雑なものになります。例えば、単純なウェブアプリケーションでも、アセットパイプライン、異なる環境へのデプロイ、異なるコンパイラによる複数の成果物のコンパイル、パッケージングなど、多くの統合が必要になることがあります。

ビルドプロセスが複雑になればなるほど、ビルドツールはより柔軟である必要があります。静的なビルド仕様は、プロジェクトが完成に近づくにつれ、あまり役に立たなくなります。Lisperである私たちは、何をすべきかを知っています。Lambdaは究極の宣言型です。

グローバルな設定マップに基づいてプロジェクトを構築する代わりに、bootはClojureで書かれたビルドスクリプトが評価される実行環境を提供します。このスクリプト、つまりチューリング完全なビルド仕様が、プロジェクトをビルドするのです。」

Leiningenも開発ツールとしては優れたものだが、特にwebページシステム開発のような複雑なアプリケーションでは設定が複雑になり例えばLeningenのprojectファイルが非常に冗長なものとなる。

これに対して、Bootでは、”build.bootというファイルを作成し、使用するライブラリやタスクを書く、Ruby の Gemfile や NodeJS の package.json のような役割をするファイルとなる。

Bootのインストールは各種パッケージマネージャを使って行う。

  • Hombrew. “brew install boot-clj”
  • nix. “nix-env -i boot”
  • aur.  “yaourt –noconfigrm -Syy boot”
  • docker  clojureイメージにbootタグを使ってインストールする

Bootがインストールできたかどうかは”boot -V”にて確認できる。

タスク

leiningenにないbootの機能の一つがタスク管理の機能となる。タスク機能はmake、rake、gruntその他のビルドツールと同様に定義でき、いくつかの中間プログラム(make, rake,Boot)によってディスパッチされたコマンドラインオプションを取る名前付き操作となる。

Bootは、deftaskマクロを使用して名前付き操作とそのコマンドラインオプションを簡単に定義できるディスパッチプログラムおよびClojureライブラリを提供する。ここでbraveclojure.comでのチュートリアルである”My pants are on fire”のprintタスクについて転記する。

まず作業として

  1. Bootのインストール(前述)
  2. “boot-walkthrough”という名前の新しいディレクトリを作成する
  3. そのディレクトリに移動し、”build.boot”という名前のファイルを作成し、以下のように記述する。
(deftask fire
  "Prints 'My pants are on fire!'"
  []
  (println "My pants are on fire!"))

ここでコマンドラインで”boot fire”を入力すると”Prints ‘My pants are on fire!'”が出力される。

この例ではleiningenのようなproject.clj、ディレクトリ構造、また名前空間の指定はなしで動作させることができる。

次にこの例を拡張して、コマンドラインオプションの書き方を実演する。

(deftask fire
  "Announces that something is on fire"
  [t thing   THING str  "The thing that's on fire"
   p pluralize  bool "Whether to pluralize"]
  (let [verb (if pluralize "are" "is")]
    (println "My" thing verb "on fire!")))

ここで以下のように入力する。

boot fire -t heart
# => My heart is on fire!

boot fire -t logs -p
# => My logs are on fire!

このFireタスクの改良により、thingとpluralizeという2つのコマンドラインオプションが導入されている。どちらのオプションもDSL(domain-specific language)を使って定義されており、DSLはそれ自身のトピックだが、簡単に言うと、より大きなプログラムの中で、狭く定義された領域(オプションの定義のように)のためにコンパクトで表現力のあるコードを書くために使用できるミニ言語のことを指す。

オプションthingでは、tがその短い名前、thingが長い名前を指定する。strはオプションの型を指定し、Bootはそれを使って引数を検証し、変換します。”the thing that’s on fire “は、オプションのドキュメントとなる。タスクのドキュメントはターミナルで “boot fire(task name) -h “とすると見ることができまる。

Options:
  -h, --help         Print this help info.
  -t, --thing THING  THING sets the thing that's on fire.
  -p, --pluralize    Whether to pluralize

ここで、THING は optarg であり、このオプションが引数を必要とすることを示している。オプションを定義するときに optarg を含める必要はない(pluralize オプションには optarg がないことに注意が必要となる)。optarg はオプションのフルネームと一致する必要はない。 THING を BILLY_JOEL などに置き換えても、タスクは同じように動作する。また、optargを使用して、複雑なオプションを指定することもできる。(この件に関する Boot のドキュメントは https://github.com/boot-clj/boot/wiki/Task-Options-DSL#complex-options を参照のこと)。基本的に、複雑なオプションでは、オプション引数をマップ、セット、ベクター、さらにはネストされたコレクションとして扱うように指定でき、これはかなり強力なものとなる。

Bootは、Clojureでコマンドラインインターフェイスを構築するために必要なすべてのツールを提供する。

REPL

Boot には、REPL タスクを含む多くの便利なビルトインタスクが付属している。boot repl を実行すると、REPLが起動する。Boot REPL は Leiningen と似ていて、プロジェクトのコードを読み込んで、それを評価できるようにしたものとなる。タスクしか書いていないので、今まで書いてきたプロジェクトには当てはまらないと思うかもしれないが、実は REPL でタスクを実行できる。オプションは文字列で指定できる。

boot.user=> (fire "-t" "NBA JAM guy")
My NBA JAM guy is on fire!
nil

オプションの値がオプションの直後に来ることに注意が必要となる。また、キーワードを使ってオプションを指定することもできる。

boot.user=> (fire :thing "NBA Jam guy")
My NBA Jam guy is on fire!
nil

また、オプションを組み合わせることも可能となる。

boot.user=> (fire "-p" "-t" "NBA Jam guys")
My NBA Jam guys are on fire!
nil
boot.user=> (fire :pluralize true :thing "NBA Jam guys")
My NBA Jam guys are on fire!
nil
構成と調整

これだけの機能であれば、Bootはかなり複雑で膨らんだツールとなってしまう。これが他のツールと異なる点はタスクの生成方法となる。例としてRakeの呼び出しの例を示す(RakeはRubyのビルドツールとなる)

rake db:create d{:tag :a, :attrs {:href "db:seed"}, :content ["b:migra"]}te db:seed

このコードは、Railsプロジェクトで実行されると、データベースを作成し、その上でマイグレーションを実行し、シードデータを投入する。しかし、注目すべきは、Rakeはこれらのタスクが互いに通信するための方法を提供していないことにある。複数のタスクを指定することは、rake db:create; rake db:migrate; rake db:seedを実行する手間を省くための便宜的なものでしかない。タスクBの中でタスクAの結果にアクセスしたい場合、ビルドツールは手助けをしてくれない。通常、タスクAの結果をファイルシステム上の特別な場所に移動し、タスクBがその特別な場所を読むようにすることでこれを行う。これは、ミュータブル変数やグローバル変数を使ったプログラミングのように見えるし、同じようにもろいものとなる。

ハンドラーとミドルウェア

Bootは、タスクをミドルウェアファクトリーとして扱うことで、このタスクコミュニケーションの問題に対処している。ミドルウェアとは、プログラマがドメイン固有の機能パイプラインを柔軟に作成するために遵守する一連の慣習となる。

ミドルウェアのアプローチがありふれた関数合成とどう違うかを理解するために、日常的な関数を合成する例を挙げる。

(def strinc (comp str inc))
(strinc 3)
; => "4"

この関数の構成は、非常にシンプルなものとなる。2つの関数があり、それぞれが独自の処理を行っていて、それが1つに合成されているものとなる。

ミドルウェアは関数の合成に新たなステップを導入し、関数のパイプラインをより柔軟に定義することができる。先ほどの例で、任意の数字に対しては“I don’t like the number X”(Xという数字は嫌いだ)と返し、それ以外のものに対しては文字列化した数字を返したいとする。その方法は次の通りとなる。

(defn whiney-str
  [rejects]
  {:pre [(set? rejects)]}
  (fn [x]
    (if (rejects x)
      (str "I don't like " x)
      (str x))))

(def whiney-strinc (comp (whiney-str #{2}) inc))
(whiney-strinc 1)
; => "I don't like 2"

ここで、もう一歩踏み込み、そもそもincを呼び出すかどうかを決めたい場合はどうすればいいのかについて考えると以下のようになる。

(defn whiney-middleware
  [next-handler rejects]
  {:pre [(set? rejects)]}
  (fn [x]
➊     (if (= x 1)
        "I'm not going to bother doing anything to that"
        (let [y (next-handler x)]
          (if (rejects y)
            (str "I don't like " y)
            (str y))))))

(def whiney-strinc (whiney-middleware inc #{2}))
(whiney-strinc 1)
; => "I'm not going to bother doing anything to that"

ここでは、compを使って関数のパイプラインを作る代わりに、パイプラインの次の関数をミドルウェア関数の最初の引数として渡している。この場合、next-handlerとしてwhiney-middlewareに第1引数としてincを渡している。whiney-middlewareはincをクローズして、それを呼ぶかどうかを選択する能力を持った無名関数を返す。この選択は➊で見ることができまる。

ミドルウェアはハンドラを第一引数に取り、ハンドラを返す。この例では、whiney-middleware は最初の引数 inc としてハンドラを受け取り、別のハンドラ、x を唯一の引数とする無名関数を返す。ミドルウェアはrejectsのような、設定として機能する余分な引数を取ることもできる。その結果、ミドルウェアが返すハンドラは、(設定のおかげで)より柔軟に振る舞うことができ、(次のハンドラを呼び出すかどうかを選択できるので)関数パイプラインをよりコントロールすることができるようになる。

タスクはミドルウェアのファクトリー

Bootでは、ミドルウェアの設定とハンドラの作成を分離することで、関数構成をより柔軟にするこのパターンをさらに一歩進めている。まず、n個の設定引数を取る関数を作成する。これがミドルウェアファクトリーで、ミドルウェア関数を返す。ミドルウェア関数は、先ほどの例と同じように、次のハンドラという引数を1つだけ受け取り、ハンドラを返す。ここで、以下のようなミドルウェアファクトリの例を述べる。

(defn whiney-middleware-factory
  [rejects]
  {:pre [(set? rejects)]}
  (fn [handler]
    (fn [x]
      (if (= x 1)
        "I'm not going to bother doing anything to that"
        (let [y (handler x)]
          (if (rejects y)
            (str "I don't like " y " :'(")
            (str y)))))))

(def whiney-strinc ((whiney-middleware-factory #{3}) inc))

見ての通り、このコードは前述の例とほぼ同じとなる。変更点は、一番上の関数 whiney-middleware-factory が rejects という 1 つの引数しか受け取らないようになったこととなる。これは無名関数であるミドルウェアを返す。ミドルウェアは 1 つの引数であるハンドラを期待する。残りのコードは同じとなる。

Boot では、タスクはミドルウェア ファクトリーとして機能することができる。これを示すために、fireタスクをwhatとfireの2つのタスクに分割してみる (下図コード参照)。whatでは、オブジェクトとそれが複数あるかどうかを指定でき、fireでは、それが燃えていることをアナウンスする。これはモジュール式ソフトウェアエンジニアリングの例となる。なぜなら、gnomesのような他のタスクを追加して、あるものがgnomesで溢れかえっていることを知らせることができ、これは客観的にも同様に有用だからである。(練習として、gnomeタスクを作成してみると。fireがそうであるように、whatタスクと合成されるはずである)。

(deftask what
  "Specify a thing"
  [t thing     THING str  "An object"
   p pluralize       bool "Whether to pluralize"]
  (fn middleware [next-handler]
➊     (fn handler [fileset]
      (next-handler (merge fileset {:thing thing :pluralize pluralize})))))

(deftask fire
  "Announce a thing is on fire"
  []
  (fn middleware [next-handler]
➋     (fn handler [fileset]
      (let [verb (if (:pluralize fileset) "are" "is")]
        (println "My" (:thing fileset) verb "on fire!")
        fileset))))

これをReplで実行すると、次のようになる。

boot.user=> (boot (what :thing "pants" :pluralize true) (fire))
My pants are on fire!
nil
ファイルセット

先ほど、ミドルウェアはドメイン固有の関数パイプラインを作成するためのものだと述べた。つまり、各ハンドラはドメイン固有のデータを受け取り、ドメイン固有のデータを返すことを期待する。例えばRingの場合、各ハンドラはHTTPリクエストを表すリクエストマップを受け取ることを想定しており、それは次のようなものとなる。

{:server-port 80
 :request-method :get
 :scheme :http}

各ハンドラは、すべてのクエリ文字列とPOSTパラメータ素敵なClojureマップを持つ:paramsキーを追加することで、次のハンドラに渡す前に、このリクエストマップを何らかの方法で変更することを選択できる。リングハンドラは、キー:status:headers、および:bodyで構成されるレスポンスマップを返し、各ハンドラは親ハンドラに返す前に、このデータを何らかの方法で変換できる。

ブートでは、各ハンドラはファイルセットを受信して返す。ファイルセットの抽象化により、ファイルシステム上のファイルを不変のデータとして扱うことができ、プロジェクトを構築することはファイル中心であるため、これはビルドツールにとって大きな革新となる。たとえば、プロジェクトは一時的な中間ファイルをファイルシステムに配置する必要があるかもしれない。通常、ほとんどのビルドツールでは、これらのファイルは特別な名前の場所、例えばproject/target/tmpに配置される。これの問題は、project/target/tmpが事実上グローバル変数であり、他のタスクが誤ってそれを取り消してしまう可能性があることとなる。

Bootのファイルセット抽象化は、ファイルシステムの上に間接レイヤーを追加することでこの問題を解決する。タスクAがファイルXを作成し、ファイルセットに格納するように指示するとする。舞台裏では、ファイルセットはファイルを匿名の一時ディレクトリに保存する。その後、ファイルセットはタスクBに渡され、タスクBはファイルXを変更し、ファイルセットに結果を保存するように要求する。舞台裏では、新しいファイル「ファイルY」が作成され、保存されるが、ファイルXは手つかずのままである。タスクBでは、更新されたファイルセットが返される。これは、マップでassoc-inを行うのと同じこととなる。タスクAは、元のファイルセットと参照するファイルにアクセスできる。

そして、上記のコードのwhatfireタスクでこのファイル管理機能も使用していないが、Bootがタスクを作成すると、ハンドラがファイルセットレコードを受信して返すことを期待している。したがって、タスク間でデータを伝えるには、ファイルセットレコードにこっそり追加する(merge fileset {:thing thing :pluralize pluralize})

Boot の背後にある概念は以上のようになる。それら以外にも、Boot には、set-env! や task-options! など、実際に使用する際にプログラミングを容易にする他の機能もたくさんある。例えば、クラスパスを分離して、1つのJVMで複数のプロジェクトを実行できるようにしたり、REPLを再起動せずにプロジェクトに新しい依存関係を追加できるようにするなど、様々な機能を備えている。

コメント

  1. […] Clojureのもう一つの開発環境Bootについて […]

タイトルとURLをコピーしました