Programming

Clojureオレオレコーディング規約

しばらくClojureのコードを書いてみてしっくりきている書き方をオレオレ規約化してメモしていく(随時更新予定)。規約策定者募集!ネタがあれば@makingまで。

コーディング編

  • Javaインスタンス生成は(ClassName.)形式。(new書くのメンドイし.ついてりゃ明白)
  • Javaメソッドの呼び出しは(.method obj)形式。(.とメソッド名が離れていると可読性が悪い。..とか見たくない)
  • 無名関数は引数の数が1以下の場合は%(...)形式、2以上の場合は(fn [x y] ...)形式。(2以上の場合は変数名に意味を持たせましょう)
  • リフレクションを(極力)発生させないこと(特にライブラリ)。*warn-on-reflection*trueにしてテスト。メソッドを呼ぶオブジェクトを引数に取るときは型ヒントをつけること。
  • (当たり前だけど)defnの中でdefdefnしない。letletfnで。
  • 副作用があるときに明示的にdoしなくても良い
  • 閉じ括弧は右下にまとめる

プロジェクト編

  • ビルドはLeiningenを使う。生ant使うな
  • ディレクトリ構成はlein newでできるもの。
  • ライブラリ系のメインファイルは{namespace}/core.cljがメイン。
  • アプリ系のメインファイルは{namespace}.cljがメイン。(この辺はまだしっくりきてないけど、、core.cljのほうがいいかな)
  • リリースに不要なjarをproject.clj:dependanciesに入れない。開発時にのみ必要なものは:dev-dependanciesに入れない

Created at : 2010-04-05 02:17:09   Updated at : 2010-04-05 02:44:44
Category : Programming::Lisp::Clojure

Clojureの関数にcondition-mapでPre・Postチェック

Clojureの関数の形式は実は

(defn function-name doc-string? attr-map? [parameter-list]
  conditions-map?
  (expressions))

です。 doc-string?はドキュメント、attr-map?はメタ情報です。引数の後にあるcondition-map?はなんぞやというと、関数の入出力の条件を設定できるmapです。(Clojure 1.1.0からかな?)
condition-map?の形式は

{:pre 入力値をチェックする条件S式のシーケンス, :post 出力値をチェックする条件S式のシーケンス}

です。例えば、引数が全て正でかつ、関数の結果が10より大きいという条件を付けたい場合、

(defn hoge [x y]
  {:pre [(> x 0) (> y 0)]
   :post [(> % 10)]}
  (+ x y))

という風になります。%には返り値が入ります。
条件を満たさない場合は、java.lang.AssertionErrorが発生します。
(ちなみにleiningen1.0.1のreplのバージョンであるClojure 1.1.0-master-SNAPSHOTではjava.lang.Exceptionでした。。。これはExceptionかErrorかという大きな違いであり、明文化が待たれます)

実行してみると、

user=> (hoge 5 6)
11
user=> (hoge 0 11)
java.lang.AssertionError: Assert failed: (> x 0) (NO_SOURCE_FILE:0)
user=> (hoge 11 0) 
java.lang.AssertionError: Assert failed: (> y 0) (NO_SOURCE_FILE:0)
user=> (hoge 5 5) 
java.lang.AssertionError: Assert failed: (> % 10) (NO_SOURCE_FILE:0)

となります。

モジュラリティを高める

このままだと常にチェックが走りますが、単純に関数を使う場合を分けたいことがあると思います。次のように書くと、関数の機能とチェック機能を分離させることができ、モジュラリティを高めることができます。

user=> (defn hoge [x y] (+ x y))
#'user/hoge
user=> (hoge 5 5)
10
user=> (defn with-condition [f x y] {:pre [(> x 0) (> y 0)], :post [(> % 10)]} (f x y))
#'user/with-condition
user=> (with-condition hoge 5 5)
java.lang.AssertionError: Assert failed: (> % 10) (NO_SOURCE_FILE:0)

partialで部分適用しておけば、元の関数を同じものを定義できますね。

user=> (def always-condition (partial with-condition hoge))
#'user/always-condition
user=> (always-condition 5 5)
java.lang.AssertionError: Assert failed: (> % 10) (NO_SOURCE_FILE:0)

エンタープライズを乗っとるには堅牢性が必要になると思います。condition-map?を上手に使うことは乗っ取りの第一歩ですね!

ちなみにここ数回のClojureネタはClojure in Action(MEAP版)を読んで、おおっと思ったことのまとめです。プログラミングClojureとぼくの書いた記事を読んでおけばClojure in Actionはいまのところ不要かもですw

Created at : 2010-03-25 02:08:44   Updated at : 2010-03-25 03:15:05
Category : Programming::Lisp::Clojure

Clojureで相互再帰最適化

そろそろClojureでのloop/recurによる末尾再帰最適化に慣れてきたんじゃないでしょうか。
Clojureでは相互再帰も最適化されません。例えばこんな例

;; ちょっと変わったみんな大好きFizzBuzz
(declare fizz buzz)

(defn fizz-buzz [n]
  (when (> n 0)
    (print n " = ")
    (fizz n)))

(defn- fizz [n]
  (if (zero? (rem n 3))
    (print 'fizz))
  (buzz n))

(defn- buzz [n]
  (if (zero? (rem n 5))
    (print 'buzz))    
  (println)
  (fizz-buzz (dec n)))




このような再帰もスタックを消費します。手元の環境ではn = 2000java.lang.StackOverflowErrorが発生しました。

このような状況を最適化するために用意されている関数は。。。trampolineです。
プログラミングClojureをお持ちの方はP.149を開いてください。さらっと説明されていますw

trampolineで最適化したコード

trampoline化させるには返り値の括弧の前に#をつけてクロージャ(closureの方)を返すように修正します。

(declare fizz buzz)

(defn fizz-buzz [n]
  (when (> n 0)
    (print n " = ")
    #(fizz n)))

(defn- fizz [n]
  (if (zero? (rem n 3))
    (print 'fizz))
  #(buzz n))

(defn- buzz [n]
  (if (zero? (rem n 5))
    (print 'buzz))    
  (println)
  #(fizz-buzz (dec n)))

呼び出すときは(trampoline f & args)です。

user> (trampoline fizz-buzz 2000) ; これでjava.lang.StackOverflowErrorが発生しなくなりました!

部分適用でインタフェース改善

このままだと使うとき毎回trampolineを書かなくてはいけなくて面倒ですね。本来渡したいのfizzbuzzの引数だけです。こんなときのためにpartial部分適用があります!
プログラミングClojureをお持ちの方はP.146を開いてください。trampolineの第一引数がfizz-buzzである新しい関数を作っちゃいます。

(declare fizz buzz)

;; rename
(defn- fizz-buzz-1 [n]
  (when (> n 0)
    (print n " = ")
    #(fizz n)))

(defn- fizz [n]
  (if (zero? (rem n 3))
    (print 'fizz))
  #(buzz n))

(defn- buzz [n]
  (if (zero? (rem n 5))
    (print 'buzz))    
  (println)
  #(fizz-buzz-1 (dec n)))

;; 部分適用
(def fizz-buzz (partial trampoline fizz-buzz-1))
user> (fizz-buzz 2000) ; これでもともと期待していたインタフェースで最適化できました!

面白いですねClojure!
まだプログラミングClojureを買っていない人はぽちりましょう!
プログラミングClojure

Created at : 2010-03-24 01:38:59   Updated at : 2010-03-24 02:34:42
Category : Programming::Lisp::Clojure