Ruby の Ractor で ActiveObject を作る
Ractor とは
2020年12月に出ると噂されている Ruby3 で入るかもしれない並行制御機能です。簡単に並行並列プログラミングが作れるようになる、といいなぁ、と思って作っています。
詳しくはこちら: https://github.com/ruby/ruby/blob/master/doc/ractor.md
仕様はリリースまで変わる予定で、リリース後も多分変わります(つまり、Ruby 3.0 は Ractor がさくっと使えるものではありません)。
作ってばかりだとつらいので、ちょっと遊んでみました。
ActiveObject とは
えーと、すみません、あんまりしっかり定義を知りません。米澤研の方はきっとよくご存じなんだろうと思います。聞くところによると、ActiveObject pattern なるものがあるとも聞いたのですが、それについて書いてある本が高そうで買えませんでした。
3つのメッセージタイプがあり、past は投げっぱなし、now はすぐに返値を待つ、future は他言語における future/promise みたいなものを返すっぽいです。
なんかメッセージを送ると、並行に実行してくれるオブジェクトみたいなものととらえます。分散オブジェクトと何が違うんだろう。
ActiveObject を作ってみる
使い勝手を検討する
とりあえず、簡単にできそうな past 型のメッセージを送る Ractor::ActiveObject
というものを作ります。設計としては、そのクラスを派生したクラスに、適当にメソッドを定義すると、そのクラスへのメソッドは(呼び出し側とは)並行並列に走る、というものです。
こんな感じで Greeter
を定義すると、
class Greeter < Ractor::ActiveObject
def initialize hello = 'Hello'
@hello = hello
end
def hello name
p "#{@hello} #{name}"
end
end
こんな感じで呼べるといいなぁ、というものです。
g1 = Greeter.new
g2 = Greeter.new('こんにちは')
g1.hello 'ko1'
#=> "Hello ko1"
g2.hello 'ko2'
#=> "こんにちは ko2"
で、hello
メソッドは並列に実行されます。... 並列に実行してもつまらないですね、これ。
いつもの例でつまんないですけど、n 番目の fibnacci 数を求める ActiveObject を作ってみます。
class Fibonar < Ractor::ActiveObject
def fib_ n
if n < 2
1
else
fib_(n-1) + fib_(n-2)
end
end
def fib n
p fib_(n)
end
end
fs = 10.times.map{Fibonar.new}
fs.each{|fibonar| fibonar.fib(30)}
fs.each{|fibonar| fibonar.fib(30)}
この行で、作った10個の Fibonar オブジェクトに対して fib
を呼んでいます。こいつらが、それぞれ並列に走ってくれるとよさそうな気がします。なお、Ractor::ActiveObject
を継承しなければ、これ、そのまま動きます。
Ractor::ActiveObject
を作ってみる
では、では、作ってみます。
class Ractor
class ActiveObject
class Dispatcher
def initialize klass, *new_args
@class = klass
@r = Ractor.new klass, new_args do |klass, new_args|
aobj = klass.new_obj *new_args
loop do
msg = Ractor.receive
mid, *args = *msg
aobj.__send__(mid, *args)
end
end
end
def method_missing id, *args
@r.send([id, *args])
end
end
class << self
alias new_obj new
def new *args
Dispatcher.new(self, *args)
end
end
end
end
主なアイディアは:
-
Greeter.new
とかFibonar.new
すると、それぞれのインスタンスではなく、Ractor
を作ってしまい、呼ばれたメソッドを、そのRactor
へメッセージを送る dispatcher オブジェクトを返す。 -
Ractor.new
の中で、実際にオブジェクトを生成し(klass.new_obj
)、送信されたメソッドをそっちで動かす。
これで、上記プログラムが動くようになります。少ない行数でできましたね。これ、返値見ないから簡単なんですよねぇ。
now 型の呼び出しをしたいなら、ちょっと変えればよさそうです。ただ、future どうやって作りましょうね。
future 型
やっぱり、返値欲しいですよねぇ、ということで作ってみました。
class Ractor
class ActiveObject
class Dispatcher
def initialize klass, *new_args
@class = klass
@ret_r = Ractor.new do
loop do
Ractor.yield Ractor.receive
end
end
@r = Ractor.new klass, new_args, @ret_r do |klass, new_args, ret_r|
aobj = klass.new_obj *new_args
loop do
msg = Ractor.receive
mid, *args = *msg
ret_r.send aobj.__send__(mid, *args)
end
end
end
def method_missing id, *args
@r.send(Ractor.make_shareable([id, *args]))
@ret_r
end
end
class << self
alias new_obj new
def new *args
Dispatcher.new(self, *args)
end
end
end
end
一つ返す用の Ractor を用意(@ret_r
)して、それ経由で受け取っています。Erlang などの receive
では、受け取り側を用意して、不要であれば(パターンにマッチしなければ)受信を pending できるのですが、Ruby の Ractor ではそういうことができないので、チャンネルっぽい Ractor を用意しているというわけです。
class Fibonar < Ractor::ActiveObject
def fib n
if n < 2
1
else
fib(n-1) + fib(n-2)
end
end
end
fs = 10.times.map{Fibonar.new}
rs = fs.map{|fibonar| fibonar.fib(30)}
rs.each{|r| p r.take}
こんなコードがちゃんと動きます。
future オブジェクトが返るので、rs = fs.map{|fibonar| fibonar.fib(30)}
では計算が行われているかどうかは問いません。そのあとの r.take
で待つわけです。
しかし、take
よりも value
のほうがいいな、ここでは。
考え出すと、本当にこれでいいのかいろいろある気がしますが、とりあえず、これくらいにしておきます。ちょっと書くだけで、なんだか、おっと、というようなことが書けるのが、スクリプト言語のいいところですよね。
とくにオチはありません。
今回のあまり面白くもないオチ
オチはないと言いましたが、思いついたので一個だけ。ActiveObject を使わないで、ただ Ractor を使うだけだと、Fibonar
の例はこんな感じになります。
def fib n
if n < 2
1
else
fib(n-1) + fib(n-2)
end
end
rs = 10.times.map do
Ractor.new do
fib(30)
end
end
rs.each{|r| p r.take}
で同じことができます。Ractor自体は、こういう Promise みたいなのがさくっと書けるんですよねぇ(Ractor.new
が Promise 返すようなイメージ)。
ActiveObject とどっちがいいですかね。Ractor.new
とか一切書かないので、ちょっと簡単そうに見えるような気がします。また、複雑になってくると、ActiveObject のほうがやりやすいとかあるかもしれませんね。例えば、今回は fib(n)
しかありませんでしたが、メソッドが複数になると、また違うのかも(あまり考えていません)。
ちなみにちなみに。そもそもRactor
をActiveObjectというインターフェースで提供する、という案もありました。が、ちょっと自由度が高すぎるので、Ractor自体はそういうのを実装するための基盤とすることができる、ということを念頭に置いています(不足がないようにしたいと思っています)。
ところで、そもそも ActiveObject の定義ってこれでいいのかな...。
Discussion