Skip to content

Instantly share code, notes, and snippets.

@syou6162
Created August 19, 2012 11:09
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save syou6162/3394296 to your computer and use it in GitHub Desktop.
Save syou6162/3394296 to your computer and use it in GitHub Desktop.
On Lispを読んでいて分からなかったCommon LispとClojureの違いについて自分なりにまとめていきます

Common Lispの関数

dolist

(dolist (var init-form result) S式 ... )

dolist は名前が示すように、リストに関係があります。最初に init-form を評価します。このとき、評価結果はリストでなければいけません。リスト以外の場合はエラーとなります。dolist は、このリストの要素を順番に変数 var に代入して S 式を評価します。リストの要素がなくなったら result を評価し、その値が dolist の返り値になります。次の例を見てください。

(dolist (x '(0 1 2 3 4)) (print x))
0
1
2
3
4 nil

ただのforループマクロだった。

do

(defun fact (x)
  (do ((n 2 (1+ n)) (result 1))     ; 変数の定義
      ((< x n) result)              ; end-test と result 
      (setq result (* result n))))  ; 繰り返す S 式

とか、フィボナッチの例らしい。

CL-USER> (do ((i 0 (1+ i))    ; 変数i  初期値:0 ステップ:i++
	      (j 10 (1+ j))   ; 変数j  初期値:0 ステップ:j++
	      (k 20 (1+ k)))  ; 変数k  初期値:0 ステップ:k++
	      ((= i 10) (+ i j k)) ; 終了条件と戻り値
	   (format t "i: ~a j:~a k: ~a~%" i j k)) ; ループ毎に実行される処理
i: 0 j:10 k: 20
i: 1 j:11 k: 21
i: 2 j:12 k: 22
i: 3 j:13 k: 23
i: 4 j:14 k: 24
i: 5 j:15 k: 25
i: 6 j:16 k: 26
i: 7 j:17 k: 27
i: 8 j:18 k: 28
i: 9 j:19 k: 29
60

nconc

nconcって何だ。

&bodyや&rest

&bodyとか&restみたいな関数の引数に取ってるやつの存在がよく分からん。

可変長引数を取るようなやつらしい。clojureのmaxとかstrとかそういう系。


multiple-value-bindは確かに便利だけど、letの中とかでできるclojureのほうがよくできているなと思うこと多々。


with-gensymsってマクロを定義しているけど、clojureだったら後ろに#をつけるだけのreader macroでできることを考えるとcommon lispはやっぱり面倒な感じである。

On Lisp 19章のクエリコンパイラの付近からについてもう一度復習しなおす。こっからが難しくなってくる。


ただ、埋め込み言語としてlisp上にprologを実現させる付近でマクロのありがたさとかが分かってきた。prologの例は本当に良くできている。基本的に穴の空いたパズルに対し、制約条件を満たすように探索を続けていく形となっている。穴の空いたパズルに対し、マッチングするものを探すのが、destructuringとなっており、それが分配の章で説明されている。また、解が複数ある場合に対し、全解列挙するのではなく、一つづつ答えを出力していくのを実現するために継続がうまく使われているし、探索は基本的にモナドのところで学んだように非決定性の問題である。

prologを埋め込み言語としてlisp上に実装する場合、上の要素を全て使う。lispならば上の要素を言語を拡張しながらうまく実装していくことができるし、lisp上に実装されている言語内DSLのようなものなので、Lispの便利な関数だとかsyntaxについてはこれまで通り使うことができる。小さな言語を作った、という感じがまさにピッタリでマクロを勉強するいい例となっているように思う。

第2章 関数

2.4 属性としての関数

On Lisp風だとこんな感じでdispatchが説明されている。

;; conditional version
(defn behave [animal]
  (cond
   (= animal 'dog) (do '(wag-tail) '(bark))
   (= animal 'rat) (do '(scurry) '(squeek))
   (= animal 'cat) (do '(rub-legs) '(scratch-carpet))))

Protocols

clojureっぽいdispatchをどうするかが説明されている。具体的にはprotocolを使ってやる。protocolは異なるデータ型に対して適用できる関数のリストみたいなものである。protocolは実際にはdeftypedefrecordされたようなものに対して使うことができる。

;; Protocols. Define the protocol
(defprotocol animal
  (behave [this] ))

;; define a dog
(defrecord dog [breed]  )

;; add the animal protocol to dog type
(extend dog
  animal
  {:behave (fn [src] (do '(wag-tail) '(bark)))})

;; create a dog
(def my-dog (dog. "collie"))

;; see what it does
(behave my-dog)

;; define a rat
(deftype rat [color])

;; add the animal protocol to the rat type
(extend rat
  animal
  {:behave (fn [src] (do '(scurry) '(squeek)))})

;; create a rat
(def brown-rat (rat. "brown") )

;; see what it does
(behave brown-rat)

(extend String
  animal
  {:behave (fn [src] (do '(what)))})

(behave "huh")

Multimethods

もう一つ方法があって、それはmultimethodを使う方法。"While protocols are similar to overriding functions, multimethods are like overloading. Protocols broadened overriding to make polymorphism work independent of class hierarchies. Multimethods allow overloading based not just on parameter type, but even parameter values."ということらしい。

multimethodsのほうはパラメータの型だけじゃなくって、パラメータの値によってもdispatchの仕方を変更できる。そういうことなので、下では偶数と奇数で振舞を変更できるようなものを作っている。ネストしまくったりしなくてよいので、見通しがよくなりそうな感じですね。

;; Multimethods. Define the multimethod type
(defmulti behave-multi identity)

;; define implementations for our animals
(defmethod behave-multi 'dog [x]
  (do '(wag-tail) '(bark)))
(defmethod behave-multi 'rat [x]
  do '(scurry) '(squeek))

;; try them out
(behave-multi 'dog)
(behave-multi 'rat)

;; You can dispatch on parameter values,
;; not just parameter types
(defmulti two-behaviors
  (fn [num] (if (odd? num) :odd :even)))

(defmethod two-behaviors :odd [num]
  (str num " is odd"))

(defmethod two-behaviors :even [num]
  (str num " is even"))

(two-behaviors 3)
(two-behaviors 4)

2.5 スコープ

(let [y 7]
  (defn scope-test [x]
    (list x y)))

(let [y 5]
  (scope-test 3)) ; (3 7)

2.6

(defn list+ [lst n]
  (map (fn [x] (+ x n))
       lst))

(list+ '(1 2 3) 10) ; (11 12 13)

;; lexically scoped counter
(let [counter (atom 0)]
  (defn new-id [] (swap! counter + 1))
  (defn reset-id [] (reset! counter 0 )))

(new-id) ; 1
(reset-id) ; 0

;; adder fuctions
(defn make-adder [n]
  (fn [x] (+ x n)))

(def add2 (make-adder 2))
(def add10 (make-adder 10))
(add2 5) ; 7
(add10 3) ; 13

;; adder that allows changing the added amount
(defn make-adderb [n]
  (let [amt (atom n)]
    (fn [x & change ]
      (if change  (reset! amt x ))
      (+ x @amt))))

(def addx (make-adderb 1))

(addx 3) ; 4
(addx 100 true) ; 200
(addx 3) ; 103

;; First dbms version
(defn make-dbms [db]
  (list
   (fn [key]
     (db key))
   (fn [key val]
     (assoc db key val))
   (fn [key]
     (dissoc db key))))

(def cities (make-dbms {'boston 'us, 'paris 'france}) )

((first cities) 'boston) ; us
((second cities) 'london 'england) ; {boston us, london england, paris france}
((last cities) 'boston) ; {paris france}

;; Mutable dbms example

(defn make-mutable-dbms [db]
  (let [mdb (atom db)]
    (list
     (fn [key]
       (@mdb key))
     (fn [key val]
       (swap! mdb assoc key val))
     (fn [key]
       (swap! mdb dissoc key)))))

(def citiesx (make-mutable-dbms
	      {'boston 'us, 'paris 'france}))

((first citiesx) 'boston) ; us
((second citiesx) 'london 'england) ; {boston us, london england, paris france}
((last citiesx) 'boston) ; {london england, paris france}

;; dbms with commands stored in a
;; map instead of a list

(defn make-dbms-map [db]
  (let [mdb (atom db)]
    {:select (fn [key] (@mdb key))
      :insert (fn [key val]
        (swap! mdb assoc key val))
      :delete (fn [key]
        (swap! mdb dissoc key))}))

(def citiesm (make-dbms-map
  {'boston 'us 'paris 'france}))
((:select citiesm) 'boston) ; us
((:insert citiesm) 'london 'england) ; {boston us, london england, paris france}
((:delete citiesm) 'boston) ; {london england, paris france}

第8章 いつマクロを使うべきか

p117からのCADのための埋め込み言語の例は分かりやすい。with系のやつは意義が見えやすいようだ。def系はなんか未だに馴染めないけど。

古典的なマクロ

最もよく使われる種類のマクロの定義方法について紹介。

11.1 コンテキストの生成

無名関数を使ってマクロを定義する例。関数適用に展開される。

(let [x 'b] (list x))

(defmacro our-let [binds & body]
  `((fn [~@(take-nth 2 binds)]
       (do ~@body)) ~@(take-nth 2 (rest binds))))

(macroexpand-1 '(our-let [x 1 y 2] (+ x y))) ; ((clojure.core/fn [x y] (do (+ x y))) 1 2)

(our-let [x 1 y 2] (+ x y)) ; 3
(defmacro when-bind [[var expr] & body]
  `(let [~var ~expr]
     (when ~var
       ~@body)))

#_(let [sun-place 'park rain-place 'library]
  (if (sunny)
    (visit sun-place)
    (visit rain-place)))
(defn condlet-bind [bindings]
  (loop [binds bindings]
    (cond (empty? binds) []
          (first binds) (first (rest binds))
          :default (recur (rest (rest binds))))))

(defmacro condlet [clauses & body]
  `(let ~(condlet-bind clauses)
     ~@body))

(macroexpand-1
 '(condlet [(= 1 2) [x 2 y 3]
           (= 1 1) [x 1 y 2]]
          (println (str "sum is " (+ x y)))))

(condlet [(= 1 2) [x 2 y 3]
           (= 1 1) [x 1 y 2]]
          (println (str "sum is " (+ x y))))

11.2 with-系マクロ

;; Clojure has a with-open macro that is used for dealing with all sorts of resources
(with-open [writer (clojure.java.io/writer "output-file" :append true)]
  (.write writer "99"))

;; file io has its own set of macros, spit and slurp
(spit "output.txt" "test" :append true )
(slurp "output.txt")

;; unwind-protect becomes try catch
(try
  (do (println "What error?")
      (throw (Exception. "This Error."))
      (println "This won't run"))
  (catch Exception e (.getMessage e))
  (finally (println "this runs regardless")))


;; with clojure data types locks are unnecessary.
(def ^:dynamic *db* "some connection")

(binding [ *db* "other connection"]
         (println *db*))


(def db2 (StringBuilder. "connection"))

;; this is the code we are going to replace with a macro
(let [old-val (.toString db2)]
  (.replace db2 0 (.length db2) "new connection")
  (locking db2
    (println (.toString db2)))
  (.replace db2 0 (.length db2) old-val))

;; with-db abstracts away the inner details
(defmacro with-db [db & body]
  `(let [temp# (.toString db2)]
    (try
      (.replace db2 0 (.length db2) ~db)
      (locking db2
        ~@body)
      (finally
       (.replace db2 0 (.length db2) temp#)))))

;; this is much nicer than the let form above.
(with-db "new connection"
   (println (.toString db2)))

11.3 条件付き評価

(if true 'phew (/ 2 0))


(defmacro if3 [test t-case nil-case f-case]
  `(let [expr# ~test]
     (cond
      (nil? expr#) ~nil-case
      (false? expr#) ~f-case
      :default ~t-case)))

(defmacro nif  [expr pos zero neg]
  `(let [expr# ~expr]
    (cond
     (pos? expr#) ~pos
     (zero? expr#) ~zero
     :default ~neg)))


;; Clojure version of Graham's `in` macro
(defmacro in? [needle & haystack]
  ( let [target# needle]
    `(or ~@(map (fn [x#] `(= ~x# ~target#)) haystack))))

(macroexpand-1
 '(in? 1 1 2 3))

;; Just to make sure it is working the way we hope
(in? 1 1 (do (println "called") 2) 3)


;; using lazyness
(defn member? [needle haystack]
  (take 1 (filter (partial = needle)  haystack)))

(member? 2 (iterate inc 1) )

(defmacro in-if [func needle & haystack]
  ( let [target# needle]
    `(or ~@(map (fn [x#] `(~func ~x# ~target#)) haystack))))

;; Clojure's cond already behaves like Graham's >case
(cond
 (do (println "First cond") false) (println "one")
 (do (println "Second cond") true) (println "two")
 (do (println "Third cond") true) (println "three")
 :default (println "Nothing matched"))

第20章 継続

継続についてもう少し勉強しなおす。ccで返ってくるオブジェクトは具体的にどういうものなのかについて理解をもう少し深めておく。言ってしまえばクロージャーだと思うが、どういうものかを把握しておけば継続を"使うとき"にぱっと使えるようになるかなと思うので。


Scheme では現在の継続 (current continuation) を求めると計算処理の未来を表現する 1 引数関数が得られる.このオブジェクトは好きなように保存できるし,保存した継続を呼び出せば,それが生成された時点で始まろうとしていた計算処理を再開する.

継続はクロージャの一般化として理解できる.クロージャとは,関数とそれが生成された時点で見えていたレキシカル変数へのポインタをまとめたものだった.継続とは,関数とそれが生成された時点で溜まっているスタック全体へのポインタをまとめたものだ.継続が評価されると,現在のスタックを無視し,それが保持しているスタックのコピーに基づいて値を返す.継続が T1 時点で生成され T2 時点で評価された場合,T1 時点において溜まっているスタックに基づいた評価が行われる.


> (define frozen)

FROZEN

> (append '(the call/cc returned)
    (list (call-with-current-continuation
            (lambda (cc)
            (set! frozen cc)
            'a))))
(THE CALL/CC RETURNED A)

call/ccaを返すが,最初に継続をグローバル変数frozenに保存している.

frozenを呼び出すと,call/ccの時点での古い計算が再開される.そしてfrozenに渡した値が,call/ccから返される.

> (frozen 'again)
(THE CALL/CC RETURNED AGAIN)

継続は,一度評価されたらなくなってしまう訳ではない.他の普通の関数と同様,繰り返し呼ぶこともできる.

> (frozen 'thrice)
(THE CALL/CC RETURNED THRICE)

継続を他の処理の内部で呼ぶと,古いスタックに立ち戻るという意味がよりはっきり分かる.

> (+ 1 (frozen 'safely))
(THE CALL/CC RETURNED SAFELY)

ここでは,frozenが呼ばれると結果を待っている+は無視される.frozenは,最初に生成された時点で溜まっていたスタックに従い,まずlist,次にappendを通じ,トップレベルまで戻る.frozenが普通の関数呼び出しと同様に値を返していたら,上の式は+1をリストに足し算しようとしたことでエラーになっていただろう.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment