LoginSignup
39
32

More than 5 years have passed since last update.

Ponylangで型付きActor生活

Last updated at Posted at 2015-12-14

この記事は 第2のドワンゴ Advent Calendar 2015の15日目です。
14日目は@toRisouPさんの音声認識で「結月ゆかり実況」動画を作るでした。

2015年新卒入社の@matsu_charaです。
今日はPonylang(Pony)という言語の紹介をしたいと思います。

Ponyとは

ざっくり言うと、Ponyは「Actorを使った並行処理を行うことが出来る静的型付けなオブジェクト指向言語」です。
Causalityというロンドンの会社が中心になって作成していて、Ponyに関する技術サポートも行っているようです。
まだversion 0.2.1なので割と新しい言語です。1

他の言語との違い

ActorといえばErlang・Elixir・Scala(Akka)あたりが有名だと思います。
ErlangやElixirは動的型付けな言語なので、静的型付けなPonyとはある程度異なる性質を持っていることが予想できます。一方でScalaは静的型付けなのでそういった意味で少し似ているかもしれません。2

関数型プログラミングのサポートを充実させているかオブジェクト指向に徹しているかという違いもあるのですが、Scala(やErlang, Elixirなどの言語)とPonyの大きな違いとして、単なる type safety だけではなく、並行処理で発生しがちな様々な問題をコンパイル時に検出するための capabilities-secure という概念を言語の中心に据えている点が挙げられます。

Ponyの特徴

ここで公式で言われているPonyの特徴をいくつか見てみましょう。

  • 型安全
    • Ponyで使われている型システムについての論文があります。ScalaやErlang、Rustといった言語との比較も載っています。
  • メモリセーフ
    • ダングリングポインタやバッファーオーバーフローが無いのはもちろん、nullも許容されません。
  • 例外安全
    • 実行時例外はなく、例外は全てハンドルされることが保証されます。
  • データ競合フリー
    • ロックやアトミック演算のようなものはありません。代わりにデータ競合が起きないことをコンパイル時に保証してくれる仕組みがあります。
  • デッドロックフリー
    • そもそもロックがないのでデッドロックがありません。

公式では上の5つをまとめて capabilities-secure と呼んでいます。
並行処理にかぎらず普通のプログラミングにおいてもデバッグしにくかったり再現性のないバグを生み出しそうな要素をできるだけ言語から排除していることが分かります。

その他の特徴としては

  • コンパイルされる
    • インタプリタやVM無し
  • C/C++との親和性
    • C言語のライブラリを呼べる
    • Cのヘッダを生成できるのでC/C++からPonyのコードを呼べる

などが挙げられています。

またErlang, Scalaより高速に動作するというベンチマークの結果もあります。

文法つまみ食い

ここからはチュートリアルにあるPonyの機能を見ていきます。以下で紹介するコードは 公式サンドボックスで試すことが出来ます。また、Mac OSXではbrew install ponycでponyコンパイラをインストールすることができます。

ハローワールド

サンドボックスに以下のコードを入力してRunを押すと結果が表示されます。

actor Main
  new create(env: Env) =>
    env.out.print("Hello, world!")

class Foo でClassが宣言できるのと同じようにactor FooのようにActorが宣言できます。
new Bar()とするとコンストラクタの宣言ができます。

Actor

Main自体もActorでしたが、もう少しActorについて見ていきましょう。

ここでは公式リポジトリのサンプルからcounterを見ていきます。(説明のため若干変更しています)

use "collections"

actor Counter
  var _count: U32

  new create(c: U32) =>
    _count = c

  be add(c: U32) =>
    _count = _count + c

  be get_and_reset(main: Main) =>
    main.display(_count)
    _count = 0

actor Main
  let env: Env

  new create(env': Env) =>
    env = env'

    let count: U32 = 10
    let counter = Counter(5)

    for i in Range[U32](0, count) do
      counter.add(1)
    end

    counter.get_and_reset(this)

  be display(result: U32) =>
    env.out.print(result.string())

上記のコードを実行すると、counterが5から始まり、10回add(1)が呼ばれた後、15が出力されます。
なんとなくコードを眺めるだけでも大体の処理が追えるのではないでしょうか?
いくつか面白そうな点を説明すると、

  • actorの受け取るメッセージ(behavior)はbeというキーワードで定義できます。
  • behaviorではメソッド宣言のように型を宣言できます。一方、返り値の型は定義できません。
  • behaviorはメソッドのように呼び出すことが出来ます。処理は非同期に行われるためブロックされません。
  • 'が変数名で使えるので、env = env'のような書き方が可能になっています。
  • 数値の型がU32など、厳密に定められています。

などが挙げられます。

behaviorがメソッドと同じように宣言できるので、存在しないbehaviorの呼び出しをコンパイル時に検出することが出来ます。また、メソッドと同じように引数に型をつけることができるなど、クラスもアクターも同じような感覚で扱うことが出来るようになっています。

Reference capabilities

Ponyで一番特徴的だと思われるReference capabilities(C/C++のconstのようなもの)の説明に入ります。
PonyではString refとするとmutableなStringの変数を、String valとするとimmutableなStringの変数を宣言することが出来ます。
Ponyでは、このような修飾子をコンパイル時にチェックすることによって、ロックのような仕組みを使うこと無くデータ競合の危険性を排除することができます。また、ロックを使わないので結果的にデッドロック問題も回避できます。

コードで見てみましょう。
例として単純にStringをprintするだけのサンプルを見ていきます。

actor Main
  new create(env: Env) =>
    let s_val: String val = "test val"
    env.out.print(s_val)

s_valはString val、つまりimmutableなので当然ながら以下はコンパイルエラーです。

actor Main
  new create(env: Env) =>
    let s_val: String val = "test val"
    s_val.append("pony")
/ponyc/webcontainer/main.pony:4:17: receiver capability is not a subtype of method capability
    s_val.append("pony")
                ^
/ponyc/webcontainer/main.pony:4:5: receiver type: String val
    s_val.append("pony")
    ^
/ponyc/packages/builtin/string.pony:576:3: target type: String ref
  fun ref append(seq: ReadSeq[U8], offset: U64 = 0, len: U64 = -1): String ref^
  ^

エラーメッセージが難しそうですが少し見てみましょう。
まずs_valString valと宣言されているのでimmutableです。
次に、最後のエラーメッセージを見るとappendメソッドは
fun ref append ~~~のように定義されているようです。
まずfunはメソッド宣言を表します。
そして、メソッドの宣言時にfun refと書くとメソッドレシーバーがrefの時のみ実行できるメソッドを作ることが出来ます。
つまりレシーバーがrefのときにだけ使えるメソッドがvalで呼ばれたからエラーになったということです。

次にrefを見ていきましょう。

actor Main
  new create(env: Env) =>
    let s_from_ref: String val = recover
      let temp: String ref = String()
      temp.append("test ref")
    end
    env.out.print(s_from_ref)

recoverってなんだ?と思われるかもしれませんが今のところrefな変数をvalに変換するものだと思ってください。3

tempString refなのでtemp.append()としてもエラーになりませんでした。
そして、一通りmutableとして書き換えが終わったらrecoverの効果でrefvalに変換されます。

temps_from_refStringとしては同じ型ですが、先ほどのfun refの仕組みによって、immutableにした後に間違えてappendを呼んでしまう恐れはありません。また、このような仕組みがあるためImmutableStringMutableStringのような二通りのクラスを作成する必要もありません。4

ちなみに下記のコードのようにprintメソッドにString refを渡すとコンパイルエラーになります。

actor Main
  new create(env: Env) =>
    let s_ref: String ref = String()
    s_ref.append("test ref")
    env.out.print(s_ref)
/ponyc/webcontainer/main.pony:5:19: argument not a subtype of parameter
    env.out.print(s_ref)
                  ^
/ponyc/packages/builtin/stdstream.pony:58:12: parameter type: Bytes val
  be print(data: Bytes) =>
           ^
/ponyc/webcontainer/main.pony:5:19: argument type: String ref
    env.out.print(s_ref)
                  ^

env.out.print()val でないと受け取ってくれない。つまりimmutableでないと処理してくれないことがエラーメッセージから分かります。先程はメソッドレシーバーの制約でしたが今度は引数の制約です。また、先程はmutable(書き換え可能)でないとコンパイルエラーだったのに対し今回はimmutable(書き換え不可能)でないとコンパイルエラーでした。

このようにPonyではあるメソッドが、どのような条件で動くのかを細かく指定することが出来ます。
条件に適合したプログラムを書くのが大変な時もありますが、複数のアクターが協調動作する場合には、
どのデータなら安全に他のアクターに渡すことが出来るか?どのデータを渡すと危険になってしまうのか?といった判断をコンパイラに委ねる事ができるので安心してロジック部分の記述に集中することが出来ます。

このような違いを持つReference capabilitiesが iso, val, ref, box, trn, tag の合計6個存在し、subtype関係なども定義されています。
今回は分かりやすいrefvalのみを紹介しましたので「それRustにもあるよ!」というツッコミが来そうですが、
Ponyでは残りの4つも駆使しながらよりActorに特化した安全性を提供しています。5
詳しくは公式の該当するページ(http://tutorial.ponylang.org/capabilities/reference-capabilities/) をご覧ください。

まとめ

以上でPonylangの紹介は終わりです。
今回はReference capabilitiesによってmutable, immutableを使い分けることを中心に説明しました。
そもそもmutableなんて使わないよ!みたいな方も居ると思いますが、Ponyにはnullを許さないメモリ安全性やデッドロックの心配がないデッドロックフリーなどの魅力的な性質がまだまだあるので、immutableガチ勢の方にもきっと満足いただけると思います。6

たまにコンパイルエラーが人智を越えた領域に到達したりしますが、CausalityLtd/ponycにて、改善・改良が現在進行形で進められています。

仕事でいますぐ使えるか?と言われるとまだ少しだけ早いような気がします7が、ponyのcapabilities-secureの概念を学んでおけば、他の言語でActorを使ったプログラミングをする際に、ここはboxっぽいなとかここはisoっぽいな、などといった新しい視点が得られると思います。
他の言語ではコンパイル時にコードがcapabilities-secureかどうかのチェックを受けることはできませんが、動的型付け言語を書くときも静的型付け言語の経験が役に立つ8のと同じようにPonyのcapabilities-secureの概念を学んだ経験が、他のプログラミング言語を書くときにも役立つといいなーと思っています。

この記事をきっかけにちょっと勉強してみようかなーと思った方はぜひ公式チュートリアルを試してみてください。とても分かりやすいですが、途中でページの内容が空になってて「なるほど〜」という気分になれます。(この辺は新しい言語なので仕方ありません。ガンガン使ってガンガンフィードバックしていくもよし、話題だけウォッチしておくのもよしです。)

ということでPonylangの紹介は終わりです。
明日は@nyamngoさんの「シェルスクリプトで型無しデンジャラス生活」です。


  1. v0.1.0のリリース日は2015/4/28です。 https://github.com/CausalityLtd/ponyc/releases 

  2. ScalaのAkkaはreceiveが Any => Unit だから・・・みたいな話もありますが、最近ではdeprecatedになったTyped Actorsに代わり、Akka TypedというDSLベースでActorの振る舞いに型をつける取り組みが(まだexperimentalですが)進んでいるのと話の都合があるので、静的型付けということで・・。 

  3. 詳しくは http://tutorial.ponylang.org/capabilities/recovering-capabilities/ で紹介されています。 

  4. Stringのような一般的なものであれば2つ作ることはそこまで苦ではないと思いますが、自作のそこまで一般的ではないクラスに対してmutable版, immutable版の2つを作り変換関数を一つ一つに対して書くのは少し大変だと思います。 

  5. 例えばRustではデッドロックのリスクをコンパイルの時点では排除しきれません。PonyではアクターやReference capabilitiesの力を活用し、ロックを使わなくても並行処理を行えるようにしているためデッドロックのリスクが原理的に存在しません。もちろん逆にPonyにはできなくてRustにはできることもたくさんあるので、RustよりPonyの方が優れているという主張ではありません。(例えばPonyはGCを利用しますが、Rustにはより洗練されたメモリ解放の仕組みがあります。)2つの言語は異なる利点と欠点を持ち、得意なところが違うという点をご了承頂ければと思います。 

  6. なぜActorモデルにshared memory方式を入れて、参照ごとに値のread/writeが安全かどうか保証したいのか。Actorはshared nothingなのが売りなのでは?と疑問に思われた方は上で紹介した論文( http://www.ponylang.org/papers/fast-cheap.pdf ) のintroduction部分を読むと少し納得が行くと思うのでぜひご覧ください。 

  7. コンパイラの枯れ具合・標準ライブラリの充実度・パッケージマネージャの存在・APIドキュメントの充実などまだまだ色々な発展が必要そうです。 

  8. あるいは逆に静的型付け言語での経験が動的型付け言語を書くときに役立つように 

39
32
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
39
32