Open Books
O'Reilly では Open Books といって一部の書籍がまるまる読めるらしい。
concurrency を調べて、groovy and concurrency を見ていたら Groovy の O'Reilly 本が目に入る。
そういえばドイツ語の本があったなということで調べてみる。
というわけで公開中でした。
2007年で Groovy 1.1 のころみたい。
Groovy in Action と同じ時期で内容も似ている気がする。
コードだけしか見てないけどオペレータに in も載ってた。
itertools in Groovy
Generator を手に入れたので、再びハミングの問題に挑戦する。
同じように実装するためには itertools が必要だ。
Python documentation に Python で書かれた仕様が載っているのでそのまま実装する。
Changes
実装する際に Generator の方も変更した。
- 名前を gen にした
- worker thread で NoSuchElementException が発生した場合は StopIteration とみなすことにした
- worker thread の例外を拾って main thread で再スローすることにした(izipLongest で必要)
Itertools
class Itertools { static def gen(Closure generator) { return new Iterable() { @Override Iterator iterator() { def queue = new LinkedList() // size == 0 or 1 def getLock = new java.util.concurrent.Semaphore(0) def putLock = new java.util.concurrent.Semaphore(0) def done = false def error = null def yield = { queue.addLast(it) getLock.release() putLock.acquire() } def worker = new Thread({ try { putLock.acquire() generator(yield) } catch (NoSuchElementException stop) { } catch (InterruptedException ignore) { } catch (Throwable t) { error = t } finally { done = true getLock.release() } }) worker.daemon = true worker.start() return new Iterator() { @Override void finalize() { worker.interrupt() } @Override boolean hasNext() { if (!done && queue.empty) { try { putLock.release() getLock.acquire() } catch (InterruptedException e) { worker.interrupt() return false } if (error) throw error } return !done } @Override def next() { if (hasNext()) return queue.removeFirst() throw new NoSuchElementException() } @Override void remove() { throw new UnsupportedOperationException() } } } } } static def iter(iterable) { return iterable.iterator() } // heapq static def merge(Object... iterables) { def iterators = iterables.toList().collect{ iter(it) } return gen{ yield -> def pqueue = new PriorityQueue(iterators.size(), { a, b -> a[0] <=> b[0] } as Comparator) for (iterator in iterators) { if (iterator.hasNext()) pqueue.add([iterator.next(), iterator]) } while (!pqueue.empty) { def (min, iterator) = pqueue.poll() yield(min) if (iterator.hasNext()) pqueue.add([iterator.next(), iterator]) } } } // Infinite Iterators: static def count(start = 0, step = 1) { def n = start return gen{ yield -> while (true) { yield(n) n += step } } } static def cycle(iterable) { def saved = [] return gen{ yield -> for (element in iterable) { yield(element) saved << element } while (saved) for (element in saved) yield(element) } } static def repeat(object, times = null) { return gen{ yield -> if (times == null) while (true) yield(object) else for (i in 0..<times) yield(object) } } // Iterators terminating on the shortest input sequence: static def chain(Object... iterables) { return gen{ yield -> for (iter in iterables) for (element in iter) yield(element) } } static def compress(data, selectors) { return izip(data, selectors).findAll{ d, s -> s }.collect{ d, s -> d } } static def dropwhile(Closure predicate, iterable) { iterable = iter(iterable) return gen{ yield -> for (x in iterable) if (!predicate(x)) { yield(x) break } for (x in iterable) yield(x) } } static def groupby(iterable, Closure key = Closure.IDENTITY) { def iterator = iter(iterable) def target = new Object() def current = target def value = target def queue = [] as Queue def done = false return new Iterator() { @Override boolean hasNext() { if (!done && queue.empty) { try { while (current == target) { value = iterator.next() current = key(value) } target = current def group = gen{ yield -> while (current == target) { yield(value) value = iterator.next() current = key(value) } } queue << [current, group] } catch (NoSuchElementException stop) { done = true } } return !queue.empty } @Override def next() { if (hasNext()) return queue.remove() throw new NoSuchElementException() } @Override void remove() { throw new UnsupportedOperationException() } } } static def ifilter(predicate = { it as boolean }, iterable) { return gen{ yield -> for (x in iterable) if (predicate(x)) yield(x) } } static def ifilterfalse(predicate = { it as boolean }, iterable) { return gen{ yield -> for (x in iterable) if (!predicate(x)) yield(x) } } static def islice(iterable, Object... args) { def (start, stop, step) = args.size() == 1 ? [null, args[0], null] : [*args] start = start != null ? start : 0 stop = stop != null ? stop : Integer.MAX_VALUE step = step != null ? step : 1 def iterator = iter(gen{ yield -> for (int i = start; i != stop; i += step) yield(i) }) int nexti = iterator.next() return gen{ yield -> iterable.eachWithIndex{ element, i -> if (i == nexti) { yield(element) nexti = iterator.next() } } } } static def imap(Closure function = null, Object... iterables) { iterables = iterables.collect{ iter(it) } return gen{ yield -> while (true) { def args = iterables.collect{ it.next() } if (function == null) yield(args) else yield(function(*args)) } } } static def starmap(Closure function, iterable) { return gen{ yield -> for (args in iterable) yield(function(*args)) } } static def tee(iterable, int n = 2) { def iterator = iter(iterable) def queues = (0..<n).collect{ [] as Queue } return queues.collect{ myqueue -> return gen{ yield -> while (true) { if (myqueue.empty) { def newval = iterator.next() for (d in queues) d << newval } yield(myqueue.remove()) } } } } static def takewhile(Closure predicate, iterable) { return gen{ yield -> for (x in iterable) if (predicate(x)) yield(x) else break } } static def izip(Object... iterables) { def iterators = iterables.toList().collect{ iter(it) } return gen{ yield -> while (iterators) yield(iterators.collect{ it.next() }) } } static class ZipExhausted extends RuntimeException {} static def izipLongest(Map keywords = [:], Object... args) { def fillvalue = keywords.get('fillvalue') def counter = args.size() - 1 def sentinel = { return gen{ yield -> if (!counter) throw new ZipExhausted() counter -= 1 yield(fillvalue) } } def filters = repeat(fillvalue) def iterators = args.collect{ iter(chain(it, sentinel(), filters)) } return gen{ yield -> try { while (iterators) yield(iterators.collect{ it.next() }) } catch(ZipExhausted ignore) {} } } // Combinatoric generators: static def product(Map keywords = [:], Object... args) { def pools = args.toList().collect{ it as List } * keywords.get('repeat', 1) def result = [[]] for (pool in pools) result = gen{ yield -> for (x in result) for (y in pool) yield(x+[y]) }.collect() return gen{ yield -> for (prod in result) yield(prod) } } static def permutations(iterable, r = null) { def pool = iter(iterable).collect() def n = pool.size() r = r == null ? n : r return gen{ yield -> for (indices in product(0..<n, repeat:r)) if (indices.unique().size() == r) yield(pool[indices]) } } static def combinations(iterable, r = null) { def pool = iter(iterable).collect() def n = pool.size() return gen{ yield -> for (indices in permutations(0..<n, r)) if (indices.sort(false) == indices) yield(pool[indices]) } } static def combinationsWithReplacement(iterable, r) { def pool = iter(iterable).collect() def n = pool.size() return gen{ yield -> for (indices in product(0..<n, repeat:r)) if (indices.sort(false) == indices) yield(pool[indices]) } } }
itertools 以外のメソッドで iter と merge を用意した。
Test
import static Itertools.* assert iter(merge([1,3,5,7],[2,4],[6])).collect() == [1,2,3,4,5,6,7] // Infinite Iterators: assert iter(count(10)).take(5).collect() == [10,11,12,13,14] assert iter(cycle("ABCD")).take(12).collect() == ['A','B','C','D','A','B','C','D','A','B','C','D'] assert repeat(10,3).collect() == [10,10,10] // Iterators terminating on the shortest input sequence: assert chain('ABC', 'DEF').collect() == ['A','B','C','D','E','F'] assert compress("ABCDEF", [1,0,1,0,1,1]) == ['A','C','E','F'] assert dropwhile({ it < 5 }, [1,4,6,4,1]).collect() == [6,4,1] assert groupby("AAAABBBCCDAABBB").collect{ k, g -> k } == ["A","B","C","D","A","B"] assert groupby("AAAABBBCCD").collect{ k, g -> g.collect().join() } == ["AAAA","BBB","CC","D"] assert ifilter({ it % 2 }, 0..<10).collect() == [1,3,5,7,9] assert ifilterfalse({ it % 2 }, 0..<10).collect() == [0,2,4,6,8] assert islice("ABCDEFG", 2).collect() == ['A','B'] assert islice("ABCDEFG", 2, 4).collect() == ['C','D'] assert islice("ABCDEFG", 2, null).collect() == ['C','D','E','F','G'] assert islice("ABCDEFG", 0, null, 2).collect() == ['A','C','E','G'] assert imap(Math.&pow, [2,3,10], [5,2,3]).collect() == [32,9,1000] assert starmap(Math.&pow, [[2,5], [3,2], [10,3]]).collect() == [32,9,1000] assert takewhile({ it < 5 }, [1,4,6,4,1]).collect() == [1,4] assert izip("ABCD", "xy").collect()*.join() == ["Ax","By"] assert izipLongest("ABCD", "xy", fillvalue:'-').collect()*.join() == ["Ax","By","C-","D-"] // Combinatoric generators: assert product("ABCD", "xy").collect()*.join() == ["Ax","Ay","Bx","By","Cx","Cy","Dx","Dy"] assert product(0..<2, repeat:3).collect()*.join() == ["000", "001", "010", "011", "100", "101", "110", "111"] assert permutations("ABCD", 2).collect()*.join() == ["AB","AC","AD","BA","BC","BD","CA","CB","CD","DA","DB","DC"] assert permutations(0..<3).collect()*.join() == ["012","021","102","120","201","210"] assert combinations("ABCD", 2).collect()*.join() == ["AB","AC","AD","BC","BD","CD"] assert combinations(0..<4, 3).collect()*.join() == ["012","013","023","123"] assert combinationsWithReplacement("ABC", 2).collect()*.join() == ["AA","AB","AC","BB","BC","CC"]
named argument は product で気づいた。
それまでは List で渡していたので Python 風になって満足。
Cyclical Iterators
import static Itertools.* def raymonds_hamming() { def output def deferred_output = gen{ yield -> for (i in output) yield(i) } def (result, p2, p3, p5) = tee(deferred_output, 4) def m2 = gen{ for (x in p2) it(2*x) } def m3 = gen{ for (x in p3) it(3*x) } def m5 = gen{ for (x in p5) it(5*x) } def merged = merge(m2, m3, m5) def combined = chain([1G], merged) output = gen{ groupby(combined).each{ k, g -> it(k) } } return result } assert iter(raymonds_hamming()).take(20).collect() == [1,2,3,4,5,6,8,9,10,12,15,16,18,20,24,25,27,30,32,36] assert iter(islice(raymonds_hamming(), 1690, 1691)).next() == 2125764000 assert iter(raymonds_hamming())[1690] == 2125764000 // assert iter(islice(raymonds_hamming(), 999999, 1000000)).next() == 519312780448388736089589843750000000000000000000000000000000000000000000000000000000
まわった!
ただ、Python に比べると圧倒的に遅い。
そもそも Python で書かれた仕様のための実装と、最適化された実装の itertools の違いもある。
ただ、以前書いた無限リスト版に比べると速いし1000000番目以外は気にならない。
終わりに
TODO をいくつか
- 遅い原因の一つはおそらく Thread でそれ自体は仕方が無いことだが、ThreadPool を使用すれば多少改善されると思われる
- Iterable で返しているので2度 iterator を呼び出すことができるがそのときの振る舞いが正しいのかわからない
def c = count(10) assert c in Iterable assert takewhile({ it < 15 }, c).collect() == [10,11,12,13,14] assert takewhile({ it < 15 }, c).collect() == [] // or [10,11,12,13,14] ?
count の場合 Python でもこのように動作したが、他は確認していない。
とりあえず、Python と同じように記述できるようになっただけでうれしい。
2012-04-18 更新
permutations の引数が 0 のときの動作が違ったので修正した
for (x in permutations([1,2,3], 0)) println x // 修正前 // [1, 2, 3] // [1, 3, 2] // [2, 1, 3] // [2, 3, 1] // [3, 1, 2] // [3, 2, 1] // 修正後 // []
Named arguments
コンパイラさんに教えてもらった。
def aMethodHasNamedArguments(Map named=[:], Object... args) { println "named=$named, args=$args" } aMethodHasNamedArguments("A", key1:1, "B", key2:2, "C") // named=[key1:1, key2:2], args=[A, B, C] aMethodHasNamedArguments() // named=[:], args=[]
Groovy 1.8.6 の AntlrParserPlugin より
if (namedArguments) { if (!expressionList.isEmpty()) { // let's remove any non-MapEntryExpression instances // such as if the last expression is a ClosureExpression // so let's wrap the named method calls in a Map expression List<Expression> argumentList = new ArrayList<Expression>(); for (Object next : expressionList) { Expression expression = (Expression) next; if (!(expression instanceof MapEntryExpression)) { argumentList.add(expression); } } if (!argumentList.isEmpty()) { expressionList.removeAll(argumentList); checkDuplicateNamedParams(elist, expressionList); MapExpression mapExpression = new MapExpression(expressionList); configureAST(mapExpression, elist); argumentList.add(0, mapExpression); ArgumentListExpression argumentListExpression = new ArgumentListExpression(argumentList); configureAST(argumentListExpression, elist); return argumentListExpression; } } checkDuplicateNamedParams(elist, expressionList); NamedArgumentListExpression namedArgumentListExpression = new NamedArgumentListExpression(expressionList); configureAST(namedArgumentListExpression, elist); return namedArgumentListExpression; } else {
名前付き引数は先に処理されるようだ。
Generator in Groovy
同期する Generator を java.util.concurrent パッケージを使って書いてみる。
Using BlockingQueue
はじめに http://groovy.codehaus.org/Iterator+Tricks のように非同期のものを BlockingQueue で書いてみる。
def generate(Closure generator) { return new Iterable() { @Override Iterator iterator() { def queue = new java.util.concurrent.LinkedBlockingQueue(1) def done = false def yield = { queue.put(it) } def worker = new Thread({ try { generator(yield) } catch (InterruptedException ignore) { } finally { done = true } }) worker.daemon = true worker.start() return new Iterator() { @Override void finalize() { worker.interrupt() } @Override boolean hasNext() { return !done || !queue.empty } @Override def next() { if (hasNext()) { try { return queue.take() } catch (InterruptedException ignore) {} } throw new NoSuchElementException() } @Override void remove() { throw new UnsupportedOperationException() } } } } }
def fibo(yield) { def a = 0 def b = 1 while (true) { yield(b) (a, b) = [b, a+b] } } assert generate(this.&fibo).find{ it % 20 == 0 } == 832040
フィボナッチ数を求めるような場合は問題ないが Python や JavaScript の yield と同じようには扱えない。
なぜ同じように扱えないのか?
問題点その1 iterator() を呼び出した時点で worker thread が起動する
def counter = generate { yield -> for (i = 0; ; i++) { println "put $i" yield(i) } }.iterator() // iterator() を呼び出した時点で put している // put 0 // put 1
問題点その2 put が先行する
def counter = generate { yield -> for (i = 0; ; i++) { println "put $i" yield(i) } }.iterator() 5.times { println "get ${counter.next()}" } // get の回数より put の回数が多い // put 0 // get 0 // put 1 // put 2 // put 3 // get 1 // get 2 // get 3 // put 4 // put 5 // put 6 // get 4
問題点その3 BlockingQueue に null を渡せない
for (x in generate{ it(null) }) println x // Exception in thread "Thread-2" java.lang.NullPointerException
Using CountDownLatch
問題点1を解決する。
CountDownLatch は合図があるまでスレッドを待機させることができるのでこれを使う。
Worker Thread をスレッド起動直後に待機させ、最初の hasNext() 呼び出しで合図を送って開始させる。
def generate(Closure generator) { return new Iterable() { @Override Iterator iterator() { def queue = new java.util.concurrent.LinkedBlockingQueue(1) def signal = new java.util.concurrent.CountDownLatch(1) def start = false def done = false def yield = { queue.put(it) } def worker = new Thread({ try { signal.await() generator(yield) } catch (InterruptedException ignore) { } finally { done = true } }) worker.daemon = true worker.start() return new Iterator() { @Override void finalize() { worker.interrupt() } @Override boolean hasNext() { if (!start) { start = true signal.countDown() } return !queue.empty || !done } @Override def next() { if (hasNext()) { try { return queue.take() } catch (InterruptedException ignore) {} } throw new NoSuchElementException() } @Override void remove() { throw new UnsupportedOperationException() } } } } }
Using SynchronousQueue & Semaphore
問題点2を解決する。
BlockingQueue の実装クラスには SynchronousQueue という常に空のキューがある。
常に空でキューなのかということだが put したと同時に take するので常に空ということらしい。
これで解決したかと思ったのだけども LinkedBlockingQueue で1個置き場所があって最大2個ずれていたので置き場所がなくなっても1個ずれる。
結局、Semaphore を使ってスレッドが同時に2つ動作しないように管理しないといけない。
Semaphore は開始時の制御にも使えるので CountDownLatch はもう使用しない。
Worker Thread の開始直後と、put 直後は Worker Thread を一度止めて、次の hasNext が呼ばれるまで待機するようにする。
def generate(Closure generator) { return new Iterable() { @Override Iterator iterator() { def queue = new java.util.concurrent.SynchronousQueue() def lock = new java.util.concurrent.Semaphore(0) def value = [] def DONE = new Object() def yield = { queue.put(it) lock.acquire() } def worker = new Thread({ try { lock.acquire() generator(yield) } catch (InterruptedException ignore) { } finally { queue.put(DONE) } }) worker.daemon = true worker.start() return new Iterator() { @Override void finalize() { worker.interrupt() } @Override boolean hasNext() { if (value.empty) { try { lock.release() value[0] = queue.take() } catch (InterruptedException e) { value[0] = DONE } } return value[0] != DONE } @Override def next() { if (hasNext()) return value.remove(0) throw new NoSuchElementException() } @Override void remove() { throw new UnsupportedOperationException() } } } } }
Using Two Semaphores
問題点3を解決する。
そもそも BlockingQueue には null を渡すことができない。
変わりに LinkedList を使い BlockingQueue が行っていた同期処理をもうひとつの Semaphore で行う。
def generate(Closure generator) { return new Iterable() { @Override Iterator iterator() { def queue = new LinkedList() // size == 0 or 1 def getLock = new java.util.concurrent.Semaphore(0) def putLock = new java.util.concurrent.Semaphore(0) def done = false def yield = { queue.addLast(it) getLock.release() putLock.acquire() } def worker = new Thread({ try { putLock.acquire() generator(yield) } catch (InterruptedException ignore) { } finally { done = true getLock.release() } }) worker.daemon = true worker.start() return new Iterator() { @Override void finalize() { worker.interrupt() } @Override boolean hasNext() { if (!done && queue.empty) { try { putLock.release() getLock.acquire() } catch (InterruptedException e) { worker.interrupt() return false } } return !done } @Override def next() { if (hasNext()) return queue.removeFirst() throw new NoSuchElementException() } @Override void remove() { throw new UnsupportedOperationException() } } } } }
同期しながら動作し、null を渡すことも可能
def counter = generate { yield -> for (i = 0; ; i++) { println "put $i" yield(i) } }.iterator() 5.times { println "get ${counter.next()}" } // put と get は1対1で同期している // put 0 // get 0 // put 1 // get 1 // put 2 // get 2 // put 3 // get 3 // put 4 // get 4 assert generate{ it(null) }).iterator().next() == null
使ってみる
def counter = generate { yield -> (1..5).each{ yield(it) } } for (i in counter) println i // counter は Iterable なので再利用可能 for (i in counter) println i // リスト内包表記のようにも書ける println generate{ for (x in 1..3) for (y in 1..3) it(x*y) }.iterator().take(10).collect() // この場合は iterator の方が都合がよいのでメソッドを定義する def gen(Closure generator) { generate(generator).iterator() } println gen{ for (x in 1..3) for (y in 1..3) it(x*y) }.take(10).collect() // ただし、これは Ruby の tap のようなものがあれば同じように書けてしまう def tap(obj, Closure doto) { obj.with(doto) return obj } println tap([]){ for (x in 1..3) for (y in 1..3) it << x*y }
終わりに
Generator を実装するのと concurrent パッケージを使用するという両方の目的で書いてみた。
TiddlyWiki の設定
TiddlyWiki の Tips を調べていたら、設定(使用しているプラグラインとか)を公開されている方がいたので自分も公開してみる。
TiddlyWiki とは
- 開発者は Jeremy Ruston 氏
- HTML ファイル1つでできている Wiki なので tiddlywiki.com からダウンロードしてくるだけで使える
この説明がわかりやすいかも
TiddlyWiki を何に使っているのか
TiddlyWiki はいろいろな用途で使われているけれども自分の場合はプログラミング関連の本やブログを読みながらメモするのに使っている。*1
Write once, read anytime
自分の TiddlyWiki のサブタイトルに書いてあった。*2
読んでみて、コードを書いてみて、実行して、まとめてみないと理解できない体質なので、本に書かれたコードを打ち込むのだけど、それを残している。
そして、必要になった時に貼り付けて実行。
例えば、今『プログラミング Clojure』を読んでいてこんな感じ。
(def foo 10) ; => #'user/foo foo ; => 10 (.start (Thread. (fn [] (println foo)))) ; => nil ; 10 ; Clojure 1.3 から var はデフォルトでは dynamic ではない (binding [foo 42] foo) ; => IllegalStateException Can't dynamically bind non-dynamic var: user/foo ; foo を dynamic にする ; (def #^{:dynamic true} foo foo) ; dynamic な foo を定義する (def ^:dynamic foo 10) ; => #'user/foo (binding [foo 42] foo) ; => 42 ; let と binding の違いを見る (defn print-foo [] (println foo)) ; => #'user/print-foo ; let は自分のフォームの外側には影響を及ぼさない ; ルート束縛である 10 を表示する (let [foo "let foo"] (print-foo)) ; 10 ; => nil ; binding は束縛の影響をそのボディから呼び出されるコードすべてに及ぶ (binding [foo "bound foo"] (print-foo)) ; bound foo ; => nil
使用中のプラグイン
SectionLinksPlugin
- 本を読むときは目次が重要なので
NestedSlidersPlugin
- コードが長かったりすると閉じたり開いたりできる方が便利
http://www.math.ist.utl.pt/~psoares/MathSVG.html
- ときには数式が必要になることもあるので
Cafe de Shrimp
タグの運用
現在、3つの軸で管理している。
- 言語 (.clj とか拡張子)
- ジャンル (algorithm, tool とか全部小文字)
- タイトル (書籍やブログのタイトルなど、大文字から始める)
『プログラミング Clojure』では章ごとに管理した。
以前は1つにしていたのだが、途中に書き加えるときに不便だったのでできるだけ小分けにするようにしている。
「Programming Clojure」 という tiddler を作成して、書籍に関連するサイトなどのリンクを書いておく。*4
そして同じ名前でタグ付け。*5
[[Programming Clojure]] .clj
各章ごとには
[[Programming Clojure]]
だけタグ付けしている。
TiddlyWiki ではタグにタグを付けられる。
タグ名の tiddler を用意するとそのタグでタグ付けされている一覧が表示されるので目次代わりにもなる。
終わりに
TiddlyWiki 使い始めた当初はインストールログやブックマークのまとめだけだったので、書いていることも変わってきている。
運用してみて、自分で書いたものを自分で読んでみることで自分のスタイルに合った使い勝手になってきた。
注意点としてはプラグインごとに記法がある場合、量が増えるに従ってプラグインの変更がしにくくなる。
両方適用するか、テキストファイルなので置換するという手もあるが最初から意識しておいた方がいいかもしれない。
自分の場合はプラグインの JavaScript を修正したので今度はプラグインの更新が簡単にできなくなった。
もうひとつ Wiki なのでコンソールに貼り付けないとコードは実行できないという欠点がある。
このあたりは高機能エディタとかで snippet を管理する方が優れているのかもしれない。*6
とはいえ、非常に満足しているツールなので自分の設定と運用法を書いてみた。
2012-03-26 追記
タブ化プラグインは便利なので入れた。
前回表示されなかったのはスタイルの設定が足りなかったかららしい。
Dan Ariely 氏の『予想どおりに不合理』を読んで面白かったのでもう少し追記しておく。
氏の実験と観察から得られた結果を自分なりにまとめると
- 人間は相対評価しかできない
- 目先のことで判断を誤るのは優秀な頭脳でも同じ
の2点だ。
2番目は特筆に価する。
理解すること自体が難しい問題はともかく、問題に対する判断力は頭脳で決まるわけではないということだ。
結局地道な相対評価を繰り返して価値観を高めていくしかない。
本読んでまとめるのはアンチパターンの部分もあるかもしれないけど継続できているので今の自分に合っている。
参照したときに自分で書いたものを評価することになるので成長できているか判断できる点もいいと思った。
Groovy で Emulating callable objects その2
前回の Closure よりもっと簡単にできた。
というか Python と同じだった。
Python と同じ方法
org.codehaus.groovy.runtime.ScriptBytecodeAdapter より
// TODO: set sender class public static Object invokeClosure(Object closure, Object[] arguments) throws Throwable { return invokeMethodN(closure.getClass(), closure, "call", arguments); }
つまり、call メソッドが実装されていれば呼び出せるようだ。
これなら Closure 以外のインスタンスとして扱える。
class HasCallMethod { def call(int i) { i+1 } } def obj = new HasCallMethod() assert obj instanceof Closure == false assert obj(1) == 2
ちなみに、Closure には doCall を呼び出す call メソッドが最初から存在するので doCall で拡張できる。
正規の方法かは不明だが、調べたら使っている人達もいるようだ。
前回の TODO: Generator が動作しなかった理由*1 *2
Iterator#next が呼ばれたから次の値を取得するのではなく、next の前に値が用意されることもある。
したがって、副作用を期待するとエラーになる。
N = 10 i = 0 N.times{ i++ } assert N == i i = 0 interator = new Generator({ yield -> while(true) { i++ println(i) yield(i) } }).iterator() N.times{ println "next()" interator.next() } assert i == N // error: N <= i <= N+2
出力
pullValue() と doCall() は同期の中にログを仕込んだ。
next() 1 doCall(1) 2 pulledValue = 1 doCall(2) 3 next() pulledValue = 2 next() doCall(3) 4 pulledValue = 3 next() doCall(4) 5 pulledValue = 4 next() doCall(5) 6 pulledValue = 5 next() doCall(6) 7 pulledValue = 6 next() doCall(7) 8 pulledValue = 7 doCall(8) 9 next() pulledValue = 8 next() doCall(9) 10 pulledValue = 9 next() doCall(10) 11 pulledValue = 10 doCall(11) 12
doCall() が呼ばれて pulledValue が呼ばれる順序は必ず守られる。
ただ pulledValue が参照されると次の doCall を呼び出してよくなり、yield でブロックされる前に評価されるので最大2ずれる。
fibonacci 数の計算では副作用がないのでうまく動作したが Tiny Prolog では env を副作用を参照するのでエラーになってしまった。
*1:エントリ書いたので前回への追記からこっちへ移動した
*2:http://groovy.codehaus.org/Iterator+Tricks の Generator のこと
Groovy で Emulating callable objects
Python では object.__call__ を定義するとオブジェクトを呼び出せるそうだ。
Scala でも apply メソッドで同じようにオブジェクトを呼び出すことができる。
Groovy で同じようにするにはどうすればよいか。
そもそもの問題
『7つの言語 7つの世界』で Prolog の章を読んで面白かったのでいろいろ探していたらOKIソフトウェアの鈴木氏の Tiny Prolog シリーズを発見した。
他に Ruby, Java, C# 版もある。
理解するために Groovy に翻訳していた。
ここで object.__call__ が使われていて、別に Groovy では object._() のように目立たないメソッド呼出しにすればいいだけなのだが悔しいので粘って見た。
むしろ問題は Generator の方なんだけどこっちは検索して見たら Java で実装されたものが見つかったのでそれを使った。
Groovy の Cookbook Examples にも Generator が載っているがこれではうまく動かなかった。*1
ログが実行毎に違ったりしたのでスレッドの競合かもしれない。
でもコードを読んでも pullValue() と doCall() は排他できていてるように見える。
何故うまく動かないのか原因がわからない。
それは TODO にしておいて Groovy で Object を呼び出すにはどうすればよいか。
Closure#setDelegate(object)
Groovy では Object を呼び出すことはできないのだけど Closure が属している Object を変更できることを思い出した。
いつもは Builder で使うときのように Closure の内側から delegate を呼び出すが、外側からも呼び出せたのだ。
class Person { def name @Override String toString() { name } } def robert = new Person(name:"Robert") println robert println() println "// new Closure(owner /*, null*/)" bob = new Closure(robert) { def doCall() { "Hi!" } def doCall(String s) { "My name is $name." } // def doCall(String s) { "My name is ${this.name}." } Error @Override String toString() { "Bob" } } println bob println bob() println bob("What's your name?") println bob.name println() println "// new Closure(null, thisObject)" bob = new Closure(null, robert) { def doCall() { "Hi!" } // def doCall(String s) { "My name is $name." } Error def doCall(String s) { "My name is ${this.name}." } @Override String toString() { "Bob" } } println bob println bob() println bob("What's your name?") // println bob.name Error println() println "// Closure#setDelegate(obj)" bob = { "My name is ${name}." } // bob = { "My name is ${this.name}." } Error bob.delegate = robert println bob println bob() println bob("What's your name?") println bob.name println()
Robert // new Closure(owner /*, null*/) Bob Hi! My name is Robert. Robert // new Closure(null, thisObject) Bob Hi! My name is Robert. // Closure#setDelegate(obj) emulating_callable_objects$_run_closure1@2f60877b My name is Robert. My name is Robert. Robert
Closure の doCall をオーバーロードすれば呼び出しのパターンも増やせる。
ただし、instanceof で調べても Closure でしかないが今回は問題ない。
Person が Closure を継承するようにしてもよいが Closure は特別なクラスなので簡単には拡張できない。
Closure が static コンテキストで生成された場合
this に何が設定されているか気になったのでついでに調べて見た。
class A { def foo() { return {} } static def bar() { return {} } } def a = new A() assert a.foo().owner == a assert A.bar().owner == A assert a.foo().thisObject == a assert A.bar().thisObject == A assert a.foo().delegate == a.foo().owner assert A.bar().delegate == A.bar().owner
static コンテキストで生成された場合は、生成したクラスが設定されている。
追記 2012-02-25
環境は Groovy 1.8.6 で確認
『Groovy・イン・アクション』によると JSR になる前の Groovy では Closure 内の this は Closure 自身を参照していたらしい。
JSR エキスパートグループによって現在の形に変更されたそうだ。
Groovy 1.8.6 では Ruby の each_cons や each_slice の動作をする List#collate が追加されていた。
未だに startGroovy.bat の問題がそのままだが patch 当てれば OK
*1:掲載されているサンプルは動作する