cider での TDD っぽいワークフロー
ワークフロー
- REPL 起動(
C-c M-j
) ※ 1 回のみ - 何か関数書く
- REPL 上でテスト
- ok なら保存・リロード(
C-x C-s
,C-c C-n
,C-c C-k
) - REPL 上で実行したコードと結果を = とした is フォームにコピペし、
適当な名前を付けて保存・リロード(
C-x C-s
,C-c C-n
,C-c C-k
) C-c ,
でテスト
2-6 を繰り返す。
TDD と言うよりは REPL で試した結果を残すようにすると言った方が正しいかも。
改善したい部分や気になる所
On Lisp -> Clojure へ移植: 14.1~14.4
14.1 関数の構築
関数合成をマクロ化する意味が結局良く分からない。 funcall を暗に呼んでいて省略できるのがメリットくらい?それにしてもまだ呼び方が
(fn (compose list 1+ truncate))
と、Clojure の comp を知っていると使う側が fn を改めて呼ぶ必要があんのか と言う気もしてマクロ版作る気にならない。
ただ comp を作ってみるのは面白そうなので関数版で見ないで作ってみた。 4Clojure であったようななかったような、忘れた。
(defn compose ([] identity) ([f] f) ([f g] (fn [& args] (f (apply g args)))) ([f g h] (fn [& args] (f (g (apply h args))))) ([f1 f2 f3 & fs] (reduce (fn [g f] (comp f g)) (concat (reverse fs) [f3 f2 f1]))))
では大元の comp はどうなんだろと見てみたらちょっと長めだったので (ソースのリンク)https://github.com/clojure/clojure/blob/028af0e0b271aa558ea44780e5d951f4932c7842/src/clj/clojure/core.clj#L2391 だけ載せる。
関数返す所で設定してる仮引数も引数多重定義してる他はそう外れた作り方じゃない感じだったかなと。
この後見ると、On Lisp で作ってる fn マクロはもう少し柔軟な合成の仕方ができるみたいだけど やっぱりマクロ版はあんま作る気にならん。comp だってそうしょっちゅう使ってるわけじゃないしなあ。
14.2 Cdr 部での再帰
14.3 部分ツリーでの再帰
すんません、これらは自分にゃ難し過ぎたのでギブ。 一つ敢えて言うとするなら、ちょっと抽象化しすぎじゃね?って気が。
14.4 遅延評価
そういや Clojure にも標準で delay/force あったなあ。 とりあえず図 82 にあるものを移植してみる。
(defrecord Delay [forced closure]) (defmacro delay [& expr] (let [self (gensym)] `(let [~self (Delay. :unforced (fn [] ~@expr))] ~self))) (defn force [x] (if (= (type x) Delay) (if (= (:forced x) :unforced) ((:closure x)) (:forced x)) x))
defconst は (def ^:const unforced (gensym))
としても良かったが、
Clojure だと Keyword を定数のように扱えるので特に設けなかった。
defstruct は defrecord で。まんま defstruct もあるんだが、 使ってるサンプルあんま見かけないし Clojure の方では試した事もない。 ちなみに defrecord で定義した名前は Clojure 標準も Delay と同じ名前。 標準の方は defrecord で作ったものではないみたいだが。
えーと、項目的に豪快にすっ飛ばしたけど、
第 14 章 関数を返すマクロ
はここまで。
On Lisp -> Clojure へ移植: 13.1~13.3
13.1 アナフォリックな変種オペレータ
p116
aif を初めとした図 72 ひと通り。
(defmacro aif [test-form then-form & [else-form]] `(let [~'it ~test-form] (if ~'it ~then-form ~else-form))) (defmacro awhen [test-form & body] `(aif ~test-form (do ~@body))) (defmacro awhile [expr & body] `(loop [~'it ~expr] (if (not ~'it) ~@body (recur ~expr)))) (defmacro aand [& args] (cond (empty? args) true (empty? (next args)) (first args) :default `(aif ~(first args) (aand ~@(next args))))) (defmacro acond [& clauses] (if (empty? clauses) nil (let [cl1 (subvec clauses 0 2) sym (gensym)] `(let [~sym ~(cl1 0)] (if ~sym (let [~'it ~sym] ~@(cl1 1)) (acond ~@(subvec clauses 2)))))))
aand はややこしいので本書の擬似コードを使って展開形を見てみる。
(pprint (clojure.walk/macroexpand-all '(aand (owner x) (address it) (town it)))) ;=> (let* [it (owner x)] ; (if it (let* [it (address it)] ; (if it (town it) nil)) ; nil))
it の束縛関係がよく分からんので、展開形しか確認できない本書の擬似コードでなく 実際に動くコードで確かめてみる。
(aand (inc x) (- it 10) (- it 100)) ;=> 99
it は直近の式に束縛されるみたい。
acond は一応 Clojure の cond に近くなるよう subvec で展開するように書いてみたが これの動作は未確認。
続いて図 74 にあるやつ。alambda は Clojure に合わせて afn と言う名前にした。 block は Clojure で言う名前が付けられる do みたいなもんだが、標準では用意されてない。 ここでは名前指定する tag は省略した。
(defmacro afn [parms & body] `(letfn [(~'self ~parms ~@body)] ~'self)) (defmacro ablock [& args] `(do ~((afn [args] (case (count args) 0 nil 1 (first args) `(let [~'it ~(first args)] ~(self (next args))))) args)))
((afn [x] (if (= x 0) 1 (* x (self (dec x))))) 10) ;=> 3628800
ここでも JVM の制限で recur を使わないとすぐ StackOverflowError になる。
((afn [x] (if (= x 0) 1 (* x (self (dec x))))) 100M) ;=> StackOverflowError clojure.lang.PersistentHashMap$BitmapIndexedNode.index (PersistentHashMap.java:580)
と言うか、Clojure だと標準の fn で名前を指定する事もできる為、 afn のようなマクロを特別に用意する必要もなかったりする。 依然 StackOverflowError はつきまとうがそれはまた別の話。
((fn fact [x] (if (= x 0) 1 (* x (fact (dec x))))) 10)
なので本文に出ている count-instances は Clojure だと afn 無しにこう書ける。
(defn count-instances [obj lists] (map (fn self [list] (if list (+ (if (= (first list) obj) 1 0) (self (next list))) 0)) lists))
ablock は、、、名前が付けられず、return-from も使えないので Clojure では定義する意味がまるでないかな。一応動くが。
13.2 失敗
p120
いきなり相違点。Clojure では nil = 空リストではない。移植でさっきから使ってる cdr と似たような next を使ってみると、一見同じじゃないかと思える。
(next '(a)) ;=> nil
となるが、それは next が nil を返してるだけで、比較してみれば分かる。
(= () nil) ;=> false
Clojure には next とほぼ似たような rest もあるが、こちらは () が返る。
(rest '(a)) ;=> ()
この辺りは Clojure 界隈では falsy なんちゃらと言われたりしてて、Joy of Clojure でも確か見た気がする。
正直使い分け方はよー分からん。自分は falsy になる nil が欲しい事が多いので もっぱら next を使うけどあんまり明確な基準はない。
また、() は falsy ではなく、次を実行してみればその事が分かる。
(if () 'a) ;=> a
nil の他に真偽値も別にあり、他言語と同様に true/false となる。
(= 1 0) ;=> false
nil と false を分けたのはおそらく nil と言う値が欲しい場合と 偽値としての nil が欲しい場合を分けたくてこうしたのかな? On Lisp でもちょうどここでこの件について語られてるが 解決法は Clojure とは異なる。詳しくは本書でどうぞ。
p122
図 74 は 図 72 にあるアナフォリックマクロの多値版があるが、前にも書いたように 個人的に多値にあまり意味を見出だせないので飛ばす。
図 75 も特段何が便利になるのか今ひとつピンと来ないので飛ばす。
13.3 参照の透明性
ここはほぼ読むだけでサンプルは無し。
第 13 章 アナフォリックマクロ
はここまで。
codox を使った GitHub へのもう少し手抜きな API doc 公開方法メモ
GitHub へ API doc を deploy する方法としては既に codox の HowTo としてもあるんだが、ここではもう少し手抜きな方法をメモしておく。GitHub Pages となるリポジトリは既に作っているものとする。
- lein doc してできた doc フォルダを普通に git commit&push
- codox でドキュメント作ってるプロジェクトの GitHub リモートリポジトリで gh-pages の名前のブランチを切る
- どこでやるかは公式の
Create a gh-pages branch
参照
- どこでやるかは公式の
- 10 分ほど待つ
- http://your-account.github.io/your-project/doc/ にアクセス
後はプロジェクトリポジトリの README.md とか適当な所にリンクを張れば終了。
これだけ。API doc の URL がプロジェクト名直下でないとヤダとかでなければ Deploying to GitHub Pages に書かれているようなコマンドライン打つ必要が無い分だけちょっと楽。
git とコマンドライン少し詳しければこの手順取らなくてもバッチなりシェルスクリプトなりで 対応可能かもしれんけどまあ一つの方法として参考になれば。
cider を 0.8.1 にアップデートしてみた
cider が 0.8.1 になってた のでアップデートしようとしたが割と手間取ったのでその手順をメモしておく。
今回は 0.7.0 の時と違い単なるインストール記事で使用感までは書いてない。 個人的に色々気になるのはあるけども今回は 元Changelog。 を見てもらうと言う事で。
手順概要
cider-nreplのアップデート
- profiles.clj の
[cider/cider-nrepl "0.7.0"]
記述を[cider/cider-nrepl "0.8.1"]
に変更 - コマンドラインで適当な leiningen プロジェクト下に移動して
lein deps
el-getレシピの追加・変更
以下内容の queue.rcp をローカルレシピパスに追加する。
(:name queue :description "Queue data structure" :type elpa)
ローカルレシピパスの cider.rcp を以下のように変更する。
(:name cider :description "CIDER is a Clojure IDE and REPL." :type github :pkgname "clojure-emacs/cider" :checkout "14a7a7f42af5c8f4945920a03c9e2d7855976061" ; 0.8.1 :depends (dash queue clojure-mode pkg-info))
:checkout
に 0.8.1 の commit ハッシュ識別子?に相当する文字列追加。
ハッシュ識別子自体は github の releases を見ればあるのでそこから持ってきた。
:checkout
を入れないと repl 起動時 cider 側が 0.8.2-SNAPSHOT だよと Warning が出る。
この辺りはel-get 公式へ既に merge されてるっぽくて、 多分こちらの環境の関係で通常手順で el-get をインストールしてないのが影響している気がするがちゃんとは調べてない。
el-get各パッケージアップデート
M-x
el-get-install
RET
queue
M-x
el-get-update
RET
cider
M-x
el-get-update
RET
clojure-mode
repl起動確認
適当な leiningen プロジェクトの project.clj 開いて C-c
M-j
として repl に 0.8.1 と出てれば OK
所要時間: 2h
MELPA リポジトリを追加すれば良さそうなのは見たんだけど上手く行かなかったのでこちらは割愛。
もし、proxy 内で使ってて通常(ってあるのか知らんけど)手順で上手く行かなかった人は 参考になるかもと思い記事にした。
とりあえずはこのまま使ってみる。仮に 0.8.1 がまだまだ不安定だったとしても、今回のアップデート手順を同じようにしてダウングレードもできそうなので。
グチ
Github とかの俺の知識不足もあるんだろうけどやっぱめんどくさいよなあ。 主に今回引っかかったのは 3 つ。
clojure-mode アップデート忘れ
leiningen の感覚で依存してるのもアップデートしてくれるのかと思いきやしてくれず、 最初に cider-jackin した時何かヘンなエラー出た。そういやレシピファイルの :depends には 確かにバージョン指定なかったけどさ。
cider と nrepl-cider のバージョンミスマッチ
一つ前のを解消した後に cider-jackin すると、cider 側が 0.8.2-SNAPSHOT だよと Warning が出てた。 まあこれは解消せずとも使えそうな気はしたんだけど何となく気持ち悪かったので。
:checkout 調べるのに時間かかった。el-get-custom.el に version or checksum 的な値を 指定できるような事が書いてあったので、最初試しに "0.8.1" としてみたがダメだった。
今は Cask ってのがベター?
上二つが上手く行かずググってた時に見つけた情報。あーまた別のパッケージマネージャ的なもの? 日付的に割と最近乗り換えたというのを見て気にはなったものの今回は解決できたのでこちらを 試すのは保留した。