やろーじだい

ブログです

Clojure あれこれ

 最近 Clojure を使い初めて、研究用のプログラムを書いたりしていた。その中で遭遇した困ったところとその解決策をまとめる。Emacs + Cider 特有の話も含んでいる。新しい情報が入った場合更新予定。
※何か間違いやよりよい方法があれば修正しますのでコメントなどにお願いします。

困ったところ

use の as にてついて

 :use:as を使ってニックネームを付けても以下のように修飾なしで呼び出せてしまう。名前空間を分けるという用途には :require を使う。

(ns test-space.core
  (:use [clojure.repl :as repl]))
 
(repl/doc +)
; -------------------------
; clojure.core/+
; ~

(doc +)
; -------------------------
; clojure.core/+
; ~
(ns test-space.core
  (:require [clojure.repl :as repl]))

(repl/doc +)
; -------------------------
; clojure.core/+
; ~

(doc +)
; Unable to resolve symbol: doc in this context

2015/7/2 追記
以下のリンクで use と require について書かれています。 use を積極的に使う理由は基本的に無いようです。
Clojure - require vs use by @athos0220 on @Qiita

ラムダ式の中でラムダ式

 この場合、以下のように # を使ったラムダ式の中でさらに # を使ったものを使うとエラーが出る。

(def mat [[1 2 3]
          [4 5 6]
          [7 8 9]])

(map #(map #(+ %1 %2) %1 %2)
     mat
     mat)

; Unmatched delimiter: )

そこでどちらかを fn を使ったものに書きかえるか、大人しく関数を定義する。

(map (fn [v1 v2] (map #(+ %1 %2) v1 v2))
     mat
     mat)
;=> ((2 4 6) (8 10 12) (14 16 18))

(defn v+ [v1 v2]
  (map #(+ %1 %2) v1 v2))

(map #(v+ %1 %2)
     mat
     mat)
;=> ((2 4 6) (8 10 12) (14 16 18))

※ただし今回の例の場合は (map #(map + %1 %2) mat mat) と書ける。

副作用が思った通りに動作しない

 副作用を用いたプログラムを以下のように書くと意図した動作をしない。

(defn new-array-flom-list [lst]
  (let [len (count lst)
        new-array (int-array len)]
    (for [x (range len)]
      (aset new-array x (nth lst x)))
    new-array))

(-> (new-array-flom-list [0 1 2 3 4])
    (aget 3))
;=> 0

今回の場合は dotimesdorun などが使える。Clojure は遅延評価であるため、副作用を使用する場合は名前に do のついた関数を用いて実行を保証する必要がある。

(defn new-array-flom-list [lst]
  (let [len (count lst)
        new-array (int-array len)]
    (dotimes [x len]
      (aset new-array x (nth lst x)))
    new-array))

(-> (new-array-flom-list [0 1 2 3 4])
    (aget 3))
;=> 3
(defn new-array-flom-list [lst]
  (let [len (count lst)
        new-array (int-array len)]
    (dorun
     (for [x (range len)]
       (aset new-array x (nth lst x))))
    new-array))

(-> (new-array-flom-list [0 1 2 3 4])
    (aget 3))
;=> 3

別のネームスペース上で定義したレコードのインスタンスを生成したい

 他のネームスペース上で定義したレコードに対しては関数などと同じようにアクセスしようとしてもできない。

;; core.clj
(ns test-space.core)

(defrecord lang [name age])

(lang. "clojure" 8)
;=> #test_space.core.lang{:name "clojure", :age 8}
;; sub.clj
(ns test-space.sub
  (:require [test-space.core :as core]))

(core/lang. "lisp" 57)
; Unable to resolve classname: lang

そこで以下のように関数を経由することで生成できた。

;; core.clj
(ns test-space.core)

(defrecord lang [name age])

(defn new-lang [name age]
  (lang. name age))

(lang. "clojure" 8)
;=> #test_space.core.lang{:name "clojure", :age 8}
;; sub.clj
(ns test-space.sub
  (:require [test-space.core :as core]))

(core/new-lang "lisp" 57)
;=> #test_space.core.lang{:name "lisp", :age 57}

2015/6/29 追記
id:ayato0211 さんから import を使った方法を教えて頂いた。この時、- を含んだネームスペースを import する場合はJavaの流儀に沿って _ に書き換える必要がある。(コメント参照。)

;; core.clj
(ns test-space.core)

(defrecord Dog [name age])
;; sub.clj
(ns test-space.sub
  (:require [test-space.core])
  (:import [test_space.core Dog]))

(Dog. "foo" 1)
;=> #test_space.core.Dog{:name "foo", :age 1}

-> を使う方法もあった。

;; sub.clj
(ns test-space.sub
  (:require [test-space.core :as core]))

(core/->Dog "foo" 1)
;=> #test_space.core.Dog{:name "foo", :age 1}

任意の桁で四捨五入がしたい

 Clojure でそのような関数が見つからなかったので、 JavaBigDecimal を利用した。

(defn roundn [x n]
  (-> (BigDecimal. x)
      (.setScale n BigDecimal/ROUND_HALF_UP)))

(roundn 0.195 0)
;=> 0M

(roundn 0.195 1)
;=> 0.2M

(roundn 0.195 2)
;=> 0.20M

(roundn 0.195 3)
;=> 0.195M

2015/6/29 追記
id:ayato0211 さんから with-precision を用いることでできるという情報を頂いた。また bigdec という関数が用意されていた。

(defn roundn [x n]
  (if (= n 0)
    (Math/round x)
    (with-precision n (/ (bigdec x) 1))))

(with-precision 0 (/ (bigdec 0.195) 1))
;=> 0.195M

(roundn 0.195 0)
;=> 0

(roundn 0.195 1)
;=> 0.2M

(roundn 0.195 2)
;=> 0.20M

(roundn 0.195 3)
;=> 0.195M

Javaメソッド高階関数に渡したい

 そのまま渡すことはできないが、ラムダ式で包むことで渡すことが出来る。

(def lst [-1 -2 -3 -4])

(map Math/abs lst)
; Unable to find static field: abs in class java.lang.Math

(map #(Math/abs %) lst)
;=> (1 2 3 4)

また memfn というマクロがありこの用途で使うことができる。しかし Math/abs には使えない。オブジェクトのメンバメソッドに対してしてのみ使用できる。

(def lst [-1 -2 -3 -4])

(map (memfn toString) lst)
;=> ("-1" "-2" "-3" "-4")

(map #(.toString %) lst)
;=> ("-1" "-2" "-3" "-4")

(map (memfn Math/abs) lst)
; No matching method found: abs for class java.lang.Long

マルチメソッドにおけるデフォルト関数の指定

 キーワード :default を指定する。

(defmulti plus1 class)

(defmethod plus1 java.lang.Long [x]
  (+ x 1))

(defmethod plus1 clojure.lang.PersistentVector [vec]
  (map plus1 vec))

(defmethod plus1 :default [x]
  (println (str "No support " (class x))))

(plus1 1)
;=> 2

(plus1 [1 2 3 4])
;=> (2 3 4 5)

(plus1 "hoge")
; No support class java.lang.String
;=> nil

関数の引数の型や範囲を制限したい

 関数の先頭にずらずらと assert を書いていたが、:pre:post からなるフォームを関数の仮引数フォームの後に置くことによって事前条件と事後条件を導入できる。

(defn restrict-plus1 [x]
  (assert (integer? x))
  (assert (< 0 x))
  (assert (< x 10))
  (+ x 1))

(restrict-plus1 9)
;=> 10

(restrict-plus1 10)
; Assert failed: (< x 10)
(defn restrict-plus1 [x]
  {:pre [(integer? x) (< 0 x) (< x 10)]
   :post [(<= % 10)]}
  (+ x 1))

(restrict-plus1 9)
;=> 10

(restrict-plus1 10)
; Assert failed: (< x 10)

assert を書くよりもかなりスッキリ書ける上、返り値の条件も記述できる。

ネームスペースの切り替えが面倒くさい

 テストをするためなど頻繁にネームスペースを切り替える必要があるが repl 上で in-ns を使って切り替えるのは非常に手間だった。Cider では C-c M-n で現在のバッファのネームスペースに repl を切り替えることができる。

プロジェクト全体の再読み込み

 毎回 repl を再起動していた時期があり非常につらい思いをしていた。Cider では C-c C-x でリロードできる。この時 repl に保存されている変数など全てリフレッシュされるので,現在のコードが正しく動作するかの確認が容易にできる。さらに、変更があったバッファとそれを参照しているものだけを自動でリロードしてくれる。

Cider の debugger からの登録解除方法

 関数をトレースするように登録したあと、解除する方法がわからなかった。C-u C-M-x で登録し、C-M-x で解除できる。

困っていること

defmethod で定義した関数を Cider の debugger でトレースしたい

 トレース方法がわからない。通常の関数と同じ方法ではトレースすることができない。