日曜プログラミング

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

Clojure の名前空間とファイル名

Clojure では名前空間を切る時、通常は名前空間名と.cljを除いたファイル名を同じにするよう構成する事になっている。 また、名前空間をドットで区切るのはパス区切りと対応付けられる。

名前空間foo.bar だったら foo\bar.clj となる。

一方、lisp でのシンボル名は通常ハイフン(-)区切りが慣習になっている。 で、名前空間でもこの慣習に従ってハイフン付きで同じファイル名で保存後 require などと すると Java の命名可能文字列仕様の制限にぶち当たる。

例えば

(ns foo.bar-baz)

名前空間を定義し、ファイル名を foo\bar-baz.clj として

(require 'foo.bar-baz)

としても

CompilerException java.io.FileNotFoundException: Could not locate foo/bar_baz__init.class or foo/bar_baz.clj on classpath: , compiling:~

と例外が出る。この例外はメッセージにある通り、 ファイル名を foo\bar-baz.clj でなく foo\bar_baz.clj とすれば解決する。

Java 自体の命名可能文字列の話は こちら が分かりやすい。

名前空間下にある関数名の場合はコンパイラで変換してくれるのか気にしなくてもいいだけに 個人的には若干気持ち悪い。解決策あるからいいっちゃいいんだけど。

これ あんまり Java 知らない Clojure 使いは一度は引っかかるんじゃないのかなあ。 そういや Clojure に最初から入っているライブラリの名前空間名にもハイフン付きって見た事ないな。

Clojure の SQL DSL の一つ sqlingvo なかなかイイですよ

[2014-07-22 追記] table 別名がデフォルトで AS なしに修正された事を示すリンクを追記

Clojure 界隈では、SQL DSL の選択肢が結構前からいくつか存在する。初期からある KormaHoneySQL そして今は ClojureJDBC ラッパーのデファクトスタンダートとなってる clojure.java.jdbc でもごく簡易的な DSL を備えている

今回はその中でも sqlingvo を使ってみたのでその感想を書いてみる。

今回はあくまで感想のみ。他ライブラリとの比較は使ってないのでできないし、 インストールやごく基本的な使い方に関しても公式を見れば大体分かると思うので書かない。

良いと思った所

WITH 句、CASE 式対応

個人的に決め手になったのはこれ。特に WITH 句はこいつが対応してるとアプリ側でなんちゃって View のようなものを共通で持つ事が簡単になる*1

他にも DELETE, INSERT, UPDATE はじめ、実用するに必要なもの一通りが揃っている感じ。

CASE 式もクエリで取る段階で判定して処理しとけるならしときたいよね、と言う場面はちょいちょ いあるのでこれは嬉しい。勿論 CASE 無しで取っておいてアプリ側でループで条件判断しながらぶん 回せば同じ事はできるけど。けどなあ。

素の SQL にかなり近い

例えばこういうの。

(select [1 2 3] (from :dual))

SQL 嫌いじゃないんでそんな Clojure っぽくマップデータ化とかする必要ないと思うんです。

役割としては sqlingvo の SQL DSLclojure.java.jdbc で扱い易い形に変換するだけ

clojure.java.jdbc で扱い易い形と言うのはこういうの↓

(sql (select [1] (from :dual)))
;=> ["SELECT 1 FROM \"dual\""]

ベクタで先頭要素がパラメータ付き SQL 文字列、残り要素がパラメータに渡す値と言う形のやつ。

つまり実際に DB からデータを取ってくるには clojure.java.jdbc も一緒に使う必要がある。 だけどこれまでも clojure.java.jdbc 使って生 SQL 投げていた自分にとっては殆ど問題にならない。

SQLClojure 上で扱いやすく書ける機能のみの提供と言うのかな。

これは言い替えると実際のスキーマに存在しているかどうかは sqlingvo では全くチェックしないの で注意するとしたらそれくらいか。

微妙だと思った所

Oracle との親和性が若干悪い

どっちかっつーとこの 2 つは Oracle 対応しろよと言う感じなのかな。標準仕様どこで読めるか分 からんから何とも言えんけど。10 以降だと対応してるのかも?

SQL 文字列展開時に DB オブジェクト名がダブルクォーテーションで括られるのに Oracle9i が対応してない

最初の変換例にあるような形そのままのは Oracle(9i) では許しちゃくれません。 なので Oracle の時はダブルクォーテーションを外すヘルパー関数をかましてやる必要がある。

FROM 句でのテーブル別名で AS を入れるが Oracle9i が(ry

昔ながらの (+) で表結合するスタイルの人はこれでもう選択肢から外れるのかな。 ただこれも JOIN で行う結合においてもサブクエリが使えないという制限がかかってしまうことになる。

[2014-07-22 追記] 0.5.17 以降は table 別名はデフォルトで AS 無しに修正されたようです

ちょっとマクロシンタックスが多くない?

結構量がある テストコード は sqlingvo の場合書き方のサンプルにもなってて、これ眺めてると分かるが、それなりに複雑なク エリになるとクォート・アンクォート・バッククォートも併用しないとちゃんと変換してくれない。

何かマクロを書いているような気分になってきてなんだかなあ、と言うのはちょっとあるw

HAVING 句のサポート

できれば欲しい。使わない書き方にするのも割と簡単だけども。

最後に

微妙と思った点などで挙げたように一部制限やクセはあるものの、クエリの一部を取り出して使い回 したいと思っていた人には結構オススメ。SQL の弱点と個人的には思っている分割粒度がどうしてもデ カくなるのを上手く補っているライブラリじゃないかなと。

*1:共通で持てる SQL 文字列を分割してるようなもの なので実際データ取る時は毎度リクエストを投げるが

トップレベルでの let,letfn を使った private 化

Clojure でコード書いてる時、^:private な変数や関数で 行数が増えてくるとこれってどこで使ってたっけ?と思う事が増えてきた。

原始的な検索や、タグジャンプでまあ分からん事もないんだけど 行数が増えてくるとそれすらも億劫になってくる。

良く書くパターンとしては以下のようなもの。

(def ^:private local-map {:a 1 :b 2})

(defn a-val [] (:a local-map))
(defn b-val [] (:b local-map))

(defn- local-fn [arg] (some-proc arg))

(defn a-fn [arg] (-> (local-fn arg) some-a-proc))
(defn b-fn [arg] (-> (local-fn arg) some-b-proc))

^:private な下に書くようにすればまあそれなりに分かるんだけど、 パッと見の時は同一インデントの為すぐには区別つかなかったりする。

ある時、ふと Clojure の関数ってクロージャだから環境抱え込めるんだよな、ってのを 思い出した。て事は let や letfn の中に Public Var な関数、変数定義すりゃ少しは 整理付くんじゃなかろーかと思い立った。

先ほど例示した擬似コードを書き直すとこんな感じ。

(let [local-map {:a 1 :b 2}]
  (defn a-val [] (:a local-map))
  (defn b-val [] (:b local-map)))

(letfn [(local-fn [arg] (some-proc arg))]
  (defn a-fn [arg] (-> (local-fn arg) some-a-proc))
  (defn b-fn [arg] (-> (local-fn arg) some-b-proc)))

どうだろう。どちらの例も local-maplocal-fn^:private な事は 変わりないが、letletfn で包まれる事で最初の例よりも関連付けが明確に なってるんじゃなかろうか。

ns やファイルを分ける程の規模ではないが、共通で使われるのがせいぜい 2,3 箇所くらいな ローカルを定義したいと言う状況では有効だと思う。もちろん ns 全体であちこちで local として使いたい場合は素直に ^:private 付与したシンボルでいいと思う。

ただ、この手法は擬似コード例で言う local-map, local-fn に docstring を 入れられなくなるという明確なデメリットもあるので結局お好みで選ぶことになるのかなやっぱり。

ちなみにこれはオリジナルな話でも何でもなく、xyzzyスクリプトを書いていた頃 こんな書き方そういやあったなと言う記憶が蘇ったもの。

codox でドキュメント出力されない ns がある件

codox、作ったツールの関数群を俯瞰するのには 十分使えるものだと思ってる。もう少しこなれてくれば提出用にしてもいいんじゃないかってくらい。

が、その場合割と致命的に困る事があり、一部 ns で何故かドキュメント出力されない事がある。

AOT コンパイルがどうやら関係してるらしく、 codox で AOT コンパイルされた時の注意 など注意書きがあるにはある。 project.clj:uberjar {:aot :all} はデフォルトでやってるが、 それ外してから lein doc しても現象変わらない所を見るに関係してる訳でもなさそう。 うーん良く分からん。

ググってみたらcodox issue#34が見つかり、 どうもClojure そのものに昔からあるバグらしくて codox 側じゃどうにもならんって close されてる。 なので記事タグは Clojure だけにした。

Clojure は好きなんだが、使い込んでいくうちにこう言う所でつまづくのがなあ。 最初から完璧な言語・環境はないんだからこの手の話はどの言語使っても一緒なんだろうけど。

Clojure 界隈での高階関数がらみのグチ

外部ライブラリを色々使い出して最近ふつふつ思うのが、どーも高階関数が絡んだ時の説明がイマイ チな事がチラホラあるような。ここで言ってるのはさすがに今更高階関数そのものの説明してくれと 言ってるわけではなく、API として説明不足と言うか。個人的には大きく 2 パターンあると思って る。

引数になる関数へ渡すデータがこちらで指定できる場合

普通にコード内で高階関数使う時はこっち。標準 API の map とかその類。 この時に必要な説明は

  • 引数で関数を適用した結果どんなデータが返されるか
  • 引数で渡した関数への引数はどう渡されるのか(ああややこしいw)

前者の説明まではあって、後者の説明が抜けてる事がちょくちょくある。それぞれ説明してもらわな いと自分の記述ミスじゃない部分で clojure.lang.ArityException 出しながらトライアンドエラー する手間が増えてちょっとイヤだ。

引数になる関数へ渡すデータがこちらで指定できない場合

渡されるデータはライブラリで(少なくとも場所やフォーマットとしては)決まってて、設定ファイル に関数を記述する場合。こちらも結局 何が データとして渡されてくるのかの説明が無い事とま た困ったことになる。まずどんなデータ渡されてくるんだろうと println や spit 入れてみたりす る必要が出てしまう。設定として関数渡せて柔軟になりますぜ、と言うのは分かるんだけどね。

何というか高階関数使えるぜスゲーで満足してないで、自分が高階関数作って API 公開した時はこ の辺りのドキュメントをちゃんとしたいと思っただけのグチでした。

core API はさすがにちゃんと書かれてる。他の lisp 界隈もこんなもんだったっけかなあ?

On Lisp -> Clojure へ移植: 12.1~12.3

ここでも一応書いておくが、前の 11 章はベースになる setf に該当するものが Clojure には見当たらないので章全体を飛ばしている。

また、この章のサンプル移植は most-of しかなく、殆ど無いも同然なので ここも飛ばしてもらっても構わない。

12.1 新しいユーティリティ

p112

図68 most-of の関数版とマクロ版、を書きたかったのだが、 破壊的変更を伴う incf は仮に定義しても Clojure と相性が悪すぎるので 変わりに loop 版を書いてみた。

(defn most-of [& args]
  (let [all (count args)]
    (>
     (count (filter (complement nil?) args))
     (/ all 2))))

(defn most-of [& args]
  (let [needs (Math/floor (/ (count args) 2))]
    (loop [hits 0
           a args]
      (if (> hits needs)
        true
        (if (seq a)
          (recur (if (first a) (inc hits) hits)
                 (rest a)))))))

で、ここでベンチマークライブラリ criterium を使って比較してみる。

上の通常関数版。

(bench (most-of :t :t :t nil))
;Evaluation count : 49568880 in 60 samples of 826148 calls.
;             Execution time mean : 1.198932 μs
;    Execution time std-deviation : 12.024127 ns
;   Execution time lower quantile : 1.180818 μs ( 2.5%)
;   Execution time upper quantile : 1.228185 μs (97.5%)
;                   Overhead used : 11.322126 ns
;
;Found 3 outliers in 60 samples (5.0000 %)
;  low-severe   3 (5.0000 %)
; Variance from outliers : 1.6389 % Variance is slightly inflated by outliers
;nil

loop 版。

(bench (most-of :t :t :t nil))
;Evaluation count : 143876640 in 60 samples of 2397944 calls.
;             Execution time mean : 412.751676 ns
;    Execution time std-deviation : 10.560372 ns
;   Execution time lower quantile : 402.826633 ns ( 2.5%)
;   Execution time upper quantile : 440.581697 ns (97.5%)
;                   Overhead used : 11.322126 ns
;
;Found 8 outliers in 60 samples (13.3333 %)
;  low-severe   3 (5.0000 %)
;  low-mild     5 (8.3333 %)
; Variance from outliers : 12.6261 % Variance is moderately inflated by outliers
;nil

確か loop に書き直す方が速くなるというのはチラホラ見ていたが、 こういうのって作るプログラムの目的や呼び出す回数との兼ね合いだよなあ。

と、本書でもそんな事が書かれてるので次の nthmost は飛ばしますw

12.2 例:B`ezier 曲線

ここもグラフィックを実際に出すまでの具体的手順が分からんので飛ばします。

12.3 応用

この章項目自体には載ってないので次へ。

第 12 章 コンパイル時の計算処理

はここまで。

On Lisp -> Clojure へ移植: 10.4~10.6

10.4 反復

p96

(defmacro forever [& body]
  `(loop []
     ~@body
     (recur)))

図 53 単純な反復用マクロ.while, for は既出なので till のみ。 Clojure の loop には go や return はないが、条件分岐と recur を組み合わせたら マクロ定義時だけは同様の事が可能。「定義時だけ」と書いたのは Common Lisp のように do 派生マクロから go や return を呼ぶ事はできないため。

(defmacro till [test & body]
  `(loop []
     (when ~test
       ~@body
       (recur))))

p97

図 54 のマクロ定義へ行きたい所だが、内部で使っている map0-n は On Lisp で定義した 関数らしく、ここでは少し戻って図 9(p39) で示されている定義の Clojure 版を先に書く。

(defn mapa-b
  ([f a b]
     (map f (range a (inc b))))
  ([f a b step]
     (map f (range a (+ b step) step))))

(defn map0-n [f n]
  (mapa-b f 0 n))

(defn map1-n [f n]
  (mapa-b f 1 n))

(defn map-> [f start test-f succ-f]
  (loop [i start
         result nil]
    (if (test-f i)
      (reverse result)
      (recur (succ-f i) (cons (f i) result)))))

(defn mapa-b [f a b & [step]]
  (map-> f
         a
         (fn [x] (> x b))
         (fn [x] (+ x step))))

ClojureCommon Lisp そのままのオプショナル引数サポートは無いものの この場合なら引数の多重定義でいける。こうして書いてみて思うが map-> と mapa-b は通常の map 以上に loop に近い気がする。

ともあれ内部関数定義はできたので図 54 のマクロ定義へ戻る。 Clojure/名前空間修飾の区切りに使われるのでマクロ名は略称でない名前に変更している。

まずは忠実な移植をしてみた。

(defmacro do-tuples-open [parms source & body]
  (if parms
    (let [src (gensym)]
      `(let [~src ~source]
         (map (fn ~parms ~@body)
              ~@(map0-n (fn [n]
                          `(nthnext ~src ~n))
                        (dec (count parms))))))))

が、REPL での結果が print の戻り値 nil も含んだような結果になってしまう。

(do-tuples-open [x y] '(a b c d)
  (print (list x y)))
;=> ((a b)(b c)nil (c d)nil nil)

Clojure なら部分リストを作るのは標準で partition が使えるので、 doseq と組み合わせて書き換えてみた。

(defmacro do-tuples-open [parms source & body]
  (if parms
    (let [src (gensym)]
      `(let [~src ~source
             body-fn# (fn ~parms ~@body)]
         (doseq [x# (partition (count '~parms) 1 ~src)]
           (apply body-fn# x#))))))

(count '~parms) の部分はマクロ展開後の評価でシンボルないよと言われるので アンクォート後にクォートした上でカウントしてやる必要がある。 関数の仮引数リストとして渡す場合はシンボル解決が行われない為問題ない*1

テスト。上手く行く。

(do-tuples-open [x y] '(a b c d)
  (print (list x y)))
;=> (a b)(b c)(c d)
;   nil

と、思ったが、よくよく考えてみれば REPL 側での 関数の戻り値と標準出力とで出力順序が不定なだけなんじゃなかろうかと思い直し確かめてみる。

(def result (do-tuples-open [x y] '(a b c d)
               (print (list x y))))
;=> (a b)(b c)(c d)
;   #'user/result
result
;=> nil

やっぱそうでした。

気をとり直し、do-tuples-close へ。・・・ちょっとネスト深すぎやしませんかコレ。 理解がまだ追いつかないがまずは忠実に移植してみる。

(defn dt-args [len rest src]
  (map0-n (fn [m]
            (map1-n (fn [n]
                      (let [x (+ m n)]
                        (if (>= x len)
                          `(nth ~src ~(- x len))
                          `(nth ~rest ~(dec x)))))
                    len))
          (- len 2)))

(defmacro do-tuples-close [parms source & body]
  (if parms
    (with-gensyms [src rest bodfn]
      (let [len (count parms)]
        `(let [~src ~source]
           (when (nthrest ~src ~(dec len))
             (letfn [(~bodfn ~parms ~@body)]
               (loop [~rest ~src]
                 (when (not (nthnext ~rest ~(dec len)))
                   ~@(map (fn [args] `(~bodfn ~@args))
                          (dt-args len rest src))
                   nil)
                 (~bodfn ~@(map1-n (fn [n] `(nth ~rest ~(dec n)))
                                   len))
                 (recur (next ~rest))))))))))

それで動かしてみるが、IndexOutOfBoundsException になる。

(do-tuples-close [x y] '(a b c d)
  (print (list x y)))
;=> IndexOutOfBoundsException   clojure.lang.RT.nthFrom (RT.java:795)

;   (a b)(b c)(c d)(d a)

nth 辺りを何とかすればいいとは思うものの、それ以前に理解がイマイチ追いつかないw 気が向いたら do-tuples-open のように書き直してみる事にして次へ。

10.5 複数の値に渡る反復

多値に対応したこれまた結構複雑なマクロが示されてるが、 シーケンスなんかで返して受け側で let なりの部分で destructuring してやりゃいいんじゃねと 言う気がしてならなく、モチベが上がらんので飛ばす。

shiroさんの解説辺りを読むと性能が要求される場面で適用する意味があるかもとの事だけど、うーん。

10.6 マクロの必要性

ここも関数でも書ける場合があると説明があり、出てるサンプルも相当小さいので飛ばす。 終わってみると 10.4 くらいしか書いてないな。

第 10 章 古典的なマクロ

はここまで。

*1:仮引数なのだから当然っちゃ当然