ふるつき

私は素直に思ったことを書いてるけど、上から目線だって言われる

Ponyがいい言語に見える

 最近D言語と倦怠期に突入して(classを引数にとって中身をがんがんいじっていた自分のプログラミングが嫌になり、structを使えばいいのかclassを使えばいいのか考え出して死んだ。そもそもぱっと出てきた変数が値型なのか参照型なのかわからないの辛くないですか? 僕は Rangeはclassだと思ってたよ)、

書いていて楽しいプログラミング言語を探す旅に出たんですが、その中でであったものの一つがPonyでした(他にはElixirとかCommon Lispをみてた。やつらも楽しい感じがありますね)。

github.com

 Ponyは静的型付けのオブジェクト指向コンパイル言語だけど、核にあるのはActor Model な並列プログラミングだと思います。私はActorモデルよく知らないので全然わからないけど、言語仕様を見ていて好きな感じがしました。私はPonyのこんなところがスキっていうのを列挙します。

  • 静的型付け(強そう)である
  • capability って言われる(ほんまか)制約みたいなんがあって格好良い
  • コンパイルする言語でバイナリが結構小さい
  • GCがあるっぽい
  • プログラムの見た目が好き
  • 例外の扱いが好き
  • その他なんとなく哲学が好き(グローバル変数がないとか、stdoutやargsは全部Envというクラスのインスタンスにまとめられてエントリポイントに渡されるとか)
  • ドキュメントを結構たくさん書いてくれてる

まあなんというか、書いていて楽しそうという感じがしました。何故か日本語の情報が少ない*1 のですが GitHub では Syntax Highlight されてるし多分メジャーになってきてるとか、メジャーだとかそういう言語なんだと想います。


ちょっと見て下さい。これはフィボナッチ数列を列挙するプログラム*2です。

primitive Fibonacci
    fun fib[A: Integer[A] val](a: A, b: A, out: OutStream) =>
        out.print(a.string())
        if (a+b) < a then
            None
        else
            fib[A](b, a+b, out)
        end

        
actor Main
    new create(env: Env) =>
        try
            let a = env.args(1)?.u128()?
            let b = env.args(2)?.u128()?
            Fibonacci.fib[U128](a, b, env.out)
        end

プログラムは Main actor のコンストラクタから始まります。コンストラクタは new <constructor name>(args) => <body> という形になっていて、脱線すると自由に名前をつけることができます。実質オーバーロードみたいなものですが。Main と書いたら引数なしの create が呼ばれるけどこれは Main.create() でも同じことです。 もし new with_arg(arg1: U32) みたいなコンストラクタが定義されていたら Main.with_arg(10) みたいな感じでコンストラクタを呼び出すこともできます。なんか好き。

閑話休題で、まあその下に書いてあるのが body だっていうのはわかると思われます。何をしてるかも雰囲気で読めますよね? あと、これはちょっと罠っぽいんですが、インデントはプログラムの挙動には関係ないです。その代わり、フィールドの宣言、コンストラクタの定義、関数の定義、befavior の定義っていう順番に書かないとダメという決まりがあります。なるほどね。

env.args はなんとなく Array[String] みたいな型を持ちそうということは推測できそうですが((実際には val という immutability を表す capability がついて Array[String val] val なんですが))、これにいくつ要素が入ってるかわからないので その () オペレータ (( apply という関数の呼び出しなんで args.apply(1)? とも書ける)) の返り型は A? です。これは A 型の値を返すかあるいは 例外を発生する事を表していて*3Javathrows みたいなもんですね)こういう関数*4は呼び出すときに ? を付けないとだめだし、呼び出している関数も partial function になるか、それとも try...else...then...end 構文で例外を捕捉しに行かないとだめです(おわかりかと思いますが elsecatch 相当、 thenfinally 相当です)。

また脱線すると例外の扱いがすごい好きで、いわゆる raise とか throw に当たるのは error という構文なんですが、これの面白いのが値を一緒に渡せないし型がないところで、つまり一切の例外を識別できないんですね。これはパフォーマンス的な理由からと書いてあるような気がするんですが、私はこれかなり好きです。例外ハンドリング嫌いなので(でもこれDBのカラム名間違ってましたみたいなエラーの原因特定するの難しそう)。

まあそんな感じで、もし引数があれば、それは String なので u128 関数で U128 型の値として取り出します。当然これも失敗しうる。ちなみに整数型は U8 U16 ... U128I8 ... I128 に加えて {U,I}{Size,Long} があります。

次は fib の呼び出しなんですが先に定義の方を解説します。

primitive はいろいろな使われ方をしていて enum のように使うときと 「メソッドまとめ」のように使うときとあります。今回は後者。 Main の関数でも良かったといえばそうだけど。

なんか雰囲気ありますがこれは Generics を使っていて ちょっとどういう書き方でこうなってるのかわかってないんですが、 「Integer に属していて読み取り専用」 の型Aを引数に取っている気持ちです。やってることは単純なのでわかるとおもいます。あ、末尾最適化があります。

定義がわかると呼び出しもわかります。いい感じの言語に見えませんか?

そう言えばコンパイルも面白くて、特定のディレクトリ以下で ponyc とすると勝手にソースコードを探索する仕組みになっています。便利と言えば便利だけど慣れないのでキモいですね。 make してる気持ちに慣ればいいんでしょう。できるバイナリはディレクトリ名をしてます。


もういっこサンプルを。折角Actorモデルがつかえるので書きたかったんですがわからなかったやつ。

use "collections"
use "itertools"

actor Worker
    be calculatePiFor(receiver: Main, start: U32, stop: U32) =>
        let iterator: Iterator[U32] = Range[U32](start, stop)
        let x: F32 = Iter[U32](iterator)
            .map[F32]({(x: U32) => x.f32()})
            .map[F32]({(x: F32) => 4.0 * ((1-((x%2)*2)) / ((2*x)+1))})
            .fold[F32](0.0, {(sum: F32, x: F32) => sum + x})
        receiver.receive(x)
        
actor Main
    var _pi: F32 = 0.0
    let _range: U32 = 10000
    let _actor_nums: U32 = 1000
    let _out: OutStream
    new create(env: Env) =>
        var start: U32 = 0
        for i in Range[U32](0, _actor_nums) do
            Worker.create().calculatePiFor(this, start, start + _range)
            start = start + _range
        end
        _out = env.out


    be receive(x: F32) => 
        _pi = _pi + x
        _out.print(_pi.string())
        

これは、 ライプニッツの公式による円周率の導出です。 1000個の actor に計算を投げてそれの集約をしてる……んですが、うまく create の中で actor の計算終了を待って出力みたいなことができませんでした。どうやるんだろ。

ところでこれも気持ちがあると読めると思うんですが、 beactor 特有のアレで、 behavior の略です。なんでこんな略し方をしたのか。 Rust の fn みたいな反感の買い方をしそう。まああの、 be は partial にできなかったり返り値を返せなかったりしますが*5、そのかわり勝手に並列になります(一つのActorインスタンス内で並列になることはないので安心)。

あーあとすっごい不思議なんですが、すべての演算子には 括弧がついていて順序付けされている必要があります。 1 + 2 * 3は許可されて無くて (1 + 2) * 31 + (2 * 3) じゃないとだめ(でもなぜか 1 + 2 + 3 のように同じ演算子の連続はOKらしい)。これ鬱陶しい。ついでに U32 + F32 とか U32 + U128 みたいな演算は定義されてないので悪しからず。さらに言えば 加算代入演算子っていうんでしたっけ、 こういうの += も存在しません。この辺なんでだろ……。

まあこんな感じの言語があって良さげだし、もうちょっと勉強したいですね。今回は capability にも言及できなかったので(わかってないから。良さそうに見えるけど)、そこもどうにかしていきたいです。

*1:矢二郎の顔ばかり出てくる

*2:本当にどうでもいい話だけど math package が最高に面白い

https://github.com/ponylang/ponyc/tree/8a8ee28f8dec1f5336f9c6e3176e41d133e5b68b/packages/math

*3:型としてはA or None を表している

*4:partial functionと呼ばれる

*5:つねにNoneが返ることになってる