Ponylangで型付きアクター生活[6] consume, recover編

注意 ここにある内容はpony2.x時代にドキュメントに書かれていた物なので、もう古い可能性が高いです。最新の情報は公式ドキュメントを参照してください。

はろぽに〜₍₍ (ง´・_・`)ว ⁾⁾

前回isotrnはそのままでは代入できないということが判明したので今回はその辺を便利に(?)使う方法を見ていきます。

本家だとこの辺→http://tutorial.ponylang.org/capabilities/consume-and-destructive-read/です。

Consuming a variable

オブジェクトの参照を他の変数に移動させることができます。

consumeを使うと、ある変数の参照を無効にして、他の変数に参照を移動させることができます。

fun test(a: Wombat iso) =>
  var b: Wombat iso = consume a // Allowed!

上記のコードだとaはもう使うことができなくなります。

fun test(a: Wombat iso) =>
  var b: Wombat iso = consume a // Allowed!
  var c: Wombat tag = a // Not allowed!

destructive read

isoなフィールドをconsumeすると、フィールドの値が空っぽになってしまい、オブジェクトの状態が不正になってしまいます。 そのため、destructive readという仕組みを使う必要があります。

class Aardvark
  var buddy: Wombat iso

  fun test(a: Wombat iso) =>
    var b: Wombat iso = buddy = consume a // Allowed!

ponyの代入演算子は代入後の新しい値ではなく代入前の古い値が返されることを思い出すと、 上記のコードはaがconsumeされた後に、buddyに代入される。buddyに入っていた値はbに入ります。 結果的にisoなフィールドであるbuddyの参照を他の変数であるbで受け取ることができました。

buddyの値が空っぽになると同時にconsume aで、aの値が入るので安全という仕組みになっています。

Recovering capabilities

Ponyではreference capabilityを lift することができます。 例えばmutable reference capability(iso, trn, ref)はどんなreference capabilityにもなれます。 またimmutable reference capability(val, box)はimmutableかopaque reference capability(tag)になれます。

つまりローカル変数ではmutableとして変数作っておいて色々操作をして値を組み立てた後、返り値としてはimmutableな参照として返すといったことができる仕組みです。

文法としては、recover Array[String] endのように使います。これはArray[String] refの代わりにArray[String] isoを返します。

標準ライブラリのToStringの実装では以下のように使われています。

recover
  var s = String((prec' + 1).max(width.max(31)))
  var value = x

  try
    if value == 0 then
      s.push(table(0))
    else
      while value != 0 do
        let index = (value = value / base) - (value * base)
        s.push(table(index))
      end
    end
  end

  s.append(typestring)
  _extend_digits(s, prec')
  s.append(prestring)
  _pad(s, width, align, fill)
  s
end

まずString refを作って色々な操作をした後にString isoとして参照を返しています。

注意点

recoverの中でもrecoverの外側の変数にアクセスすることが出来ますが、sendableな値(他のアクターへ送れるisoval, tagなど)しか参照できません。これが安全にliftを行うための条件です。(refとかで参照をコピーしちゃえば他でも使えてしまったりする危険性があるのでコンパイルエラーになります)

Automatic receiver recovery

メソッドレシーバーがisotrnだと、refなメソッドを呼ぶことが出来ません。メソッドを呼ぶ側はレシーバーへの参照を持っていますし、メソッドを処理するところでは、自分自身への参照を持っているからです。なのでisoだったらtag, trnだったらboxなどのようにaliasを行う必要があります。

しかし全ての引数と返り値の型がsendableであれば自動的に変換を行ってくれるので上記のaliasは必要なくなります。

下に例を示します。

let s = recover String end
s.append("hi")

sはString isoであり、それに対してappendを行っています。appendはrefなメソッドレシーバーを要求するので通常はisoに対して呼ぶことは出来ませんが、automatic receiver recoveryによって呼ぶことができています。

Combining capabilities

オブジェクトにはreference capabilitiesがあり、そのフィールドにもreference capabilitiesがあるので、最終的にどのように見えるのかはこの2つの要素(origin, field)の組み合わせが重要になってきます。

Viewpoint adaptation

この組み合わせのことをviewpoint adaptationと呼びます。

iso field trn field ref field val field box field tag field
iso origin iso tag tag val tag tag
trn origin iso trn box val box tag
ref origin iso trn ref val box tag
val origin val val val val val tag
box origin tag box box val box tag
tag origin n/a n/a n/a n/a n/a n/a

例として、trnなoriginからrefなフィールドにアクセスするとboxになります。

class Foo
  var x: String ref

class Bar
  fun f() =>
    var y: Foo trn = getTrnFoo()
    var z: String box = y.x

isoなorigin/fieldのときに参照はisoになりますが、単純にaliasingすることができないのでread/writeユニークが保たれます。 例えば、

class Foo
  var a: String iso = recover String().append("test") end
  
  new create() =>
    "foo"

があったとき

actor Main
  new create(env: Env) =>
    var foo: Foo iso = recover Foo.create() end
    var f1: String iso = foo.a                                        // コンパイルエラー(foo.aはiso!)
    var f2: String iso = foo.a = recover String() end    // コンパイルを通る(destructive readを使う)

のようになります。

Passing and sharing

mutableなデータは他のアクターに 渡す ことができます。またimmutableなデータは他のアクターと 共有 することができます。 しかもロックやコピーなどをしなくても安全です。

Passing

mutableなデータ(iso, trn, ref)は他のアクターからreadすることはできません。 その参照が存在する限り値が書き換わってしまうかもしれないからです。

しかし、値が書き換わってしまうような参照を放棄すれば他のアクターへ安全に渡すことが出来ます。

isoな参照をconsume aを使って他のアクターへ渡すことで上記のような安全なデータの渡し方が実現できます。 またtrnでもvalに変換すれば同じように他のアクターへ安全に渡すことができます。(trn自体は渡せないことに注意)

他のアクターへ渡す予定がない変数はisoではなくrefを使ってもよいです。渡す予定がある場合はisoを使いましょう。

Sharing

他のアクターと値を共有するためには完全に値をimmutableにする必要があります。つまりvalを使う必要があります。boxは他に変えるかもしれない参照がいるので渡すことが出来ません。

他のアクターへ渡す予定がある場合はvalを使いましょう。ない場合はboxなどを使っても構いません。

またtagも共有することができます。readが出来ないのでいくら書き換わっても安全だからです。

Reference capabilities that can't be sent

ref, box, trnは値を渡すことが出来ません。trnは以下のようにvalに変換することができるので、全く渡せない訳ではありませんが、trn自体は渡せないことに注意してください。

actor Main
  new create(env: Env) =>
    var foo = Foo.create(env)

   var s: String trn = recover String().append("test") end
   foo.foo(consume s) //  trnをconsume
  
actor Foo
  var env: Env
  
  new create(env': Env) =>
    env = env'
    
  be foo(f: String val) => // valで受け取る
    env.out.print(consume f)

終わりに

consumeが使えるようになったのでだいぶいい感じにPonyでコードを書けるようになってきました。 次回はreference capabilitiesのサブタイプ関係などについて見ていきます₍₍ (ง´・_・`)ว ⁾⁾