日曜プログラミング

休日趣味でやってるプログラミング関連記事をダラダラと

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 はよりそれらしい再帰は確かに書けるが、

((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 公開方法メモ

GitHubAPI doc を deploy する方法としては既に codox の HowTo としてもあるんだが、ここではもう少し手抜きな方法をメモしておく。GitHub Pages となるリポジトリは既に作っているものとする。

  1. lein doc してできた doc フォルダを普通に git commit&push
  2. codox でドキュメント作ってるプロジェクトの GitHub リモートリポジトリで gh-pages の名前のブランチを切る
    • どこでやるかは公式Create a gh-pages branch参照
  3. 10 分ほど待つ
  4. 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 ってのがベター?

上二つが上手く行かずググってた時に見つけた情報。あーまた別のパッケージマネージャ的なもの? 日付的に割と最近乗り換えたというのを見て気にはなったものの今回は解決できたのでこちらを 試すのは保留した。

日本語キーワード

久しぶりの更新。小ネタだけど。

Clojure で日本語 Keyword は避けた方が良さそう。

:あとか:いとか単純なものだと普通に使えるんだけど アルファベットや漢字と混合したものだと関数のプロセスによっちゃ キーはあるのに値が取れない事がある。

取れなかったキーだけの単純マップデータ作って検索した場合は取れる。 しょうがないので全てアルファベットの Keyword に置き換えるとすんなり思い通りに動作した。

再現条件が良く分からないので悪いけどサンプルは載せられない。 まあ日本語 Keyword でどうこうしようと言う人はまずいないとは思うが。

Clojure のちょい便利マクロ

複数のシーケンスを対象に先頭からそれぞれ要素を取って処理したい時どうするか。 map が順当なんだけど渡す処理がごく簡単な場合ならまだしも ちょっと複雑になってくると少し見辛くなる。

例えばこんなの。

(map
  (fn [avar bvar cvar]
    (->> (foo-fn avar)
         (bar-fn bvar)
         (baz-fn cvar)))
    coll-a coll-b coll-c)

;; リーダマクロで多少簡略化できるが・・・
(map
 #(->> (foo-fn %1)
       (bar-fn %2)
       (baz-fn %3))
 coll-a coll-b coll-c)

これは結局渡すコレクションが下に来ているのが見辛さの原因になってる。 だったら上に持ってくる doseq でいいんじゃないかと最初思うがそうもいかない。

(doseq [a [1 2 3]
        b [4 5 6]]
  (println [a b]))
[1 4]
[1 5]
[1 6]
[2 4]
[2 5]
[2 6]
[3 4]
[3 5]
[3 6]
nil

複数シーケンスを渡した時の doseq は、全要素の組み合わせをたどる。 これはこれで良いのだが、各シーケンス要素を平行に取って処理したい時もしばしばある。

だったら最初の map に展開するようなマクロ作っちゃえと言う事でこんなん作った。

(defmacro domap
  "map 版 doseq。複数のシーケンスをバインドさせた時、全ての組み合わせを行うのではなく、
   map と同様に先頭から順に処理し、一番短い所で終了する。残りのアイテムは無視する。"
  [seq-exprs & body]
  `(doall (map (fn ~(vec (take-nth 2 seq-exprs))
                 ~@body)
               ~@(take-nth 2 (rest seq-exprs)))))

doall を入れておかないと REPL 上で副作用結果と map の結果シーケンスが入り混じって 表示される事になる。実際に返すシーケンスに副作用結果値が入るわけではないので それが気にならないなら doall は入れなくても構わない。lazy-seq じゃなくなってしまうしね。 結果シーケンスを捨てても良いなら doall の替わりに dorun という手もある。

使い方がこんな感じ。

(domap [i (range)
        j [3 4 5]]
  (println i j))
0 3
1 4
2 5
(nil nil nil)

ちょっとした小ネタでした。

Clojure で lazy-seq を生む関数についてのトラブルシューティング

Clojure で lazy-seq を生む関数と言うのは cheatsheetの Creating a Lazy Seq に書かれている関数の他に も良く使う所では map 関数なんかもそうだったりする。Programming Clojure を読んだ事があれば 何となく記憶してるかもしれない。

REPL で確かめてみると分かる。

(type (map inc [1 2 3]))
;=> clojure.lang.LazySeq

さて、今回改めて取り上げるのはこの lazy-seq、一部 NPE を誘発する状況があり、 その場合どうするかをメモしておこうと思う。

自分が発生した時の詳細な条件は掴みきれていないものの Java interop, nth 辺りを組み合わせて使っていた時に発生しており、 多分前者じゃないかと予想はしているが未検証。

この話で厄介なのは、REPL や println デバッグだと出ないという事。 REPL も println も lazy-seq を渡しても一度全て評価してから表示するためである。

その場しのぎ的な対応にはなるが、lazy-seq を生む関数なのかどうかは一旦忘れ、 NPE が発生したらシーケンス加工中に lazy-seq になってるのが原因かどうかを確認し そうであれば不格好だが doall とかかましていくように対処する事にした。 lazy-seq を全て避けた標準の代替関数を作るのもかったるいし。

これは機会があればどこで発生するのか改めて検証したい。