『クロージャによる超軽量並行プロセス』を Squeak Smalltalk で 2


id:sumim:20070618:p1 からの続き。


フリップフロップ機構はなんのために必要なのかな…と疑問だったので、状態遷移など調べていて謎が解けました。元記事OCaml の例では、Senders と Receivers が同時に必要となる状況がないのを利用して、空となったそれぞれを使い捨てしているのですね(もっとも細かいことを言えば、使い捨てといえば要素の増減時にすでに使い捨ててるわけですが、まあ、そこはあくまで私のメンタルモデルとして)。それで、... Senders [v, v, v]、Senders [v, v]、Senders [v] と Receivers [f] 、Receivers [f, f]、Receivers [f, f, f] ...の間を行き来するのにフリップフロップが必要だった、ということが分かったところで、より忠実にその動きを再現して遊んでみることにしました。


内輪ネタ的にはクック言うところの「型による抽象化」手法がとられているので、Senders や Receivers は単なる順序付きコレクションではだめで、しかるべく型情報を持たせておく必要があります。また、チャンネルも(型情報こそ必要ありませんが)参照としての振る舞いをさせるために、なんらかの工夫が必要です。普通ならここでクラスの出番となるわけですが、あえてそれをしないとすれば、関連付けオブジェクト(an Association)を使ってみてはどうかと思いつきました。


関連付けオブジェクトは「キー」と「値」を key -> value と記述(リテラル式っぽいですが、れっきとしたメッセージ式です…)することで生成できるオブジェクトで、二つの値を関連付けして扱うのに便利なものです。Smalltalk では辞書(a Dictionary。他の言語では一般的にハッシュ)の要素として利用されています。


通信用のキューおよび参照を実現するために、この関連付けオブジェクトの「キー」にシンボルで模した型情報を、「値」にキュー代わりの順序付きコレクションを割り振ってみてはどうかと思ってそのように書いてみました。

| newc send recv c repeat servc serv r fibc fib nn |

newc := [#Senders -> OrderedCollection new].

send := [:ch :vals |
    ch key caseOf: {
        [#Senders] -> [ch value add: vals].
        [#Receivers] -> [ch value
            ifEmpty: [ch key: #Senders value:  (OrderedCollection with: vals)]
            ifNotEmpty: [ch value removeFirst valueWithArguments: vals]]}].

recv := [:ch :proc |
    ch key caseOf: {
        [#Receivers] -> [ch value add: proc].
        [#Senders] -> [ch value
            ifEmpty: [ch key: #Receivers value: (OrderedCollection with: proc)]
            ifNotEmpty: [proc valueWithArguments: ch value removeFirst]]}].

World findATranscript: nil.  Transcript clear.

"整数を受信して画面に表示"
c := newc value.
send value: c value: {3}.
recv value: c value: [:x | Transcript cr; show: x].

"何回でも c から整数を受信して画面に表示"
repeat := [recv value: c value: [:x | Transcript cr; show: x. repeat value]].
repeat value.
(1 to: 3) do: [:i | send value: c value: {i}].

"関数サーバー"
servc := newc value..
serv := [
    recv value: servc value: [:i :recp |
        send copy value: recp value: {i * i}.
        serv value]].
serv value.
r := newc value.
send copy value: servc value: {123. r}.
recv value: r value: [:j |  Transcript cr; show: j].
send copy value: servc value: {45. r}.
recv value: r value: [:j |  Transcript cr; show: j].

"フィボナッチ数列"
fibc := newc value.
fib := [
    recv value: fibc value: [:n :repc |
        fib fixTemps value.
        n <= 1
            ifTrue: [
                send value: repc value: {n}]
            ifFalse: [
                | repc1 repc2 |
                repc1 := newc value.
                repc2 := newc value.
                send copy value: fibc value: {n - 1. repc1}.
                send copy value: fibc value: {n - 2. repc2}.
                recv copy value: repc1 value: [:rep1 |
                    recv copy value: repc2 value: [:rep2 |
                        send copy value: repc value: {rep1 + rep2}]]]]].
fib fixTemps value.
nn := 10.
r := newc value.
send copy value: fibc value: {nn. r}.
recv copy value: r value: [:m | Transcript cr; show: ('fib({1}) = {2}' format: {nn. m})].
"=> fib(10) = 55 "

もくろみ通り。機能してくれるようです。


例のごとく Ruby でも。Ruby には Smalltalk の an Association に当たるオブジェクトはありませんが、なんちゃって版を Association として定義。#-> はメソッド名として使えないので Symbol#>= を定義してこれを代わりに用います。

class Association; attr_accessor :key, :val; def initialize(k,v); @key=k; @val=v end end
class Symbol; def >=(other); Association.new(self,other) end end

newc = proc{ :Senders >= Array.new }

send = proc do |ch,*vals|
  case ch.key
  when :Senders
    ch.val << vals
  when :Receivers
    if ch.val.empty?
      ch.key = :Senders; ch.val = [vals]
    else
      ch.val.shift.call(*vals)
    end
  end
end

recv = proc do |ch,proc|
  case ch.key
  when :Receivers
    ch.val << proc
  when :Senders
    if ch.val.empty?
      ch.key = :Receivers; ch.val = [proc]
    else
      proc.call(*ch.val.shift)
    end
  end
end

#整数を受信して画面に表示"
c = newc.call
send.call(c, 3)
recv.call(c, proc{ |x| p x })

#何回でも c から整数を受信して画面に表示"
rep = proc{ recv.call(c, proc{ |x| p x; rep.call }) }
rep.call
(1..3).each{ |i| send.call(c, i) }

#関数サーバー
servc = newc.call
serv = proc do
  recv.call(servc, proc{ |i,recp| send.call(recp, i * i); serv.call })
end
serv.call
r = newc.call
send.call(servc, 123, r)
recv.call(r, proc{ |j| p j })
send.call(servc, 45, r)
recv.call(r, proc{ |j| p j })

#フィボナッチ数列
fibc = newc.call
fib = proc do
  recv.call(fibc, proc do |n,repc|
    fib.call
    if n <= 1
      send.call(repc, n)
    else
      repc1 = newc.call
      repc2 = newc.call
      send.call(fibc, n - 1, repc1)
      send.call(fibc, n - 2, repc2)
      recv.call(repc1, proc do |rep1|
        recv.call(repc2, proc do |rep2|
          send.call(repc, rep1 + rep2)
        end)
      end)
    end
  end)
end
fib.call
nn = 10
r = newc.call
send.call(fibc, nn, r)
recv.call(r, proc{ |m| p "fib(#{nn}) = #{m}" })


id:sumim:20070619:p1 に続く。