なんの役に立つのかわからんが書いてみるシリーズ。
SLIBに同名のものがあるけど方向性がちがう。 こっちは単なるhash-tableもどき。
(use srfi-1) (use gauche.collection) (define-class <alist-table> (<collection>) ((key= :getter key=-of :init-keyword :key= :init-value eq?) (alist :accessor alist-of :init-value '()))) (define (make-alist-table . maybe-key=) (make <alist-table> :key= (get-optional maybe-key= eq?))) (define-method alist-table-get ((self <alist-table>) key . maybe-default) (cond ((assoc key (alist-of self) (key=-of self)) => cdr) ((null? maybe-default) (error "alist table doesn't have an entry for key" key)) (else (car maybe-default)))) (define-method alist-table-put! ((self <alist-table>) key val) (cond ((assoc key (alist-of self) (key=-of self)) => (lambda (kv) (set-cdr! kv val))) (else (push! (alist-of self) (cons key val))))) (define-method alist-table-exists? ((self <alist-table>) key) (if (assoc key (alist-of self) (key=-of self)) #t #f)) (define-method alist-table-delete! ((self <alist-table>) key) (receive (kv alist) (assoc+delete1! key (alist-of self) (key=-of self)) (if kv (begin (set! (alist-of self) alist) #t) #f))) (define-method call-with-iterator ((self <alist-table>) proc . args) (let1 p (alist-of self) (proc (lambda () (null? p)) (lambda () (pop! p))))) (define (find-tail+prev pred lis) (let loop ((lis lis) (prev '())) (if (null? lis) (values #f #f) (if (pred (car lis)) (values lis prev) (loop (cdr lis) lis))))) (define (assoc+delete1! key alist . args) (let1 = (get-optional args eq?) (receive (tail prev) (find-tail+prev (lambda (elt) (= (car elt) key)) alist) (if tail (if (null? prev) (values (car tail) (cdr alist)) (begin (set-cdr! prev (cdr tail)) (values (car tail) alist))) (values #f alist))))) (use gauche.test) (test "" '((a . 1) ((b . 2) (c . 3))) (lambda () (receive r (assoc+delete1! 'a '((a . 1) (b . 2) (c . 3))) r))) (test "" '((b . 2) ((a . 1) (c . 3))) (lambda () (receive r (assoc+delete1! 'b '((a . 1) (b . 2) (c . 3))) r))) (test "" '((c . 3) ((a . 1) (b . 2))) (lambda () (receive r (assoc+delete1! 'c '((a . 1) (b . 2) (c . 3))) r))) (test "" '(#f ((a . 1) (b . 2) (c . 3))) (lambda () (receive r (assoc+delete1! 'd '((a . 1) (b . 2) (c . 3))) r))) (define a (make-alist-table)) (alist-table-put! a 'a 1) (alist-table-put! a 'b 2) (alist-table-put! a 'c 3) (alist-table-put! a 'a 4) (test "" #t (lambda () (alist-table-delete! a 'b))) (test "" #f (lambda () (alist-table-delete! a 'b))) (test "" 4 (lambda () (alist-table-get a 'a))) (test "" #f (lambda () (alist-table-get a 'b #f))) (test "" #t (lambda () (alist-table-exists? a 'a)))
Shiro 2003/01/11 21:07:02 PST: prevが必要なのって、callerがset-cdr!したいから ですよね。srfi-1はわりと関数型のアプローチに見えるので、そういう副作用を 前提とするアプローチは避けたんじゃないでしょうか。srfi-1には確かに!付き 手続きも多数定義されていますが、あくまで「実装は渡されたリストのセルを 再利用してもよい」というadvisoryな仕様であって、純粋に副作用無しで 実装することも許していますね。
write barrier使った世代別GCで、 新しいセルはstack allocationするような処理系だと、もしかするとset-cdr!より セルをコピーした方が速いなんてことはあるんでしょうか。 ありそうな気もするけど、そのへんちゃんと測ったこと無いのでわかりません。
あとfind等で2つ値を返すようにするのはCommonLispなんかで良くありますが、 CLでは使わない返り値が無視できるのに対し、Schemeでは受け手もきっちり 同じ数で受けなきゃならないので、嫌ったのかも。
memoize。いわゆるひとつのmemoize。 テキストや論文にはよく出てくるくせに 実際に使われているのをめったに見ない謎の関数。 くわしくはこっち。
http://www.geocities.co.jp/SiliconValley-PaloAlto/7043/#memoize
(ていうかぜんぜん説明してないやろ、自分)
なお、PerlやRubyにもmemoizeモジュールがある。 でもってやっぱりあんまり使われていないみたい。
Schemeで書いてあるなかでいちばん凝ってそうなのがこれ。
http://www.mit.edu/afs/sipb/project/mechanics/scmutils/src/general/memoize.scm
R5RSだとこんな感じ。
;; In R5RS (define (memoize fn) (let ((cache '())) (lambda args (apply values (or (cond ((assoc args cache) => cdr) (else #f)) (call-with-values (lambda () (apply fn args)) (lambda result (set! cache (cons (cons args result) cache)) result)))))))
どこにでもあるmemoize。 多値を扱えるのが特徴といえば特徴。
でもハッシュがあるならやっぱりそっちを使いたいよね。
Gaucheだとこんなん。
;; In Gauche (define (memoize fn) (let1 cache (make-hash-table 'equal?) (lambda args (apply values (or (hash-table-get cache args #f) (receive result (apply fn args) (hash-table-put! cache args result) result))))))
よくある例。
(define (fib n) (cond ((= n 0) 0) ((= n 1) 1) (else (+ (fib (- n 1)) (fib (- n 2)))))) (set! fib (memoize fib)) (fib 100) => 354224848179261915075
よくない例。
(use srfi-27) (set! random-real (memoize random-real)) ;memoized random (random-real) => 0.9575068354342976 (random-real) => 0.9575068354342976 (random-real) => 0.9575068354342976
引数にキーワードが入ってると 「equal?じゃないけど意味的にはいっしょ」 な場合がありえるんですけどどうしたらいいですかせんせー。
ほっとけ。引数をどう扱うかなんてそんなもん関数の勝手だ。 気にせんでよろしい。
上のmemoizeはバックエンドがハッシュに固定されているので おもしろくない。 変えられるようにしてみる。
まずは単純に可変要素を引数にした例。
(define (memoize fn . args) (let-optionals* args ((init (make-hash-table 'equal?)) (put! hash-table-put!) (get (lambda (cache args) (hash-table-get cache args #f)))) (let1 cache init (lambda args (apply values (or (get cache args) (receive result (apply fn args) (put! cache args result) result)))))))
意味もなくキューをキャッシュにしてみる。
(use util.queue) (set! fib (memoize fib (make-queue) (lambda (cache args result) (queue-push! cache (cons args result))) (lambda (cache args) (cond ((find-in-queue (lambda (e) (equal? (car e) args)) cache) => cdr) (else #f)))))
上の例はこんなふうに書いたほうがよりScheme的かもしれない。
(define (memoize fn put! get) (lambda args (apply values (or (get args) (receive result (apply fn args) (put! args result) result))))) (use util.queue) (define (memoize-queue fn) (let1 cache (make-queue) (define (put! args result) (queue-push! cache (cons args result))) (define (get args) (cond ((find-in-queue (lambda (e) (equal? (car e) args)) cache) => cdr) (else #f))) (memoize fn put! get))) (set! fib (memoize-queue fib))
なお、memoizeを上のようにしておけば、 キューなんてもちださなくてもassocで素直に書ける。
(define (memoize-assoc fn) (let1 cache '() (define (put! args result) (set! cache (acons args result cache))) (define (get args) (cond ((assoc args cache) => cdr) (else #f))) (memoize fn put! get))) (set! fib (memoize-assoc fib))
ハッシュ版はこう。
(define (memoize-hash fn) (let1 cache (make-hash-table 'equal?) (define (put! args result) (hash-table-put! cache args result)) (define (get args) (hash-table-get cache args #f)) (memoize fn put! get))) (set! fib (memoize-hash fib))
むかしのLispの教科書だとこのへんで「おおLispの拡張性はすばらしい」 となるのかもしらんが、 いまどきこれで感心してくれる人はいないと思う。
次回は(つづくんかい)OO的なアプローチについて。
混迷の現代をうろうろ切る(迷惑)。
いやーmemoizeってけっこうえらかったのね。 てきとうに 検索にひっかかったやつ を読んでたら、 memoizeの源流は1968年のNatureに載った記事にさかのぼるらしい。 Natureって、あのNatureだよねえ?
Schemeだとget, put!はクロージャとして渡すところだけど、 今のプログラマーなら 「get, put!というふたつのメッセージを受け取るオブジェクトを渡す」 と考える人のほうが圧倒的に多いと思う。 少なくともOO系言語ならそうする。現にRubyのmemoizeはそうなってる。
Gaucheだと、次のようにすれば <hash-table>がget, put!メッセージを受け取る感じになる。
(define-method get ((self <hash-table>) key) (hash-table-get self key #f)) (define-method put! ((self <hash-table>) key val) (hash-table-put! self key val)) (define h (make-hash-table)) (put! h 'a 1) (get h 'a) => 1
で、memoizeをキャッシュとなるオブジェクトを取るように書き直す。
(define (memoize fn cache) (lambda args (apply values (or (get cache args) (receive result (apply fn args) (put! cache args result) result))))) (set! fib (memoize fib (make-hash-table 'equal?)))
Gaucheのメソッドディスパッチはあまり速くないらしいが、 この場合クロージャ渡し版と比べてたいした差は見られない。
クロージャ版。
;(time (fib 10000)) ; real 23.829 ; user 23.711 ; sys 0.047
OO版。
;(time (fib 10000)) ; real 24.581 ; user 24.500 ; sys 0.047
ということでいとも簡単に終わってしまったので、 別のネタをひねり出すことにする。
「Lispだとオブジェクトシステムがほうらこんなに簡単」みたいなのは いろいろ見たけど、そのなかでは Soft Objects がいちばんのお気に入り。なにしろ単純でいい。 ちなみにCGの世界でいうsoft objectsとは全然関係なし。 どんなもんかというと、
つまりここでは「階級制を打破して大いなる一者に回帰すべし」という まことに日本人ごのみというかドイツ人ごのみというかな 思想が表明されているわけだ(ちがう)。 クラスがないあたりちょっと Self に似てなく もない。
まあうだうだ説明するより現物を見てもらったほうが早いと思う。 ミニマムな実装を論文から抜いてきた。
;; Taken from: ;; Ken Hasse, "Soft Object: A Paradigm for Object Orient Programming" ;; (ftp://publications.ai.mit.edu/ai-publications/pdf/AIM-1014.pdf) (define (make-new-object old-object handlers) (define (new-object message . arguments) (if (eq? message 'handle) (make-new-object new-object arguments) (let ((local-handler (mget message handlers))) (if local-handler (apply local-handler arguments) (apply old-object message arguments))))) new-object) (define (mget message handlers) (if (null? handlers) #F (if (eq? (car handlers) message) (cadr handlers) (mget message (cddr handlers))))) (define (ignoramus message . arguments) (error "Can't handle message" message arguments)) (define ur-object (make-new-object ignoramus ()))
こんだけ。なにそれ。
いちおう上のコードを説明する。 "ur" はドイツ語から来た語でprimaryみたいな意味。 "ignoramus" はラテン語から来た語で「ものしらず」。 ってコードの説明じゃないけど、つまずきそうなのはこれくらいのもんでしょ。 なお、 EDICT でignoramusを引くと、「盲」「不束者」と出る。そう読みかえるのも一興。
例を見ないとようわからんという向きのために(おれだ)実際に使ってみる。 キャッシュオブジェクトを定義する。
(define cache (let1 h (make-hash-table 'equal?) (ur-object 'handle 'get (lambda (key) (hash-table-get h key #f)) 'put! (lambda (key val) (hash-table-put! h key val)))))
ここではur-objectを変形してcacheというオブジェクトを生成している。 オブジェクトが括弧の先頭に来るのがいいやね。
let1の下をいんちきOO言語で書くと、
ur-object.handle('get => (lambda (key) (hash-table-get h key #f)), 'put! => (lambda (key val) (hash-table-put! h key val)))
といったところか。 つまり、ur-objectに対して 「これこれこういうメッセージを処理するオブジェクトを作ってね。 getが来たらこう、put!が来たらこう」という指令を出しているわけだ。 いいかえれば、原初の一を棒でつつくとたちどころに懐胎し、 光のアムダを産み落とすという筋書き。
アムダ(もういいって)をさらにつついてみる。
(cache 'put! 'a 1) (cache 'get 'a) => 1
上で定義したcacheを取るようにmemoizeを書き換える。
(define (memoize fn cache) (lambda args (apply values (or (cache 'get args) (receive result (apply fn args) (cache 'put! args result) result))))) (set! fib (memoize fib cache))
あんまり変わんないね。 スピードはさすがに遅くなるだろうと思ったら、こっちもたいして変わらない。
;(time (fib 10000)) ; real 23.403 ; user 23.336 ; sys 0.031