皆さんこんにちは、虎の穴ラボのY.Fです。
本記事は『虎の穴ラボアドベントカレンダー』23日目の記事になります。
22日目の記事はおっくんさんの『2020 年も終わりなので SVG で花火を打ち上げたい』です。ぜひお読みください。
toranoana-lab.hatenablog.com
24日目の記事はY.Mさんの『虎の穴ラボのチームビルディング -通販編-』記事です。ご期待ください。
本記事は以下の記事の続きになります。こちらもぜひご一読ください。
Reference Capabilities
本記事ではPony言語のコア機能である Reference Capabilities
について紹介します。
ちょっと復習
本題に入る前に、前回の記事から日が空いているのでPony言語の特徴を少し復習しておきます。
- オブジェクト指向、アクターモデル、静的型付け
- 型安全
- 数学的証明によって担保される型安全
- メモリ安全
- ダングリングポインタやバッファオーバーフローが起きない
- 例外安全
- 実行時例外が存在しない
- データ競合フリー、デッドロックフリー
- ロックやアトミック系操作が存在せず、型システムによってコンパイル時にデータ競合が無いことが保証される
- アクターモデルを使った言語
class
とactor
が存在するclass
はJavaなどと同様なクラスactor
は非同期で動作するメソッドを持てる
- 継承は無い
actor
内でbe
でメソッドを宣言することで非同期関数として動作する- コンストラクタは
new {任意の名前}
で宣言できる - traitによる宣言的部分型と、interfaceによる構造的部分型がある
(sample.pony)
trait Named fun name(): String => "Bob" class Bob is Named interface HasName fun name(): String class Larry fun name(): String => "Larry" actor Hoge new create(env: Env) => env.out.print("Hoge init") eat(env) be eat(env: Env) => env.out.print("eat") actor Main new create(env: Env) => // 非同期処理に関するもの // それぞれのactorは非同期に動作するが、各actor内の処理は順番通りに実行される Hoge(env) call_me_later(env) env.out.print("This is printed first") // 多態性に関するもの let bob: Named = Bob let larry: Larry = Larry trait_call(bob, env) // 構造的部分型によってnameメソッドがありさえすれば呼べる interface_call(larry, env) interface_call(bob, env) be call_me_later(env: Env) => env.out.print("This is printed last") // トレイトのNamedを指定しているので明示的にNamedであると宣言されているものだけが引数になれる fun trait_call(n: Named, env: Env) => env.out.print(n.name()) // インターフェースのHasNameが指定されているのでnameメソッドを持っていれば引数になれる fun interface_call(n: HasName, env: Env) => env.out.print(n.name())
Reference Capabilitiesとは
そのまま翻訳すると参照機能になるかと思います。その名の通り、参照周りの機能を制御する機構です。
具体的には、Ponyが担保している型安全性、メモリセーフ、例外安全、データ競合フリー、デッドロックフリーなどを支える機能になります。
上記の様な物を実現するには、Pony言語では以下のものをコア機能としています。
- 不変データなら安全に共有できる
- 分離(isolated)されたデータは安全
- 参照が一つしか無いということ
- 参照を共有することは出来ないが、渡すことはできる
- これらを保証できれば安全だが一般的には難しい
- Ponyではこれらをコンパイラが保証する
より具体的には以下の修飾子が言語機能として提供されます。
- Isolated
iso
修飾子- isolatedな変数宣言
- 参照が一つしかない
- Value
val
修飾子- immutableなデータ
- Reference
ref
修飾子- mutableなデータかつisolatedでないもの
- いわゆるCとかJavaとかの普通の変数
- actor間で共有できない
- Box
box
修飾子- read-onlyな参照
- 他のactorから変更されている可能性があるがread-onlyなので今のスレッドでは安全
- Transition
trn
修飾子- 書きこみはユニークだが、
box
も提供する - 変換することで
val
になれる- 書き込みを放棄することで共有可能な変数になれるというもの
- Tag
tag
修飾子- 名前の通りタグ。参照に対する識別子
- 参照の等値性とかを比較できる
付随するものとして以下の様な機能があります。
consume
- 名前の通り変数を消費するものです。
- consumeされた変数を以降使えなくなります。Rustの所有権の移動などを思い浮かべてもらえればよいかと思います。
let a = "aaa" let b = consume a env.out.print(a) // NG コンパイルエラー
recover
- 各Reference Capabilitiesを変換する機能
recover hogehoge end
といった形式になり、最後の値が戻り値になります
// isoなWombatを作る let w: Wombat iso = recover Wombat("this is wombat") end
実際には各修飾子は型の後ろに書かれます
String iso // An isolated string String trn // A transition string String ref // A string reference String val // A string value String box // A string box String tag // A string tag
classで宣言することでデフォルトのタイプを指定出来ます。以下はStringクラスの例です。
class val String
レシーバ(いわゆるthis)の指定も出来ます
class Foo let x: U32 new val create(x': U32) => x = x' class Bar let x: U32 new val create_val(x': U32) => x = x' new ref create_ref(x': U32) => x = x' fun val test(env: Env) => env.out.print("this receiver is val") fun ref test2(env: Env) => env.out.print("this receiver is ref")
ここからは、各機能の詳細を見ていきたいと思います。
iso
isoは説明したとおり、参照がユニークであることを保証する修飾子です。
class Wombat let name: String var _hunger_level: U64 new create(name': String) => name = name' _hunger_level = 0 new hungry(name': String, hunger': U64) => name = name' _hunger_level = hunger' actor SubActor new create(env: Env) => env.out.print("Sub Actor Activate") be test(env: Env, a: Wombat iso) => var b: Wombat iso = consume a env.out.print(b.name) actor Main new create(env: Env) => let w: Wombat iso = recover Wombat("this is wombat") end let sub_actor: SubActor = SubActor(env) // isoは必ず参照が一つでないといけないのでconsume // また、consumeすることで別アクターにも渡せる sub_actor.test(env, consume w) // sub_actor.test(env, w) // 引数で参照が2つになるのでNG
val
これは直感的にもわかりやすimmutableなデータです。
class Wombat let name: String var _hunger_level: U64 new create(name': String) => name = name' _hunger_level = 0 new hungry(name': String, hunger': U64) => name = name' _hunger_level = hunger' // レシーバvalなものに対して破壊的な変更は出来ない // fun val set_hunger(hunger': U64) => // _hunger_level = hunger' actor SubActor new create(env: Env) => env.out.print("Sub Actor Activate") be test(env: Env, a: Wombat iso) => var b: Wombat iso = consume a env.out.print(b.name) be test_val(env: Env, a: Wombat val) => env.out.print("wombat is immutable") actor Main new create(env: Env) => // ... let immutable_wombat: Wombat val = recover Wombat("this is val wombat") end // 変更されないので別アクターに渡せる sub_actor.test_val(env, immutable_wombat)
ref
これは通常のJavaやC#で用いられる参照系の変数と同様です。
class Wombat let name: String var _hunger_level: U64 new create(name': String) => name = name' _hunger_level = 0 new hungry(name': String, hunger': U64) => name = name' _hunger_level = hunger' // refなレシーバのメンバに対して破壊的な変更 fun ref set_hunger(hunger': U64) => _hunger_level = hunger' actor SubActor //... // 以下はコンパイルエラー // メッセージ:this parameter must be sendable (iso, val or tag) // be test_ref(env: Env, a: Wombat ref) => // env.out.print("wombat is ref") actor Main new create(env: Env) => //... let ref_wombat: Wombat ref = Wombat("this is ref wombat") ref_wombat.set_hunger(1)
SubActor
の引数にrefなものは渡せなくなっています。これは、変更可能なものが複数スレッドから参照されることにより安全性を保てないためです。
box
これはread-onlyな参照を示すための修飾子です。これを使うことで参照専用の変数とすることができます。
actor Main new create(env: Env) => //... let c = "test" let d: String box = c local_test(env, d) fun local_test(env: Env, arg: String box) => // boxとして受け取ってるので破壊的な変更はできない // arg.append("test") env.out.print("test")
実際には trn
と一緒に使われることが多いように思われます。
trn
ユニークな書き込み変数と同時にboxも提供してくれる修飾子になります。box、valと合わせて使ってみます。
actor Main new create(env: Env) => //... let trn_wombat: Wombat trn = recover Wombat("this is trn wombat") end // boxなものにも代入可能 let box_wombat: Wombat box = trn_wombat // consumeすると書き込みが消えてvalとして渡せる sub_actor.test_val(env, consume trn_wombat) // 元変数がconsumeされてもboxの方は残る env.out.print(box_wombat.name)
tag
これは読み書きをするためのものではなく、オブジェクトの参照などの同値性などをチェックするためのものです。
actor Main new create(env: Env) => //... let temp_wombat: Wombat iso = recover Wombat("iso wombat") end // 読み書きどっちも出来ないのでisoなものでもtagにならなれる let tag_wombat1: Wombat tag = temp_wombat // valなものも同様(参照数の制限はないが、書き込まれないことは保証される) let tag_wombat2: Wombat tag = immutable_wombat
Reference Capabilitiesのsubtyping
各Reference Capabilitiesには派生関係があります。
子 | 親 |
---|---|
iso | trn |
trn | val,ref |
ref | box |
val | box |
box | tag |
実際に変換してみます。
actor Main new create(env: Env) => //... // isoなものはtrnになれるisoは書き込み読み込みともにユニークなのに対してtrnは書き込みはいくつかもてる let temp: Wombat iso = recover Wombat("iso wombat") end let temp_trn: Wombat trn = consume temp // trnはvalになれる これは書き込み権限を放棄すればいくつリードがあってもよいため // 受け口として狭くなるが、その代わりtrnを放棄する let temp_val: Wombat val = consume temp_trn // trnはrefにもなれる refはいくつでも書き込み読み込みができる let temp_trn2: Wombat trn = recover Wombat("iso wombat") end let temp_ref: Wombat ref = consume temp_trn2 // boxは単にread onlyな参照なので好きに取得できる let temp_box1: Wombat box = temp_val let temp_box2: Wombat box = temp_ref // tagは読み取りも書き込みもできないのでこちらも好きにできる let temp_tag1: Wombat tag = temp_box1 let temp_tag2: Wombat tag = temp_box2
Destructive read
ここまで consume
などを使ってReference Capabilitiesについて説明してきましたが、一点問題があります。
例えば、 iso
なフィールドがあったときに、consumeして使いたいけどこれをやってしまうとフィールドの変数がダングリングポインタなどになってしまいます。
Ponyでは代入後の戻り値として古い変数を返す仕様があり、これを使って以下のように代入できます。
class Aardvark var buddy: Wombat iso new create() => buddy = recover Wombat("Aardvark") end fun ref test(a: Wombat iso) => // buddyの値をbにしてbuddyにはaが入り、aは使えなくなる // buddyは依然Wombat iso型のままでダングリングポインタではないことは保証されている var b: Wombat iso = buddy = consume a
これを Destructive read
と呼びます。
まとめ
今回はPony言語の安全性の肝となるReference Capabilitiesについて紹介しました。
Ponyにはまだまだジェネリクスやパターンマッチなどの最近のモダンな言語に備わっている機能もあるので、追々紹介していきたいと思います。
P.S.
【新年LT初め】オタクが最新技術を追うLTイベント#20【オンライン】
2021/1/6(水) 19:30から新年初となるLT会を開催します。ぜひ参加ください! yumenosora.connpass.com
その他採用情報
虎の穴ラボでの開発に少しでも興味を持っていただけた方は、採用説明会やカジュアル面談という場でもっと深くお話しすることもできます。ぜひお気軽に申し込みいただければ幸いです。 カジュアル面談では虎の穴ラボのエンジニアが、開発プロセスの内容であったり、「今期何見ました?」といったオタクトークから業務の話まで何でもお応えします。 カジュアル面談や採用情報はこちらをご確認ください。 yumenosora.co.jp