虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

Pony言語について:Reference Capabilities編

皆さんこんにちは、虎の穴ラボのY.Fです。

那須どうぶつ王国の馬
那須どうぶつ王国の馬

本記事は『虎の穴ラボアドベントカレンダー』23日目の記事になります。

qiita.com

22日目の記事はおっくんさんの『2020 年も終わりなので SVG で花火を打ち上げたい』です。ぜひお読みください。
toranoana-lab.hatenablog.com

24日目の記事はY.Mさんの『虎の穴ラボのチームビルディング -通販編-』記事です。ご期待ください。

本記事は以下の記事の続きになります。こちらもぜひご一読ください。

toranoana-lab.hatenablog.com

Reference Capabilities

本記事ではPony言語のコア機能である Reference Capabilities について紹介します。

tutorial.ponylang.io

ちょっと復習

本題に入る前に、前回の記事から日が空いているのでPony言語の特徴を少し復習しておきます。

  • オブジェクト指向、アクターモデル、静的型付け
  • 型安全
    • 数学的証明によって担保される型安全
  • メモリ安全
    • ダングリングポインタやバッファオーバーフローが起きない
  • 例外安全
    • 実行時例外が存在しない
  • データ競合フリー、デッドロックフリー
    • ロックやアトミック系操作が存在せず、型システムによってコンパイル時にデータ競合が無いことが保証される
  • アクターモデルを使った言語
  • classactor が存在する
    • 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