(define -ayalog '())

括弧に魅せられて道を外した名前のないプログラマ

Boot の REPL 上では Clojure の compile 関数は動かない

タイトルまま。
例えばこういう build.boot を書いているとする。

;; build.boot
(set-env! :resource-paths #{"src"}
          :dependencies '[[org.clojure/clojure "1.6.0"]])

さらにこういうファイルを書いているとする。

;; bar.clj
(ns foo.bar)

(gen-class
 :name foo.bar.Baz
 :methods [[piyo [] void]])

(defn -piyo [this]
  (println "Piyo!!"))

そうして REPL を起動して以下を実行する。

boot.user=> (compile 'foo.bar)
CompilerException java.io.IOException: No such file or directory, compiling:(foo/bar.clj:1:1) 

最初何か悪いことしたかなーって思ったけど、そうでもなさそう。ちょっと github の issue で聞いたら「別に aot タスク使えば良くない?」みたいな感じだったのでたぶん boot だと REPL の中で compile 関数は使わない方がいいんだと思う。

boot.user=> (boot (aot :namespace #{'foo.bar}))
boot.user=> (require 'foo.bar :reload)
boot.user=> (.piyo (foo.bar.Baz.))

こうすれば一応実行は出来ますと。ただし、もし bar.clj を修正して aot でリコンパイルしたとしても、残念ながら修正を REPL 上に反映させることができない。

じゃあ、どうしたらいいかというと、以下のように変更監視用の REPL サーバーを設けてあげれば良いですよと。

$ boot watch aot -n foo.bar repl -s wait &

そして REPL クライアントを起動して普通に使えば良いと。

boot.user=> (require 'foo.bar :reload)
boot.user=> (.piyo (foo.bar.Baz.))

こうすれば bar.clj を修正したらそれに自動的に追随してくれるわけですねー。便利。

Boot で Leiningen のような Checkout dependencies を実現する

これは Leiningen 同様 Boot のデフォルトタスクで実現出来るのですが、ちょこっと手間があるので解説。
Leiningen の Checkout Dependencies については前に触れたのでそちらを参照ください。


とりあえず、ドキュメントを読む。

boot.user=> (doc checkout)
-------------------------
boot.task.built-in/checkout
([& {:keys [help dependencies], :as *opts*}])
  Checkout dependencies task.
  
  This task facilitates working on a project and its dependencies at the same
  time, by extracting the dependency jar contents into the fileset. Transitive
  dependencies will be added to the class path automatically.
  
  You'll need at least two boot instances---one to build the dependency jar and
  the other to build the project. For example:
  
      $ boot watch pom -p foo/bar -v 1.2.3-SNAPSHOT jar install
  
  to build the dependency jar, and
  
      $ boot repl -s watch checkout -d foo/bar:1.2.3-SNAPSHOT cljs serve
  
  to build the project with the checkout dependency [foo/bar "1.2.3"].
  
  Keyword Args:
    :help          bool         Print this help info.
    :dependencies  [[sym str]]  The vector of checkout dependencies.
nil

Leiningen のように前準備として何かが必要というわけではなくて、 Boot のインスタンスがふたつは最低でも必要というのが Leiningen と違うところでしょうか。

実際にドキュメントの中でやっている例をみると

$ boot watch pom -p foo/bar -v 1.2.3-SNAPSHOT jar install

まず、これはライブラリプロジェクト配下で実行されるタスクですね。恐らく build.boot に set-env! を使って :resource-paths なりを指定して src 配下を target に含まれるようにしていると思います*1
watch 以降のタスクはファイルの変更をキャッチする度に実行されるタスクですので、ライブラリのディレクトリ以下でファイルの変更があった場合 pom -> jar -> install という順番で実行されることが分かります。

次にふたつ目の boot のコマンドをみてみます。

$ boot repl -s watch checkout -d foo/bar:1.2.3-SNAPSHOT cljs serve

ライブラリを依存性に含める側のプロジェクトで REPL をサーバーとして起動しています。 watch の後は checkout タスクがきています。これが変更をキャッチしたあとにライブラリをリロードしてくれる部分です。 -d オプションで依存するライブラリを指定します*2。そのあとのふたつは今回あまり関係ありませんが cljs のコンパイルと何故か http サーバーの起動です(このタイミングで起動するのはダメな気が、 watch 以前じゃないと複数起動されませんか?)。

ちなみにここでは言及されていませんが、これに対応するクライアントは例えば次のように起動できます。

$ boot repl -c

で、実際にはこの REPL を使って開発したりすると思いますが、ライブラリの変更を受けたあとに自動的にライブラリがインストールされ、プロジェクトは自動的にその変更を察知してライブラリを更新するまではいいんですが、 REPL のクライアントからプロジェクトのリロードを行う必要があります。
リロードについては前にも言及しているのでそちらを参照してください。

というところで Boot で Checkout dependencies でした。

*1: set-env! の source-paths はコンパイルが必要なソースのパスで resource-paths はコンパイルが不要でそのまま target にいれたいソースコードが置いてあるディレクトリです。今回は aot タスクの実行がないので resource-paths に src などを定義している前提だということが分かります

*2:ここでは上述の foo/bar ライブラリの 1.2.3-SNAPSHOT です

Boot で alembic てきな依存関係の動的解消(?)ぽいことをやる

最近 Boot を本格運用し始めて色々と質問されて調べたりする機会が出来たのでちょこちょこノウハウというか Tips 的な何かを書いていきたい。

alembic とは

github.com

まぁ Clojure やってる人なら使っている人も多いと思いますが、実行中の REPL に対して動的に依存性を追加出来るという代物です。

Boot で同じことをやる

Boot の場合はそれを最初から機能として持ってしまっているので新たに何か必要になるということはないです。

あまりこれはドキュメントの中で明示されていないような気がしますが、実際には set-env! や merge-env! がそもそもの機能として起動中の JVM インスタンスに対して何かしら足したりするようになっているのでできるようです。

(set-env! key val & kvs)
Sets the values for the given keys. Note that this may produce side effects, changes to the JVM's classpath, etc.
Boot Environment · boot-clj/boot Wiki · GitHub

It does amazing magical things like providing classpath isolation so that you can run multiple projects using one JVM and letting you add new dependencies to your project without having to restart your REPL.
Boot, the Fancy New Clojure Build Framework

ここでも REPL を再起動することなく依存性を追加出来ると書いてあるのでまちがいないでしょう。

実際にやる場合は例えば REPL で以下のような set-env! を評価すると良さそうです。

boot.user=> (set-env!
 :dependencies '[[clj-time "0.9.0"]])
       #_=> nil
boot.user=> (require '[clj-time.core :as t])
nil
boot.user=> (t/date-time 1991 05 29)
#<DateTime 1991-05-29T00:00:00.000Z>

もしくは build.boot を開いて編集しているなら set-env! の部分を REPL へと飛ばして評価するのもありでしょう。

defn- のような def- を定義したけど、 private にならないという話

ML で読んでほえーと思った。

Why (defn- ...) but (def ^:private? ...)

元の質問者は def フォームは ^:dynamic? になるの?って聞いてますが、何故かというと彼の書いたマクロ def- が綺麗に動かなかったからです。
こういう感じ。

(defmacro def-
  "Why (defn- private-fn ...) but (def ^:private var ...)?"
  [sym & body]
  `(def ^:private ~sym ~@body))

(macroexpand '(def- blah "foo bar quux")) ;=> (def blah "foo bar quux")

という風にマクロ展開すると ^:private が消えちゃってますね。ちなみにこれ定義後に meta 情報を見ても同様に private のメタ情報は消えています。

(meta #'user/blah) ;=> {:ns #<Namespace user>, :name blah, :column 1, :line 1}

これに対する回答はすごく単純でリーダーマクロは読み込み時に評価されるのであってマクロ展開時じゃないよ、ということ。
つまり ^:private はマクロで操作出来るシンタックスではなくて、マクロで評価するときには存在しないものとなっているということなんですね。
なので、 defn- のような def- を定義したいなら defn- のソースを参考にしたほうがいいよと言われています。

(defmacro defn-
  "same as defn, yielding non-public def"
  {:added "1.0"}
  [name & decls]
    (list* `defn (with-meta name (assoc (meta name) :private true)) decls))

これを受けて def- を定義しなおすならこうなりますと。

(defmacro def-
  "same as def, yielding non-public def"
  ([name] `(def- ~name nil))
  ([name expr]
   (list `def (with-meta name (assoc (meta name) :private true)) expr)))

何がリーダーマクロなのかとかちゃんと理解してないとこういうところでハマるんですね :)

Clojure のこの関数だけ読み込みたくない!!

最近 Clojure で書かれたコード読みまくってるんですけど、まぁ知らないテクニックとか色々あるわけでして。そんな中で知ったひとつのテクニックがこれ。

例えば、 Clojure そのものの compile 関数は使いたくないけど、このネームスペースに compile 関数というのを別途作りたいというとき、 compile2 とか foo-compile とか名前つけるの嫌なので Clojure の compile 関数を無視して新しい compile 関数を定義したいですよね。そのままやると競合が起こっていると警告が出るので、それを防ぐために refer-clojure を使います。

(ns foo.bar
  (:refer-clojure :exclude [compile]))

(defn compile [x]
  (println "New compile function"))

めでたしめでたし。