Python互換の静的型付け言語「Erg」
承前
Ergは私が数年前から開発を始め、つい一昨日公開したばかりのプログラミング言語です。
のはずですが、
早速、qnighyさんに捕捉していただきました。ありがとうございます。
この記事ではそのErgがどのような言語なのかざっくりと解説していきたいと思います。なお、紹介した機能の一部は未実装です。実装途中の機能を含むコードには*を、完全に未実装の機能を含むコードには**をつけておきます。
はじめに
Pythonは概ね素晴らしい言語です。
オフサイドルールを世に知らしめた(?)、その可読性の高い文法。Numpy, SciPyを始めとする機械学習、科学技術計算用の膨大なライブラリ。
しかしPythonにもいくつかの弱点が存在します。
まず、動的型付け であること。それ自体は弱点というより良し悪しなのですが、明らかに動的型付けでは手に余るPythonプロジェクトが世に溢れています。
次に、一貫性、利便性に欠けるオブジェクトモデル 。
# この挙動はつい最近まで修正されていませんでした
# ここは==を使うべきですが、そもそも比較演算子が2種類あることからして問題です
i = 256
assert i is 256
j = 257
assert j is 257 # AssertionError:
# これは未だにそうです
l = ["a"] * 3
l[0] = "b"
print(l) # ["b", "a", "a"]
l = [[1]] * 3
l[0][0] = 2
print(l) # [[2], [2], [2]]
Pythonに詳しくない人にとって、これらのコードの挙動を予測するのは非常に困難だと思われます。
また、CPythonはオブジェクト管理に際してGIL(Global Interpreter Lock)を採用しており、近年重要度が増してきている並行プログラミングを著しく制限します。
そして、これはPython自身の問題ではありませんが、深刻な検索結果汚染 も挙げられます。Pythonはいまやプログラミング入門者にとって最も人気のある言語の一つです。それの副作用として、Google等の検索エンジンは、python.orgなど公式サイトのレファレンスより、SEO対策に力を入れても記事内容には力を入れないプログラミングスクールのサイトを検索結果の上位に置くようになりました。これはPython開発者がどれだけ努力しようと最早どうにもならないことです。我々には汚れなき新たなフロンティアが必要なのです(?)。
他にも色々言いたいことはありますが、本記事はPythonの批判記事ではないため、このあたりでお終いにしておきます(それに私自身は概ねPythonを気に入っています)。
Ergはこれらの問題を無謀にもまとめて解決するべく開発が始められました。
ErgはざっくりいうとTypeScriptのPython版です。
しかしPythonの文法を単に拡張したわけではなく、より一貫性と堅牢性のあるプログラムのために0からデザインされています。
このような人におすすめ
Pythonに静的型付けがほしい
Pythonにはmypy, typingモジュールなど型付け補助のための機能が実装されており、型付け自体は現行のPythonでも可能です。
現行のPythonで絶対に不可能なのは、型推論と強制力のある型検査です。Pythonを使っている限り、プログラマーは型を指定して静的型付けのメリットを20%ほど享受するか、指定せず放棄するかのトレードオフを考え続けなくてはなりません。しかしErgは「型を指定せず静的型付けのメリットを享受する」という第3の選択肢、最善の世界を与えます。
rand = pyimport "random"
print! rand.randint!(0, 10) # 2
print! rand.randint!(0, 1.2) # TypeError: the type of b is mismatched
# expect: Int
# but found: {%v: Float | %v == 1.2}
"Light Rust"がほしい
ErgはRustで実装されており、その言語設計にも大きな影響を与えています。
例えば、Ergでは所有権の概念が存在します。デフォルトでは、可変オブジェクトの(可変)参照は1つまでです。
v = ![]
v.push! 1
assert v == [1]
w = v # v == [1] moves to w
print! w
print! v # this causes a MoveError
しかし、ErgはRustよりも外観としてはシンプルです。Ergは型推論をものすごく頑張ってくれるので関数の型付けも殆どの場合省略できます。それでいて型完全性を保っています(多分)。
# **
f x, y = x * y + x
print! classof(f)(Int, Int) # Int
print! classof(f)(Ratio, Complex) # Complex
print! classof(f)(Str, Int) # Str
それともう一つ、異論があるかもしれませんが、筆者はRustの最大の価値は「丁寧なエラーメッセージ」だと考えています。Ergはこの思想を継承しています。
proc! x =
l = [1, 2, 3]
l.push!(x)
l
Error[#12]: File example.er, line 3, in <module>::proc!
2│ l = [1, 2, 3]
3│ l.push!(x)
^^^^^
AttributeError: Array object has no attribute `.push!`
hint: in order to update the internal state of an object, make it mutable by using `!` operator
hint: `Array` has `push`, see https://erg-lang.github.io/docs/prelude/Array/#push for more information
hint: `Array!` has `push!`, see https://erg-lang.github.io/docs/prelude/Array!/#push! for more information
エラーメッセージが日本語で表示されるビルドオプションもあります。
Error[#12]: ファイル example.er, 3行目, <module>::proc!
2│ l = [1, 2, 3]
3│ l.push!(x)
^^^^^
AttributeError: Arrayオブジェクトは`.push!`という属性を持っていません
ヒント: オブジェクトの内部状態を変更したい場合は、`!`演算子を使って可変化してください
ヒント: `Array`は`push`メソッドを持っています、詳しくは https://erg-lang.github.io/docs/prelude/Array/##push を参照してください
ヒント: `Array!`は`push!`メソッドを持っています、詳しくは https://erg-lang.github.io/docs/prelude/Array!/##push! を参照してください
特徴
OOP & FP
オブジェクト指向プログラミングと関数型プログラミングの高度な融合を実現しています。
# Functional style, same as `.sorted()` in Python
immut_arr = [1, 3, 2]
assert immut_arr.sort() == [1, 2, 3]
assert immut_arr.mapped(i -> i + 1) == [2, 3, 4]
# Object-oriented (mutable) style
mut_arr = ![1, 3, 2]
mut_arr.sort!()
assert mut_arr == [1, 2, 3]
mut_arr.map! i -> i + 1
assert mut_arr == [2, 3, 4]
# Functions cannot cause side effects
push_world s: Str =
s.push! "world!"
# EffectError: cannot call a procedural method in a function. Only methods of mutable types can change the state of objects
# hint: define `push_world` as a procedure, not a function
MyStr! = Patch Str!
.push_world! ref!(self) =
self.push! "world!"
s = !"Hello, "
s.push_world!()
assert s == "Hello, world!"
型に関する機能
現代の型理論の粋を集めた、数々の強力な機能を備えています。これにより、実行時エラー0のコードも夢ではありません。
依存型
依存型は値に依存して決まる型です。
依存型の例としてよく引き合いに出されるのは、長さがコンパイル時に分かる配列です。
arr = [1, 2, 3]
arr[10] # IndexError: `arr` has 3 elements but was accessed the 11th element
concat|T: Type, M, N: Nat|(l: [T; M], r: [T; N]): [T; M+N] = l + r
assert concat([1], [2]) in [Int; 2]
篩型
篩(ふるい)型は既存の型を述語式(Boolを返す式)を使って更に絞り込む型です。
例えば、この機能を使ってゼロ除算をコンパイル時に検知することが可能です。
Nat = {I: Int | I >= 0}
2.times! do!:
print! "hello, ", end: ""
# => hello, hello,
-2.times! do!:
print! "hello, ", end: ""
# TypeError: `.times!` is a method of `Nat` (0 or more Int), not `Int`
Pos = {N: Num | N > 0}
NonZero = {N: Num | N != 0}
# *
Pos <: NonZero # OK
Real - {0} <: NonZero # OK
Int <: NonZero # TypeError
Nat <: NonZero # TypeError
繰り返しますが、Ergは動的型付け言語ではありません。上の判定はコンパイル時に可能です。
質疑応答
Q: Ergが型完全なら、Pythonの関数はどうやって呼び出すのか?
A: Ergにはassert castingという機能があり、コンパイル時に検証不能な型判定を実行時まで遅延することができます(これはTypeScriptでいうところのdeclareに相当しますが、より強力です)。
unsound = import "unsound"
o = unsound.pyeval("1 + 1")
print! classof(o) # Object
assert o in Int # これ以降、oはIntとみなせる
print! classof(o) # Int
print! o + 1 # 3
引数によって戻り値の型が変わってしまう関数は、or型を使って型付けます。
parse_int(x: Str): Int or IntParseError
Ergは構造的型システムも持つため、アサーションなしで引数の属性にいきなりアクセスするやる気のない関数も型付け出来ます。
get_name(named: Structural {.name = Str}): Str =
named.name
これらの機能を使ってPythonの関数はほとんど型付け可能です。
それでもexec
など少数の関数は原理的に扱えませんが(これらの安易な呼び出しはそもそもバッドプラクティスなので禁止、としています)、型安全性を損なわない範囲で制限されたeval
関数が利用可能です。
Python組み込みの関数についてはコンパイラが特別に型付けしているため、ゼロコストで呼び出し可能です。組み込みライブラリの関数については順次対応していく予定です。
Q: Ergはトランスパイル言語なのか?
A: 現時点では、そうです。当初の予定ではPythonの処理系(VM)まで書いてしまって、静的型付けで実行されるPythonの上位互換言語を作ろうと考えていました(所有権の文法があるのはそのためで、GC/GILレス化を目論んでいました)。ただコンパイラの実装に比してVMの実装がほとんど進んでおらず、このまま開発を続けた場合もう数年かかると判断したため、コンパイラのみ先行公開することとしました。VMの方は「Dyne」と名付け、サブプロジェクトとして開発していく方針です。
Q: Python互換と言っているが、文法の互換性はなさそうだが?
A: JS/TSのような文法の互換性はありません(移行しやすいようにそれなりに似せたつもりですが)。前問で述べた通り、処理系の互換性を意味しています。
Q: ErgからTensorFlowやNumpyのAPIを呼び出せるのか?
TSのDefinitelyTypedのように、Erg側で型付けして呼び出せるようにする予定です。
終わりに
このプロジェクトが気に入ったら、ぜひGitHubでStarをつけてください。とても励みになります。
またプロジェクトへのコントリビューションは常に歓迎しています。
コントリビューションに限らず、何かわからないことがあれば、Discordチャンネルで気軽に質問してください。
Discussion