基本事項
Warning: 本ドキュメントは未完成です。校正(文体、正しいリンクが張られているか、など)がなされていません。また、Ergの文法はバージョン0.*の間に破壊的変更が加えられる可能性があり、それに伴うドキュメントの更新が追いついていない可能性があります。予めご了承ください。 また、本ドキュメントの誤りを見つけた場合は、こちらのフォームまたはGitHubリポジトリから修正の提案をしていただけると幸いです。
本ドキュメントは、Ergの基本文法について解説するものです。 既にpythonなどの言語に触れた経験がある方は、概説的なquick tourもあるためそちらを参照してください。 また、標準APIその型定義、コントリビューター向けの内部資料は別途存在します。文法やErg本体についての詳細な説明が必要な場合はそちらを参照してください。
Hello, World!
まずは恒例の、Hello Worldを行いましょう。
print!("Hello, World!")
Pythonや同系統の言語とほぼ同じです。目を引くのはprint
の後に付く!
ですが、これの意味はおいおい説明します。
また、Ergでは解釈に紛れのない限り括弧()
を省略することが出来ます。
括弧の省略ができるのはRubyと似ていますが、複数の解釈ができる括弧省略はできませんし、また引数が0個のときもPythonと同じく()
の省略が出来ません。
print! "Hello, World!" # OK
print! "Hello,", "World!" # OK
print!() # OK
print! # OKだが呼び出しという意味ではなく、単に`print!`を呼び出し可能なオブジェクトとして取得するという意味となる
print! f x # OK、これは`print!(f(x))`として解釈される
print!(f(x, y)) # OK
print! f(x, y) # OK
print! f(x, g y) # OK
print! f x, y # NG, `print!(f(x), y)`または`print!(f(x, y))`の二通りの解釈ができてしまう
print!(f x, y) # NG, `print!(f(x), y)`または`print!(f(x, y))`の二通りの解釈ができてしまう
print! f(x, g y, z) # NG, `print!(x, g(y), z)`または`print!(x, g(y, z))`の二通りの解釈ができてしまう
スクリプト
Ergのコードはスクリプトと呼ばれます。スクリプトはファイル形式(.er)で保存・実行できます。
以下のコードを"hello.er"として保存します。
print! "hello, world"
ターミナルですぐに実行することができます。
$ erg hello.er
hello, world
コメント
#
以降はコメントとして無視されます。コードの意図を説明したいときや一時的にコードを無効化したいときなどに使います。
# コメント
## `#`以降は改行されるまで無視されるため、`#`は何個でも使用できる
#[
複数行コメント
対応する`#[`から`]#`のところまでがコメントとして扱われる
]#
ドキュメンテーションコメント
'''...'''
はドキュメンテーションコメントです。コードの意図を説明したいときに使えて、実行時に文字列として参照することも出来ます。Pythonと違い、クラス・関数の外側で定義することに注意してください。
'''
この関数は、与えられた数値を2倍にして返す関数です。
'''
twice x = x * 2
print! twice.__doc__
# この関数は、与えられた数値を2倍にして返す関数です。
'''
クラス全体のドキュメンテーションコメント
'''
C = Class {x = Int}
'''
メソッドのドキュメンテーションコメント
'''
.method self = ...
'''
の直後に言語コードを記述することで、ドキュメントの言語を指定することが出来ます。するとErg Language Serverは各言語バージョンに合わせたドキュメントをMarkdown形式で表示します(デフォルトの言語は英語です)。
登録されている言語コードについてはこちらを参照してください。
'''
Answer to the Ultimate Question of Life, the Universe, and Everything.
cf. https://www.google.co.jp/search?q=answer+to+life+the+universe+and+everything
'''
'''japanese
生命、宇宙、そして全てについての究極の謎への答え
参照: https://www.google.co.jp/search?q=answer+to+life+the+universe+and+everything
'''
ANSWER = 42
またerg
と指定すると、Ergのサンプルコードとして表示されます。
'''
the identity function, does nothing but returns the argument
'''
'''erg
assert id(1) == 1
assert id("a") == "a"
'''
id x = x
式、セパレータ
Ergのスクリプトは、式(expression)の連なりです。式とは計算・評価ができるもので、Ergではほとんどすべてのものが式です。
各式はセパレータ―改行かセミコロン;
―で区切ります。
Ergのスクリプトは基本的に左から右へ、上から下へ評価されます。
n = 1 # 代入式
f x, y = x + y # 関数定義
f(1, 2) # 関数適用式
1 + 1 # 演算子適用式
f(1, 2); 1 + 1
以下のように、ブロック内で最後に評価した式を変数の値とするインスタントブロックという機能があります。
これは引数なし関数とは違い、()
をつけません。ブロックがその場で1度だけ評価されることに注意してください。
i =
x = 1
x + 1
assert i == 2
これはセミコロン(;
)では実現できません。
i = (x = 1; x + 1) # SyntaxError: cannot use `;` in parentheses
インデント
ErgはPythonと同じくインデントを使ってブロックを表します。ブロックの開始を示すトリガーとなる演算子(特殊形式)は、=
, ->
, =>
の3種類です(その他に、演算子ではありませんが:
と|
もインデントを生成します)。それぞれの意味は後述します。
f x, y =
x + y
for! 0..9, i =>
print! i
for! 0..9, i =>
print! i; print! i
ans = match x:
0 -> "zero"
_: 0..9 -> "1 dight"
_: 10..99 -> "2 dights"
_ -> "unknown"
また1行が長くなりすぎる場合、\
を使って途中で改行させることができます。
# これは`x + y + z`と解釈されず`x; +y; +z`と解釈される
x
+ y
+ z
# これは`x + y + z`と解釈される
x \
+ y \
+ z
リテラル
基本的なリテラル
整数リテラル(Int Literal)
0, -0, 1, -1, 2, -2, 3, -3, ...
整数(Int)リテラルはInt型のオブジェクトです。
Note:
Int
型の部分型としてNat
型が存在します。 0以上の数値はNat
型とも解釈できます。
有理数リテラル(Ratio Literal)
0.00, -0.0, 0.1, 400.104, ...
有理数を表すリテラルです。専ら小数として表現されますが、内部的には分数として扱われます。
Ratio
リテラルで整数部分または小数部分が0
のときは、その0
を省略できます。
assert 1.0 == 1.
assert 0.5 == .5
Note: この
assert
という関数は、1.0
と1.
が等しいことを示すために使用しました。 以降のドキュメントでは、結果が等しいことを示すためにassert
を使用する場合があります。
文字列リテラル(Str Literal)
文字列を表すリテラルです。"
でクオーテーション(囲み)します。Unicodeで表現可能な文字列は、すべて使用できます。
Pythonとは違い、'
ではクオーテーションできません。文字列の中で"
を使いたいときは\"
としてください。
"", "a", "abc", "111", "1# 3f2-3*8$", "こんにちは", "السَّلَامُ عَلَيْكُمْ", ...
\{...}
によって文字列の中に式を埋めこめます。これを文字列補間(string interpolation)といいます。
\{...}
自体を出力したい場合は\\{...}
とします。
assert "1 + 1 is 2" == "\{1} + \{1} is \{1+1}"
ドキュメンテーションコメントも文字列リテラルとして扱われるので、文字列補間が使えます。 これはコンパイル時に展開されます。コンパイル時に確定できない式を埋め込むと警告されます。
PI = 3.14159265358979323
'''
S(r) = 4 × \{PI} × r^2
'''
sphere_surface r = 4 * PI * r ** 2
指数リテラル(Exponential Literal)
これは学術計算でよく使用される指数表記を表すリテラルです。Ratio
型のインスタンスになります。
非常に大きな/小さな数を表すときに使用します。Pythonと表記法は同じです。
1e-34, 0.4e-10, 2.455+e5, 245e5, 25E5, ...
assert 1e-10 == 0.0000000001
リテラルを組み合わせて生成するもの(複合リテラル)
これらのリテラルは、それぞれ単独で解説されているドキュメントがあるので、詳しくはそちらを参照してください。
配列リテラル(Array Literal)
[], [1], [1, 2, 3], ["1", "2",], ...
組リテラル(Tuple Literal)
(), (1, 2, 3), (1, "hello", True), ...
辞書リテラル(Dict Literal)
{:}, {"one": 1}, {"one": 1, "two": 2}, {"1": 1, "2": 2}, {1: "1", 2: True, "three": [1]}, ...
レコードリテラル(Record Literal)
{=}, {one = 1}, {one = 1; two = 2}, {.name = "John"; .age = 12}, {.name = Str; .age = Nat}, ...
集合リテラル(Set Literal)
{}, {1}, {1, 2, 3}, {"1", "2", "1"}, ...
Array
リテラルとの違いとして、Set
では重複する要素が取り除かれます。
集合演算を行うときや、重複を許さないデータを扱うときに便利です。
assert {1, 2, 1} == {1, 2}
リテラルのように見えるがそうではないもの
真偽値オブジェクト(Boolean Object)
True, False
真偽値オブジェクトはBool型の単なるシングルトン(ダブルトン?)です。
Pythonからの伝統により、Bool
型はInt
型ないしNat
型のサブタイプとなります。
すなわち、True
は1
、False
は0
と解釈できます。
assert True * 2 == 2
Noneオブジェクト
None
NoneType
型のシングルトンです。
範囲オブジェクト(Range Object)
assert 0..10 in 5
assert 0..<10 notin 10
assert 0..9 == 0..<10
assert (0..5).to_set() == {1, 2, 3, 4, 5}
assert "a" in "a".."z"
Pythonのrange
と似ていますが、IntだけでなくStrオブジェクトなども範囲として扱うことができます。
浮動小数点数オブジェクト(Float Object)
assert 0.0f64 == 0
assert 0.0f32 == 0.0f64
Ratio
オブジェクトにFloat 64
(倍精度浮動小数点型)の単位オブジェクトであるf64
を乗算したものです。f32
をかけることでFloat 32
(単精度浮動小数点型)も指定できます。
これらは基本的にRatio
よりも高速に計算できますが、誤差が生じる可能性があります。
assert 0.1 + 0.2 == 0.3
assert 0.1f64 + 0.2f64 != 0.3f64 # Oops!
複素数オブジェクト(Complex Object)
1+2Im, 0.4-1.2Im, 0Im, Im
Complex
オブジェクトは、単に虚数単位オブジェクトであるIm
との演算の組み合わせで表します。
*-less multiplication
Ergでは、解釈に紛れがない限り乗算を表す*
を省略できます。
ただし、演算子の結合強度は*
よりも強く設定されています。
# `assert (1*m) / (1*s) == 1*(m/s)`と同じ
assert 1m / 1s == 1 (m/s)
変数
変数は代数の一種です。Ergにおける代数―紛れがなければ単に変数と呼ばれることもあります―とは、オブジェクトを名前付けしてコード中の別の場所から利用できるようにする機能を指します。
変数は以下のように定義します。
n
の部分を変数名(または、識別子)、=
を代入演算子、1
の部分を代入値と呼びます。
n = 1
このようにして定義したn
は、以降整数オブジェクトである1
を示す変数として使用できます。このシステムを代入(または束縛)といいます。
いま1
はオブジェクトであると述べました。オブジェクトが何であるかは後述しますが、今は代入できるもの、すなわち代入演算子(=
など)の右側におけるものとしておきます。
変数の「型」を指定したい場合は以下のようにします。型とは、これも後述しますが、大まかにはオブジェクトの属する集合です。
ここでn
は自然数(0以上の整数)に属する、すなわちNat
型であると指定しています。
n: Nat = 1
他の言語とは違い、多重代入はできないので注意してください。
# NG
l1 = l2 = [1, 2, 3] # SyntaxError: multiple assignment not allowed
このようなことをしたい場合、.clone()
を使います。これはオブジェクトのコピーを作成します。
# OK
l1 = [1, 2, 3]
l2 = l1.clone()
また、変数への再代入もできません。その代わりに使える機能、すなわち可変な状態を保持する機能については後述します。
i = 1
i = i + 1 # AssignError: cannot assign twice
内側のスコープで同じ名前の変数を定義できますが、上に被せているだけで、値を破壊的に書き換えているわけではありません。外側のスコープに戻れば値も戻ります。 これはPythonの「文」のスコープとは違う挙動なので注意してください。 このような機能は一般にシャドーイングと言います。ただし他言語のシャドーイングとは違い、同一スコープではシャドーイングできません。
簡単に言うと、Ergでは変数が勝手に書き換えられていないか心配する必要はないのです。
x = 0
# x = 1 # AssignError: cannot assign twice
if x.is_zero(), do:
x = 1 # 外側のxとは同名の別物になる
assert x == 1
assert x == 0
以下は一見すると可能なように思えますが、やはりできません。これは技術的な制約ではなく、設計判断です。
x = 0
if x.is_zero(), do:
x = x + 1 # NameError: cannot define variables refer to variables with the same name
assert x == 1
assert x == 0
定数
定数も代数の一種です。識別子を大文字で始めると定数として扱われます。一度定義したら変わらないので、定数と呼ばれます。
N
の部分を定数名(または、識別子)と呼びます。その他は変数と同じです。
N = 0
if True, do:
N = 1 # AssignError: constants cannot be shadowed
pass()
定数は定義されたスコープ以降では不変になります。シャドーイングもできません。この性質から、定数はパターンマッチで使用できます。 パターンマッチについては後に説明します。
定数は、数学的定数、外部リソースに関する情報など不変な値に対して使用すると良いでしょう。 型以外のオブジェクトは、オールキャップス(全ての文字を大文字にするスタイル)にするのが一般的です。
PI = 3.141592653589793
URL = "https://example.com"
CHOICES = ["a", "b", "c"]
PI = 3.141592653589793
match! x:
PI => print! "π"
other => print! "other"
上のコードはx
が3.141592653589793
のときπ
を出力します。x
を他の数字に変えると、other
を出力します。
定数には代入できないものがあります。可変オブジェクトなどです。詳しくは後述しますが、可変オブジェクトは内容を変更することができるオブジェクトです。 これは定数には定数式のみを代入できるという規則があるためです。定数式についても後述することとします。
X = 1 # OK
X = !1 # TypeError: cannot define Int! object as a constant
X = input!() # SyntaxError: not a constant expression
代数の削除
Del
関数を使うことで、代数を削除することが出来ます。その代数に依存している(その代数の値を直接参照している)他の代数もまとめて削除されます。
x = 1
y = 2
z = 3
f a = x + a
assert f(2) == 3
Del x # xを直接参照しているためfも削除される
Del y, z
f(2) # NameError: f is not defined (deleted in line 6)
ただし、Del
によって削除できるのはモジュール内で定義された代数のみです。True
などの組み込み定数は削除できません。
Del True # TypeError: cannot delete built-in constants
Del print! # TypeError: cannot delete built-in variables
Del
によって削除した名前は、その後に再利用することが出来ます。
付録: 代入と同値性
注意として、x = a
であるとき、x == a
とは限らない。例としてはFloat.NaN
がある。これはIEEE 754により定められた正式な浮動小数点数の仕様である。
x = Float.NaN
assert x != Float.NaN
assert x != x
その他、そもそも同値関係が定義されていないオブジェクトも存在する。
f = x -> x**2 + 2x + 1
g = x -> (x + 1)**2
f == g # TypeError: cannot compare function objects
C = Class {i: Int}
D = Class {i: Int}
C == D # TypeError: cannot compare class objects
厳密に言うと=
は右辺値をそのまま左辺の識別子に代入するわけではない。
関数オブジェクトやクラスオブジェクトの場合、オブジェクトに変数名の情報を与えるなどの「修飾」を行う。
ただし構造型の場合はその限りではない。
f x = x
print! f # <function f>
g x = x + 1
print! g # <function g>
C = Class {i: Int}
print! C # <class C>
宣言
宣言は、使用する変数の型を指定する構文です。 宣言はコード中のどこでも可能ですが、宣言しただけでその変数を参照することはできません。必ず初期化する必要があります。 代入後の宣言では、代入されたオブジェクトと型が適合するかをチェック可能です。
i: Int
# i: Int = 2のように代入と同時に宣言できる
i = 2
i: Num
i: Nat
i: -2..2
i: {2}
代入後の宣言はassert
による型チェックと似ていますが、コンパイル時にチェックされるという特徴があります。
実行時のassert
による型チェックは「〇〇型かもしれない」で検査が可能ですが、コンパイル時の:
による型チェックは厳密です。
「〇〇型である」ことが確定していなくては検査を通らず、エラーとなります。
i = (-1..10).sample!()
i: Int # -1~10までの整数はすべてIntに属するので、これは通る
i = (-1..10).sample!()
i: Nat # これは通らない(-1はNatすなわち0以上の整数ではないため)
i = (-1..10).sample!()
assert i in Nat # これは通る可能性がある
関数は以下の2種類の方法で宣言が可能です。
f: (x: Int, y: Int) -> Int
f: (Int, Int) -> Int
引数名を明示して宣言した場合、定義時に名前が違うと型エラーとなります。引数名の任意性を与えたい場合は2番目の方法で宣言すると良いでしょう。その場合、型検査で見られるのはメソッド名とその型のみです。代償としてキーワード引数による呼び出しはできなくなります。
関数
関数は「引数」を受け取ってそれを加工し、「戻り値」として返すブロックです。以下のように定義します。
add x, y = x + y
# or
add(x, y) = x + y
関数定義の際に指定される引数は、詳しくは仮引数(parameter)と呼ばれるものです。
これに対し、関数呼び出しの際に渡される引数は、実引数(argument)と呼ばれるものです。
add
はx
とy
を仮引数として受け取り、それを足したもの、x + y
を返す関数です。
定義した関数は、以下のようにして呼び出し(適用)ができます。
add 1, 2
# or
add(1, 2)
コロン適用スタイル
関数はf x, y, ...
のように呼び出しますが、実引数 が多く一行では長くなりすぎる場合は:
(コロン)を使った適用も可能です。
f some_long_name_variable_1 + some_long_name_variable_2, some_long_name_variable_3 * some_long_name_variable_4
f some_long_name_variable_1 + some_long_name_variable_2:
some_long_name_variable_3 * some_long_name_variable_4
上の2つのコードは同じ意味です。このスタイルはif
関数などを使用するときにも便利です。
result = if Bool.sample!():
do:
log "True was chosen"
1
do:
log "False was chosen"
0
この場合、:
の後はコメント以外のコードを書いてはならず、必ず改行しなくてはなりません。
また、関数の直後に:
を使うことはできません。これができるのはdo
とdo!
のみです。
# NG
f:
x
y
# Ok
f(
x,
y
)
キーワード引数
仮引数の数が多い関数を定義されていると、実引数を渡す際に順番を間違える危険性があります。 そのような場合はキーワード引数を使用して呼び出すと安全です。
f x, y, z, w, v, u: Int = ...
上に定義された関数は、引数が多く、分かりにくい並びをしています。 このような関数は作るべきではありませんが、他人の書いたコードを使うときにこのようなコードにあたってしまうかもしれません。 そこで、キーワード引数を使います。キーワード引数は並びよりも名前が優先されるため、順番を間違えていても名前から正しい引数に値が渡されます。
f u := 6, v := 5, w := 4, x := 1, y := 2, z := 3
デフォルト引数
ある仮引数が大抵の場合決まりきっており省略できるようにしたい場合、デフォルト引数を使うと良いでしょう。
デフォルト引数は:=
(default-assign operator)で指定します。base
が指定されなかったらmath.E
をbase
に代入します。
math_log x: Ratio, base := math.E = ...
assert math_log(100, 10) == 2
assert math_log(100) == math_log(100, math.E)
引数を指定しないこととNone
を代入することは区別されるので注意してください。
p! x := 0 = print! x
p!(2) # 2
p!() # 0
p!(None) # None
型指定、パターンと併用することもできます。
math_log x, base: Ratio := math.E = ...
f [x, y] := [1, 2] = ...
しかしデフォルト引数内では、後述するプロシージャを呼び出したり、可変オブジェクトを代入したりすることができません。スクリプトを実行する度にデフォルト引数の値が変わってしまうのは混乱を招くためです。
f x := p! 1 = ... # NG
また、定義したばかりの引数はデフォルト引数に渡す値として使えません。
f x := 1, y := x = ... # NG
可変長引数
実引数をログ(記録)として出力するlog
関数は、任意の個数の引数を受け取ることができます。
log "Hello", "World", "!" # Hello World !
このような関数を定義したいときは、仮引数に*
を付けます。このようにすると、引数を可変長の配列として受け取ることができます。
f *x: Int =
# x: [Int; _]
for x, i ->
log i
# x == [1, 2, 3, 4, 5]
f 1, 2, 3, 4, 5
複数パターンによる関数定義
fib n: Nat =
match n:
0 -> 0
1 -> 1
n -> fib(n - 1) + fib(n - 2)
上のような定義直下にmatch
が現れる関数は、下のように書き直すことができます。
fib 0 = 0
fib 1 = 1
fib(n: Nat): Nat = fib(n - 1) + fib(n - 2)
複数のパターンによる関数定義は、いわゆるオーバーロード(多重定義)ではないことに注意してください。1つの関数はあくまで単一の型のみを持ちます。上の例では、n
は0
や1
と同じ型である必要があります。また、match
と同じくパターンの照合は上から順に行われます。
違うクラスのインスタンスが混在する場合は、最後の定義で関数引数がOr型であることを明示しなくてはなりません。
f "aa" = ...
f 1 = ...
# `f x = ...` is invalid
f x: Int or Str = ...
また、match
と同じく網羅性がなくてはなりません。
fib 0 = 0
fib 1 = 1
# PatternError: pattern of fib's parameter is not exhaustive
しかし、上のような場合でも、後述する篩型を使って明示的に型指定することで、網羅性を獲得できます。
fib: 0..1 -> 0..1
fib 0 = 0
fib 1 = 1
# OK
fib 2 # TypeError: the first parameter type of fib is 0..1, but got {2}
再帰関数
再帰関数は自身を定義に含む関数です。
簡単な例として階乗の計算を行う関数factorial
を定義してみます。階乗とは、「それ以下の正の数をすべてかける」計算です。
5の階乗は5*4*3*2*1 == 120
となります。
factorial 0 = 1
factorial 1 = 1
factorial(n: Nat): Nat = n * factorial(n - 1)
まず階乗の定義から、0と1の階乗はどちらも1です。
順に考えて、2の階乗は2*1 == 2
、3の階乗は3*2*1 == 6
、4の階乗は4*3*2*1 == 24
となります。
ここでよく見ると、ある数nの階乗はその前の数n-1の階乗にnをかけた数となることがわかります。
これをコードに落とし込むと、n * factorial(n - 1)
となるわけです。
factorial
の定義に自身が含まれているので、factorial
は再帰関数です。
注意として、型指定を付けなかった場合はこのように推論されます。
factorial: |T <: Sub(Int, T) and Mul(Int, Int) and Eq(Int)| T -> Int
factorial 0 = 1
factorial 1 = 1
factorial n = n * factorial(n - 1)
しかし例え推論が出来たとしても、再帰関数には型を明示的に指定しておくべきです。上の例では、factorial(-1)
のようなコードは有効ですが、
factorial(-1) == -1 * factorial(-2) == -1 * -2 * factorial(-3) == ...
となって、この計算は停止しません。再帰関数は慎重に値の範囲を定義しないと無限ループに陥ってしまう可能性があります。 型指定は想定しない値の受け入れを防ぐのにも役立つというわけです。
高階関数
高階関数は引数や返り値に関数を取る関数のことを指します。 例えば引数に関数を取る高階関数は以下のように記述できます。
arg_f = i -> log i
higher_f(x: (Int -> NoneType)) = x 10
higher_f arg_f # 10
もちろん、関数を返り値にすることもできます。
add(x): (Int -> Int) = y -> x + y
add_ten = add(10) # y -> 10 + y
add_hundred = add(100) # y -> 100 + y
assert add_ten(1) == 11
assert add_hundred(1) == 101
このようにして関数を引数や返り値にとることで、より柔軟な表現を関数で定義することができます。
コンパイル時関数
関数名を大文字で始めると、その関数はコンパイル時関数であると宣言したことになります。ユーザー定義のコンパイル時関数は、引数がすべて定数で、かつ型を明示する必要があります。 コンパイル関数ができることは限られています。コンパイル時関数内で使えるのは定数式のみ、すなわち、いくつかの演算子(四則演算や比較演算、型構築演算など)とコンパイル時関数のみです。代入する引数も定数式である必要があります。 そのかわり、計算をコンパイル時に行うことができるというメリットがあります。
Add(X, Y: Nat): Nat = X + Y
assert Add(1, 2) == 3
Factorial 0 = 1
Factorial(X: Nat): Nat = X * Factorial(X - 1)
assert Factorial(10) == 3628800
math = import "math"
Sin X = math.sin X # ConstantError: this function is not computable at compile time
コンパイル時関数は多相型の定義などでもよく使われます。
Option T: Type = T or NoneType
Option: Type -> Type
コンパイル時関数の仮引数は、既に定義されている如何なる定数とも違う名前である必要があります。名前が被った場合、定数パターンとして解釈されます。
# Intは仮引数ではない。型Intのみを引数にとる関数
K Int = None
付録1: 関数の比較
Ergでは、関数に==
が定義されていません。それは関数の構造的な同値性判定アルゴリズムが一般には存在しないためです。
f = x: Int -> (x + 1)**2
g = x: Int -> x**2 + 2x + 1
assert f == g # TypeError: cannot compare functions
f
とg
は常に同じ結果を返しますが、その判定を行うのは至難の業です。コンパイラに代数学を教え込む必要があります。
そのため、Ergは関数の比較をまるごと諦めており、(x -> x) == (x -> x)
もコンパイルエラーになります。これはPythonとは違った仕様なので注意する必要があります。
# Pythonの奇妙な例
f = lambda x: x
assert f == f # True
assert (lambda x: x) != (lambda x: x) # Passed
付録2: ()の補完
f x: Object = ...
# これは以下のように補完される
f(x: Object) = ...
f a
# これは以下のように補完される
f(a)
f a, b # TypeError: f() takes 1 positional argument but 2 were given
f(a, b) # TypeError: f() takes 1 positional argument but 2 were given
f((a, b)) # OK
関数型T -> U
は実際のところ、(T,) -> U
の糖衣構文です。
組み込み関数
if
if
は条件に応じて処理を変える関数です。
result: Option Int = if! Bool.sample!(), do:
log "True was chosen"
1
print! result # None (または1)
.sample!()
は集合の値をランダムに返します。もし戻り値が真ならば、print! "True"
が実行されます。
条件が偽であった際の処理も指定できます。2つ目のdoブロックはelseブロックと呼ばれます。
result: Nat = if Bool.sample!():
do:
log "True was chosen"
1
do:
log "False was chosen"
0
print! result # 1 (または0)
処理が1行ならば、インデントを省略できます。
result = if Bool.sample!():
do 1
do 0
for
繰り返し行う処理を書くときはfor
が使えます。
match_s(ss: Iterator(Str), pat: Pattern): Option Str =
for ss, s ->
if pat.match(s).is_some():
break s
演算子
演算子(オペレーター)は、演算を表す記号です。被演算子(オペランド)は演算子の(左)右にあるもので、Ergでは専らオブジェクトです。
演算子は関数の一種であり、したがってそれ自体も第一級オブジェクトで変数に束縛できます。束縛の際は``で囲む必要があります。
+
(と-
)については、単項演算子と二項演算子の両方が存在するため、一意化するために_+_
(二項演算)/+_
(単項演算)のどちらかを指定する必要があります。
add = `+` # SyntaxError: specify `_+_` or `+_`
add = `_+_`
assert f(1, 2) == 3
assert f("a", "b") == "ab"
mul = `*` # OK, this is binary only
assert mul(1, 2) == 2
ただし、特殊形式と呼ばれる一部の演算子は束縛できないことに注意してください。
def = `=` # SyntaxError: cannot bind `=` operator, this is a special form
# NG: def x, 1
function = `->` # SyntaxError: cannot bind `->` operator, this is a special form
# NG: function x, x + 1
副作用とプロシージャ
これまでprint!
の!
の意味を説明せずにいましたが、いよいよその意味が明かされます。この!は、ズバリこのオブジェクトが「副作用」のある「プロシージャ」であることを示しています。プロシージャは関数に「副作用」という効果を与えたものです。
f x = print! x # EffectError: functions cannot be assigned objects with side effects
# hint: change the name to 'f!'
上のコードはコンパイルエラーになります。関数中でプロシージャを使用しているからです。このような場合は、プロシージャとして定義しなくてはなりません。
p! x = print! x
p!
, q!
, ...は、プロシージャを表すための典型的な変数名です。
このようにして定義したプロシージャもまた関数内では使用できないため、副作用は完全に隔離されるわけです。
メソッド
関数とプロシージャにはそれぞれメソッドが存在します。関数メソッドはself
の不変参照のみを取れ、プロシージャルメソッドはself
の可変参照を取れます。
self
は特殊な引数で、メソッドの文脈では呼び出したオブジェクト自身を指します。参照のself
は他のいかなる変数にも代入できません。
C.
method ref self =
x = self # OwnershipError: cannot move out 'self'
x
メソッドはself
の所有権を奪うこともできます。そのメソッドの定義ではref
またはref!
を外します。
n = 1
s = n.into(Str) # 所有権がnから移動し、s = '1'になる
n # ValueError: n was moved by .into (line 2)
可変参照を持てるのは常に1つのプロシージャルメソッドのみです。さらに可変参照が取られている間は元のオブジェクトから参照を取れなくなります。その意味でref!
はself
に副作用を引き起こします。
ただし、可変参照から(不変/可変)参照の生成はできることに注意してください。これによって、プロシージャルメソッド中で再帰したりself
をprint!
できたりします。
T -> T # OK (移動)
T -> Ref T # OK
T => Ref! T # OK (一度のみ)
Ref T -> T # NG
Ref T -> Ref T # OK
Ref T => Ref! T # NG
Ref! T -> T # NG
Ref! T -> Ref T # OK
Ref! T => Ref! T # OK
付録: 副作用の厳密な定義
コードに副作用があるかないかのルールはすぐに理解できるものではありません。 理解できるようになるまでは、とりあえず関数として定義してエラーが出ればプロシージャとするコンパイラ任せのスタイルを推奨します。 しかし、言語の厳密な仕様を把握しておきたい人のために、以下ではもう少し詳しく副作用について説明します。
まず、Ergにおける副作用に関して、戻り値の同値性は関係がないということに注意してください。
任意のx
に対してp!(x) == p!(x)
となるプロシージャが存在します(常にNone
を返すなど)し、f(x) != f(x)
となる関数も存在します。
前者の例はprint!
で、後者の例は以下の関数です。
nan _ = Float.NaN
assert nan(1) != nan(1)
また、クラスや関数のように同値判定自体ができないオブジェクトも存在します。
T = Structural {i = Int}
U = Structural {i = Int}
assert T == U
C = Class {i = Int}
D = Class {i = Int}
assert C == D # TypeError: cannot compare classes
本題に戻ります。Ergにおける「副作用」の正確な定義は、
- 外部の可変な情報にアクセスすること
です。外部とは、一般には外側のスコープを指します。Ergがタッチできないコンピューターリソースや、実行前/後の情報については「外部」に含まれません。「アクセス」は書き込みだけでなく読み込みも含めます。
例としてprint!
プロシージャについて考えます。print!
は一見何の変数も書き換えていないように見えます。しかし、もしこれが関数だったとすると、例えばこのようなコードで外側変数を書き換えられます。
camera = import "some_camera_module"
ocr = import "some_ocr_module"
n = 0
_ =
f x = print x # 仮にprintを関数として使えたとする
f(3.141592)
cam = camera.new() # カメラはPCのディスプレイの方向を向く
image = cam.shot!()
n = ocr.read_num(image) # n = 3.141592
camera
モジュールはあるカメラ製品のAPIを提供する外部のライブラリ、ocr
はOCR(光学文字認識)のためのライブラリとします。
直接の副作用はcam.shot!()
によって引き起こされていますが、明らかにその情報はf
から漏洩しています。よって、print!
は性質上関数とはなれません。
とはいえ、関数中で値を一時的に確認するとき、そのためだけに関連する関数まで!
を付けたくない場合もあるでしょう。その際はlog
関数が使えます。
log
はコード全体の実行後に値を表示します。これにより、副作用は伝搬されません。
log "これは実行後にプリントされます"
print! "これはすぐにプリントされます"
# これはすぐにプリントされる
# これは実行後にプリントされる
つまり、プログラムへのフィードバックがない、言い換えればいかなる外部オブジェクトもその情報を使うことが出来ないならば、情報の「漏洩」自体は許される場合があります。「伝搬」されなければよいのです。
プロシージャ
プロシージャは副作用を許容する関数を意味します。
基本的な定義や利用方法は関数を参照してください。
関数名に対して!
をつけることで定義することができます。
proc!(x: Int!, y: Int!) =
for! 0..x, i =>
for 0..y, j =>
print! i, j
プロシージャは可変オブジェクトを取り扱う際に必要となります。 ですが、可変オブジェクトを引数に持つときやプロシージャの定義するだけの場合はプロシージャであるとは限りません。
peek_str s: Str! = log s
make_proc(x!: (Int => Int)): (Int => Int) = y => x! y
p! = make_proc(x => x)
print! p! 1 # 1
またプロシージャと関数はproc :> func
の関係にあります。
そのため、プロシージャ内で関数ブロックを定義することもできます。
しかし、逆はできないので注意をしてください。
proc!(x: Int!) = y -> log x, y # OK
func(x: Int) = y => print! x, y # NG
バインド
プロシージャはスコープ外の可変変数を操作することができます。
x = !0
proc!() =
x.inc!()
proc!()
assert x == 1
このとき、proc!
は以下のような型を持ちます。
proc!: {|x: Int!|}() => ()
{|x: Int!|}
の部分はバインド列と呼ばれ、そのプロシージャが操作する変数とその型を表します。
バインド列は自動で導出されるため、明示的に書く必要はありません。
注意として、通常のプロシージャは予め決められた外部変数のみを操作することができます。これはつまり、引数に渡された変数を書き換えることはできないということです。
そのようなことがしたい場合は、プロシージャルメソッドを使う必要があります。プロシージャルメソッドは、self
を書き換えることができます。
C! N = Class {arr = [Int; N]!}
C!.
new() = Self!(0)::__new__ {arr = ![]}
C!(N).
# push!: {|self: C!(N) ~> C!(N+1)|}(self: RefMut(C!(N)), x: Int) => NoneType
push! ref! self, x = self.arr.push!(x)
# pop!: {|self: C!(N) ~> C!(N-1)|}(self: RefMut(C!(N))) => Int
pop! ref! self = self.arr.pop!()
c = C!.new()
c.push!(1)
assert c.pop!() == 1
組み込みプロシージャ
id!
オブジェクトのユニークな識別番号を返します。
純粋なErgの意味論の中では同一の構造を持つオブジェクトの間に差異を見出す事はできませんが、実際のところ、オブジェクトはメモリ上の位置が異なります。id!
はこの位置を表す数値を返します。
配列
配列はもっとも基本的な コレクション(集約) です。 コレクションとは、内部にオブジェクトを複数保持できるオブジェクトのことです。
a = [1, 2, 3]
a: [Int; 3] # 型指定: セミコロンの後の数字は要素数
# 要素数がわからない場合は省略できる
a: [Int]
mut_a = [!1, !2, !3]
mut_a[0].inc!()
assert mut_a == [2, 2, 3]
配列には、原則として違う型のオブジェクトを入れることはできません。
[1, "a"] # TypeError: 1st element is Int, but 2nd element is Str
しかし、このように要素の型を明示的に指定すると制限を回避できます。
[1: Int or Str, "a"]
しかしこのような場合、基本的には後述する タプル を使うべきです。
(1, "a")
スライス
配列は、複数の値をまとめて取り出すこともできます。これをスライスと呼びます。
l = [1, 2, 3, 4]
# Pythonのl[1:3]に相当
assert l[1..<3] == [2, 3]
assert l[1..2] == [2, 3]
# l[1]と同じ
assert l[1..1] == [2]
# Pythonのl[::2]に相当
assert l[..].step(2) == [2, 4]
スライスで得られるオブジェクトは配列の(不変)参照です。
print! Typeof l[1..2] # Ref [Int; 4]
辞書
Dictはキーと値のペアを持つコレクションです。
ids = {"Alice": 145, "Bob": 214, "Charlie": 301}
assert ids["Alice"] == 145
キーはHashableであるならば文字列でなくても構いません。
# rangeオブジェクトをキーにするのは非推奨(スライスと混同される)
r = {1..3: "1~3", 4..6: "4~6", 7..9: "7~9"}
assert r[1..3] == "1~3"
l = {[]: "empty", [1]: "1"}
assert l[[]] == "empty"
l = {0.0: "a", 1.0: "b"} # TypeError: Float is not Hashable
Dictに順番は関係ありません。また、重複する要素を持つことも出来ません。この点でDictはSetと似ています。 Dictは値付きのSetと言うこともできるでしょう。
{"Alice": 145, "Bob": 214, "Charlie": 301} == {"Alice": 145, "Charlie": 301, "Bob": 214}
DictリテラルからDictを生成する場合、キーの重複がないかチェックされます。 重複がある場合コンパイルエラーとなりますが、自明でない場合もあり、その場合は後に登録された方が残ります(左から順番に登録されます)。
{"Alice": 145, "Alice": 1} # KeyError: Duplicate key "Alice"
x = f(...) # x == 2
{2x+2: 1, 2(x+1): 2} # {6: 2}
空のDictは{:}
で生成します。{}
は空の配列を表すことに注意してください。
mut_dict = !{:}
mut_dict.insert! "Alice", 145
mut_dict.insert! "Bob", 214
assert mut_dict["Alice"] == 145
非等質な辞書
キー・値の型は単一でなくてもよく、そのような辞書を 非等質な辞書(heterogenous dict) といいます。
d: {Str: Int, Int: Str} = {"a": 1, 1: "a"}
assert d["a"] == 1
assert d[1] == "a"
しかし、違う型のキーに同じ型の値、または同じ型のキーに違う型の値をあてることはできません。 このような場合は代わりにOr型(Union)を使います。
invalid1 = {1: "a", "a": "b"}
invalid2 = {1: "a", 2: 2}
# Ergの型推論はOr型を推論しないため、型指定が必要となる
valid1: {Int or Str: Str} = {1: "a", "a": "b"}
valid2: {Int: Int or Str} = {1: "a", 2: 2}
型表示との併用
{}
の中でのx: y
という形式は、辞書のキーと値のペアとして優先的に解釈されます。
型表示として使いたい場合は、()
で囲む必要があります。
x = "a"
{(x: Str): 1}
添字アクセス
[]
は通常のメソッドとは異なっています。
a = [!1, !2]
a[0].inc!()
assert a == [2, 2]
サブルーチンの戻り値には参照を指定できないということを思い出してください。
a[0]
の型は、ここでは明らかにRef!(Int!)
であるはずです(a[0]
の型は文脈に依存します)。
よって、[]
は実際には.
と同じく特別な構文の一部です。Pythonとは違い、オーバーロードできません。
メソッドで[]
の挙動を再現することもできません。
C = Class {i = Int!}
C.steal(self) =
self::i
C.get(ref self) =
self::i # TypeError: `self::i` is `Int!` (require ownership) but `get` doesn't own `self`
# OK (assigning)
c = C.new({i = 1})
i = c.steal()
i.inc!()
assert i == 2
# or (own_do!)
own_do! C.new({i = 1}).steal(), i => i.inc!()
# NG
C.new({i = 1}).steal().inc!() # OwnershipWarning: `C.new({i = 1}).steal()` is not owned by anyone
# hint: assign to a variable or use `uwn_do!`
また、[]
は所有権を奪うこともできますが、その際に要素がシフトするわけではありません。
a = [!1, !2]
i = a[0]
i.inc!()
assert a[1] == 2
a[0] # OwnershipError: `a[0]` is moved to `i`
タプル
タプルは配列と似ていますが、違う型のオブジェクトを保持できます。 このようなコレクションを非等質なコレクションと呼びます。対して等質なコレクションには配列、セットなどがあります。
t = (1, True, "a")
(i, b, s) = t
assert(i == 1 and b == True and s == "a")
タプルt
はt.n
の形式でn番目の要素を取り出すことができます。Pythonと違い、t[n]
ではないことに注意してください。
これは、タプル要素のアクセスはメソッド(配列の[]
はメソッドです)というより属性に近い(コンパイル時に要素の存在がチェックされる、nによって型が変わりうる)ためです。
assert t.0 == 1
assert t.1 == True
assert t.2 == "a"
括弧()
はネストしないとき省略可能です。
t = 1, True, "a"
i, b, s = t
タプルは違う型のオブジェクトを保持できますが、そのかわり配列のようなイテレーションができなくなります。
t: ({1}, {2}, {3}) = (1, 2, 3)
(1, 2, 3).iter().map(x -> x + 1) # TypeError: type ({1}, {2}, {3}) has no method `.iter()`
# すべて同じ型の場合配列と同じように`(T; n)`で表せるが、これでもイテレーションは出来ない
t: (Int; 3) = (1, 2, 3)
assert (Int; 3) == (Int, Int, Int)
ただし、非等質なコレクション(タプルなど)はアップキャスト、Intersectionなどによって等質なコレクション(配列など)に変換できます。 これを等質化といいます。
homogenize rule
* (X, Y, Z, ...) can be [T; N] if T :> X, T :> Y, T :> Z, ...
t: (Int, Bool, Str) = (1, True, "a") # 非等質
a: [Int or Bool or Str; 3] = [1, True, "a"] # 等質
_a: [Show; 3] = [1, True, "a"] # 等質
_a.iter().map(x -> log x) # OK
t.try_into([Show; 3])?.iter().map(x -> log x) # OK
ユニット
要素が0個のタプルはユニットと言います。ユニットは値ですが、自身の型そのものも指します。
unit = ()
(): ()
ユニットはすべてのタプルのスーパークラスです。
() :> (Int; 0)
() :> (Str; 0)
() :> (Int, Str)
...
このオブジェクトの使いみちは、引数、戻り値がないプロシージャなどです。Ergのサブルーチンは、必ず引数と戻り値を持つ必要があります。しかしプロシージャなどの場合、副作用を起こすだけで意味のある引数・戻り値がない場合もあります。その際に「意味のない、形式上の値」としてユニットを使うわけです。
p!() =
# `print!`は意味のある値を返さない
print! "Hello, world!"
p!: () => () # 引数部分は構文の一部でありタプルではない
ただしPythonはこのようなときユニットではなくNone
を使う傾向があります。
Ergでの使い分けとしては、プロシージャなどではじめから意味のある値を返さないことが確定しているときは()
、要素の取得のように操作が失敗して何も得られなかったときはNone
を返してください。
レコード
レコードは、キーでアクセスするDictとコンパイル時にアクセスが検査されるタプルの性質を併せ持つコレクションです。 JavaScriptをやったことがある方ならば、オブジェクトリテラル記法の(より強化された)ようなものと考えてください。
john = {.name = "John"; .age = 21}
assert john.name == "John"
assert john.age == 21
assert john in {.name = Str; .age = Nat}
john["name"] # Error: john is not subscribable
.name
, .age
の部分を属性、"John"
, 21
の部分を属性値と呼びます。
JavaScriptのオブジェクトリテラルとの相違点は、文字列でアクセスできない点です。すなわち、属性は単なる文字列ではありません。
これは、値へのアクセスをコンパイル時に決定するためと、辞書とレコードが別物であるためといった理由があります。つまり、{"name": "John"}
はDict,{name = "John"}
はレコードです。
では、辞書とレコードはどう使い分ければいいのでしょうか。
一般的にはレコードの使用を推奨します。レコードには、コンパイル時に要素が存在するかチェックされる、 可視性(visibility) を指定できるなどのメリットがあります。
可視性の指定は、Java言語などでみられるpublic/privateの指定に相当します。詳しくは可視性を参照してください。
a = {x = 1; .y = x + 1}
assert a.y == 2
a.x # AttributeError: x is private
# Hint: declare as `.x`.
上の例はJavaScriptに習熟している人間からすると奇妙かもしれませんが、単にx
と宣言すると外部からアクセスできず、.
をつけると.
でアクセスできるというわけです。
属性に対する明示的な型指定もできます。
anonymous = {
.name: Option! Str = "Jane Doe"
.age = 20
}
anonymous.name.set! "John Doe"
レコードはメソッドも持てます。
o = {
.i = !0
.inc! ref! self = self.i.inc!()
}
assert o.i == 0
o.inc!()
assert o.i == 1
レコードに関して特筆すべき文法があります。レコードの属性値が全てクラス(構造型ではダメです)のとき、そのレコード自体が、自身の属性を要求属性とする型としてふるまいます。 このような型をレコード型と呼びます。詳しくは[レコード]の項を参照してください。
# レコード
john = {.name = "John"}
# レコード型
john: {.name = Str}
Named = {.name = Str}
john: Named
greet! n: Named =
print! "Hello, I am \{n.name}"
greet! john # "Hello, I am John"
print! Named.name # Str
レコードの分解
レコードは以下のようにして分解できます。
record = {x = 1; y = 2}
{x = a; y = b} = record
assert a == 1
assert b == 2
point = {x = 2; y = 3; z = 4}
match point:
{x = 0; y = 0; z = 0} -> "origin"
{x = _; y = 0; z = 0} -> "on the x axis"
{x = 0; ...} -> "x = 0"
{x = x; y = y; z = z} -> "({x}, {y}, {z})"
また、レコードは属性と同名の変数があるとき、例えばx = x
またはx = .x
をx
に、.x = .x
または.x = x
を.x
に省略できます。
ただし、属性が一つのときはセットと区別するために;
を付ける必要があります。
x = 1
y = 2
xy = {x; y}
a = 1
b = 2
ab = {.a; .b}
assert ab.a == 1
assert ab.b == 2
record = {x;}
tuple = {x}
assert tuple.1 == 1
この構文を利用して、レコードを分解して変数に代入できます。
# same as `{x = x; y = y} = xy`
{x; y} = xy
assert x == 1
assert y == 2
# same as `{.a = a; .b = b} = ab`
{a; b} = ab
assert a == 1
assert b == 2
空レコード
空のレコードは{=}
で表されます。空のレコードはUnitと同じく、自身のクラスそのものでもあります。
empty_record = {=}
empty_record: {=}
# Object: Type = {=}
empty_record: Object
empty_record: Structural {=}
{x = 3; y = 5}: Structural {=}
空のレコードは空のDict{:}
や空のセット{}
とは異なります。特に{}
とは意味が正反対なので注意が必要です(Pythonでは{}
は空の辞書となっているが、Ergでは!{:}
です)。
列挙型としての{}
は何も要素に含まない空虚な型です。Never
型は、これをクラス化したものです。
逆に、レコードクラスの{=}
は要求インスタンス属性がないので、全てのオブジェクトがこれの要素になります。Object
は、これのエイリアスです。
Object
(のパッチ)は.__sizeof__
などの極めて基本的な提供メソッドを持ちます。
AnyPatch = Patch Structural {=}
.__sizeof__ self = ...
.clone self = ...
...
Never = Class {}
注意として、{}
, Never
型と構造的に等価な型・クラスは他に存在できず、ユーザーが{}
, Class {}
を右辺に指定して型を定義するとエラーとなります。
これにより、例えば1..10 or -10..-1
とするところを1..10 and -10..-1
としてしまうようなミスを防げます。
また、合成の結果Object
となるような型(Int and Str
など)を定義すると、単にObject
とするように警告が出ます。
インスタントブロック
Ergにはもう一つインスタントブロックという構文がありますが、これは単に最後に評価した値を返すだけです。属性の保持はできません。
x =
x = 1
y = x + 1
y ** 3
assert x == 8
y =
.x = 1 # SyntaxError: cannot define an attribute in an entity block
データクラス
素のレコード(レコードリテラルで生成されたレコード)は、これ単体でメソッドを実装しようとすると、直接インスタンスに定義する必要があります。 これは効率が悪く、さらに属性の数が増えていくとエラー表示などが見にくくなり使いにくいです。
john = {
name = "John Smith"
age = !20
.greet! ref self = print! "Hello, my name is \{self::name} and I am \{self::age} years old."
.inc_age! ref! self = self::age.update! x -> x + 1
}
print! john + 1
# TypeError: + is not implemented for {name = Str; age = Int; .greet! = Ref(Self).() => None; inc_age! = Ref!(Self).() => None}, Int
そこで、このような場合はレコードクラスを継承します。このようなクラスをデータクラスと呼びます。 これについてはクラスの項で詳しく説明します。
Person = Inherit {name = Str; age = Nat}
Person.
greet! ref self = print! "Hello, my name is \{self::name} and I am \{self::age} years old."
inc_age! ref! self = self::age.update! x -> x + 1
john = Person.new {name = "John Smith"; age = 20}
print! john + 1
# TypeError: + is not implemented for Person, Int
セット
セットは集合を表し、データ構造的には重複、順序のない配列です。
assert Set.from([1, 2, 3, 2, 1]) == {1, 2, 3}
assert {1, 2} == {1, 1, 2} # 重複は自動で削除される
assert {1, 2} == {2, 1}
型や長さを指定して宣言することもできます。
a: {Int; 3} = {0, 1, 2} # OK
b: {Int; 3} = {0, 0, 0} # NG、重複が削除されて長さが変わる
# TypeError: the type of b is mismatched
# expected: Set(Int, 3)
# but found: Set({0, }, 1)
また、Eq
トレイトが実装されているオブジェクトのみが集合の要素になれます。
そのため、Floatなどを集合の要素として使用することはできません。
d = {0.0, 1.0} # NG
# Error[#1366]: File <stdin>, line 1, <module>::d
#
# 1 | d = {0.0, 1.0}
# : --------
# : |- expected: Eq
# : |- but found: {0.0f, 1.0f, }
# : `- Float has no equivalence relation defined. you should use l == R instead of l - r <= Float.EPSILON
#
# TypeError: the type of _ is mismatched
セットは集合演算を行えます。
assert 1 in {1, 2, 3}
assert not 1 in {}
assert {1} or {2} == {1, 2}
assert {1, 2} and {2, 3} == {2}
assert {1, 2} not {2} == {1}
セットは等質なコレクションです。別のクラスのオブジェクトを共存させるためには、等質化させなくてはなりません。
s: {Int or Str} = {"a", 1, "b", -1}
型としてのセット
セットは型としても扱えます。このような型は 列挙型(Enum type) と呼ばれます。
i: {1, 2, 3} = 1
assert i in {1, 2, 3}
セットの要素がそのまま型の要素になります。 セット自身は違うことに注意が必要です。
mut_set = {1, 2, 3}.into {Int; !3}
mut_set.insert!(4)
型
型はErgにおいて非常に重要な機能ですので、専用のセクションを用意しています。そちらをご覧ください。
Ergの型システム
以下では、Ergの型システムを概略的に説明します。詳細については他の項で解説します。
定義方法
Ergの特徴的な点として、(通常の)変数、関数(サブルーチン)、型(カインド)の定義にあまり大きな構文上の違いがないというところがあります。すべて、通常の変数・関数定義の文法に従って定義されます。
f i: Int = i + 1
f # <function f>
f(1) # 2
f.method self = ... # SyntaxError: cannot define a method to a subroutine
T I: Int = {...}
T # <kind 'T'>
T(1) # Type T(1)
T.method self = ...
D = Class {private = Int; .public = Int}
D # <class 'D'>
o1 = {private = 1; .public = 2} # o1はどのクラスにも属さないオブジェクト
o2 = D.new {private = 1; .public = 2} # o2はDのインスタンス
o2 = D.new {.public = 2} # InitializationError: class 'D' requires attribute 'private'(: Int) but not defined
分類
Erg のオブジェクトは全て型付けされています。
最上位の型は{=}
であり、__repr__
, __hash__
, clone
などを実装します(要求メソッドではなく、これらの属性はオーバーライドもできません)。
Ergの型システムは構造的部分型(Structural subtyping, SST)を取り入れています。このシステムにより型付けされる型を構造型(Structural type)と呼びます。
構造型には大きく分けて3種類、Attributive(属性型)/Refinement(篩型)/Algebraic(代数演算型)があります。
Record | Enum | Interval | Union | Intersection | Diff | |
---|---|---|---|---|---|---|
kind | Attributive | Refinement | Refinement | Algebraic | Algebraic | Algebraic |
generator | record | set | range operator | or operator | and operator | not operator |
記名的部分型(Nominal subtyping, NST)を使用することもでき、SST型のNST型への変換を型の記名化(Nominalization)と呼びます。こうしてできた型を記名型(Nominal type)と呼びます。 Ergでは、記名型はクラスとトレイトがそれに該当します。単にクラス/トレイトといった場合、それはレコードクラス/レコードトレイトを指す場合が多いです。
Type | Abstraction | Subtyping procedure | |
---|---|---|---|
NST | NominalType | Trait | Inheritance |
SST | StructuralType | Structural Trait | (Implicit) |
記名型全体を表す型(NominalType
)と構造型全体の型(StructuralType
)は型全体の型(Type
)のサブタイプです。
Ergは型定義に引数(型引数)を渡すことができます。型引数を持つOption
, Array
などを多項カインドと呼びます。これら自体は型ではありませんが、引数を適用することで型となります。また、引数を持たないInt
, Str
型などを単純型(スカラー型)と呼びます。
型は集合とみなすことができ、包含関係も存在します。例えばNum
はAdd
やSub
などを含んでおり、Int
はNat
を含んでいます。
全てのクラスの上位クラスはObject == Class {:}
であり、全ての型の下位クラスはNever == Class {}
です。これについては後述します。
型
Array T
のような型は型T
を引数にとりArray T
型を返す、つまりType -> Type
型の関数とみなせます(型理論的にはカインドともいう)。Array T
のような型は、特に多相型(Polymorphic Type)と呼び、Array
そのものは1項カインドといいます。
引数、戻り値の型が判明している関数の型は(T, U) -> V
のように表記します。型が同じ2引数関数全体を指定したい場合は|T| (T, T) -> T
、N引数関数全体を指定したい場合、Func N
で指定できる。ただしFunc N
型は引数の数や型に関する情報がないので、呼び出すと戻り値はすべてObj
型になります。
Proc
型は() => Int
などのように表記します。また、Proc
型インスタンスの名前は最後に!
をつけなくてはなりません。
Method
型は第1引数に自身が属するオブジェクトself
を(参照として)指定する 関数/プロシージャです。依存型においては、メソッド適用後の自身の型も指定できます。これは T!(!N)
型でT!(N ~> N-1).() => Int
などのようにメソッドを指定できるということです。
Ergの配列(Array)はPythonでいうところのリストとなります。[Int; 3]
はInt
型オブジェクトが3つ入る配列クラスです。
Note:
(Type; N)
は型であり値でもあるので、このような使い方もできます。Types = (Int, Str, Bool) for! Types, T => print! T # Int Str Bool a: Types = (1, "aaa", True)
pop|T, N|(l: [T; N]): ([T; N-1], T) =
[*l, last] = l
(l, last)
lpop|T, N|(l: [T; N]): (T, [T; N-1]) =
[first, *l] = l
(first, l)
!
の付く型はオブジェクトの内部構造書き換えを許可する型です。例えば[T; !N]
クラスは動的配列となります。
T
型オブジェクトからT!
型オブジェクトを生成するには、単項演算子の!
を使います。
i: Int! = !1
i.update! i -> i + 1
assert i == 2
arr = [1, 2, 3]
arr.push! 4 # ImplError:
mut_arr = [1, 2, 3].into [Int; !3]
mut_arr.push! 4
assert mut_arr == [1, 2, 3, 4]
型定義
型は以下のように定義します。
Point2D = {.x = Int; .y = Int}
なお、i: Int
などのように.
を省略すると、型内で使われる非公開変数になります。しかしこれも要求属性です。
型もオブジェクトなので、型自体にも属性は存在します。このような属性を型属性といいます。クラスの場合はクラス属性ともいいます。
型クラス、データ型(に相当するもの)
先に述べたように、Ergにおける「型」とは大まかにはオブジェクトの集合を意味します。
以下は+
(中置演算子)を要求する Add
型の定義です。R, O
はいわゆる型引数で、Int
やStr
など実装のある型(クラス)が入れられます。他の言語で型引数には特別な記法(ジェネリクス、テンプレートなど)が与えられていますが、Ergでは通常の引数と同じように定義できます。
なお型引数は型オブジェクト以外も使用できます。例えば配列型[Int; 3]
はArray Int, 3
の糖衣文法です。型の実装がかぶる場合、ユーザは明示的に選択しなくてはなりません。
Add R = Trait {
.AddO = Type
.`_+_` = Self.(R) -> Self.AddO
}
._+_
は Add._+_
の省略形です。前置演算子の.+_
はNum
型のメソッドです。
Num = Add and Sub and Mul and Eq
NumImpl = Patch Num
NumImpl.
`+_`(self): Self = self
...
多相型は関数のように扱えます。Mul Int, Str
などのように指定して単相化します(多くの場合は指定しなくても実引数で推論されます)。
1 + 1
`_+_` 1, 1
Nat.`_+_` 1, 1
Int.`_+_` 1, 1
上の4行は同じ結果を返しますが(正確には、一番下はInt
を返します)、一番上を使うのが一般的です。
Ratio.`_+_`(1, 1)
とすると、エラーにはならず2.0
が返ります。
これは、Int <: Ratio
であるために1
がRatio
にダウンキャストされるからです。
しかしこれはキャストされません。
i = 1
if i: # TypeError: i: Int cannot cast to Bool, use Int.is_zero() instead.
log "a"
log "b"
これは、Bool < Int
であるためです(True == 1
, False == 0
)。サブタイプへのキャストは一般に検証が必要です。
型推論システム
Ergは静的ダックタイピングを採用しており、明示的に型を指定する必要は殆どありません。
f x, y = x + y
上のコードの場合、+
を持つ型、すなわちAdd
が自動的に推論されます。Ergはまず最小の型を推論します。f 0, 1
とすればf x: {0}, y: {1}
と推論され、n: Nat; f n, 1
の場合f x: Nat, y: {1}
と推論されます。最小化後は実装が見つかるまで型を大きくしていきます。{0}, {1}
の場合Nat
が+
の実装がある最小型なのでNat
に単相化されます。
{0}, {-1}
の場合はNat
にマッチしないのでInt
に単相化されます。部分型、上位型の関係にない場合は、濃度(インスタンス数)が低い(多相型の場合はさらに引数の少ない)方からトライされます。
{0}
と{1}
はInt
やNat
などの部分型となる列挙型です。
列挙型などには名前を付けて要求/実装メソッドを付けられます。その型にアクセスできる名前空間では、要求を満たすオブジェクトは実装メソッドを使用できます。
Binary = Patch {0, 1}
Binary.
# selfにはインスタンスが格納される。この例では0か1のどちらか。
# selfを書き換えたい場合、型名、メソッド名に`!`を付けなければならない。
is_zero(self) = match self:
0 -> True
1 -> False # _ -> Falseとしてもよい
is_one(self) = not self.is_zero()
to_bool(self) = match self:
0 -> False
1 -> True
以降は0.to_bool()
というコードが可能となります(もっとも0 as Bool == False
がビルトインで定義されていますが)。
コード中に示されたように、実際にself
を書き換える事のできる型の例を示します。
Binary! = Patch {0, 1}!
Binary!.
switch! ref! self = match! self:
0 => self = 1
1 => self = 0
b = !1
b.switch!()
print! b # => 0
構造型(無名型)
Binary = {0, 1}
上のコードでのBinary
は、0
および1
が要素の型です。0
と1
両方を持っているInt
型の部分型とも言えます。
{}
のようなオブジェクトはそれ自体が型であり、上のように変数に代入して使ってもよいし、代入せずに使うこともできます。
このような型を構造型といいます。クラス(記名型)と対比して後者としての使い方を強調したいときは無名型ともいいます。{0, 1}
のような種類の構造型は列挙型と呼ばれ、他に区間型、レコード型などがあります。
型同一性
下のような指定はできません。Add
はそれぞれ別のものを指すと解釈されるからです。
例えば、Int
とStr
はともにAdd
だが、Int
とStr
の加算はできません。
add l: Add, r: Add =
l + r # TypeError: there is no implementation of `_+_`: |T, U <: Add| (T, U) -> <Failure>
また、下のA
, B
は同じ型とはみなされません。しかし、型O
は一致するとみなされます。
... |R1; R2, O; A <: Add(R1, O); B <: Add(R2, O)|
型に関する基本的な文法
型指定
Ergでは以下のように:
の後に変数の型を指定します。これを型指定または型注釈と呼びます。
型指定は代入と同時に行うこともできます。
i: Int # これから使う変数iはInt型であると宣言する
i: Int = 1
j = 1 # 型指定は省略できる
単純な変数代入の場合、ほとんどの型指定は省略可能です。 型指定は単純な変数よりもサブルーチンや型の定義時に役立ちます。
# 引数の型指定
f x, y: Array Int = ...
T X, Y: Array Int = ...
上の場合、x, y
は共にArray Int
であることに注意して下さい。
# 大文字変数の値は定数式でなくてはならない
f X: Int = X
あるいは、型引数の情報が完全にいらない場合は_
で省略することもできます。
g v: [T; _] = ...
ただし、型指定の箇所で_
を指定するとそれはObject
を意味することに注意して下さい。
f x: _, y: Int = x + y # TypeError: + is not implemented between Object and Int
型表示
Ergでは変数だけでなく任意の式に対して明示的に型を表示することができます。この構文を型表示(type ascription)と呼びます。 変数の型表示=型指定です。
x = (1: Nat)
f("a": Str)
f("a"): Int
"a": Nat # TypeError:
部分型指定
Ergでは:
(型宣言演算子)による型と式の関係指定の他に、<:
(部分型宣言演算子)で型同士の関係を指定することもできます。
<:
の左辺はクラスしか指定できません。構造型同士の比較はSubtypeof
などを使用して下さい。
これも単純な変数の指定より、サブルーチンや型の定義時に使うことが多いです。
# 引数の部分型指定
f X <: T = ...
# 要求属性の部分型指定(.Iterator属性はIterator型のサブタイプであることを要求する)
Iterable T = Trait {
.Iterator = {Iterator} # == {I | I <: Iterator}
.iter = Self.() -> Self.Iterator T
...
}
また、クラス定義時に部分型指定を行うと、クラスが指定した型のサブタイプか静的に検査できます。
# クラスCはShowの部分型
C = Class Object, Impl=Show
C.show self = ... # Showの要求属性
特定の場合だけ部分型指定することもできます。
K T: Eq
K Int <: Show and Eq
K T = Class Object
K(T).
`==` self, other = ...
K(Int).
show self = ...
構造型を実装する際は、部分型指定を行うことを推奨します。 構造的部分型付けの特性から、要求属性の実装をする際にタイポや型指定の間違いがあってもエラーが出ないためです。
C = Class Object
C.shoe self = ... # TypoのせいでShowが実装できない(単なる固有のメソッドとみなされる)
属性定義
トレイトやクラスには、モジュール内でのみ属性を定義できます。
C = Class()
C.pub_attr = "this is public"
C::private_attr = "this is private"
c = C.new()
assert c.pub_attr == "this is public"
C.
かC::
のあとに改行してインデント以下にまとめて定義する文法を一括定義(batch definition)といいます。
C = Class()
C.pub1 = ...
C.pub2 = ...
C::priv1 = ...
C::priv2 = ...
# これは以下と同じ
C = Class()
C.
pub1 = ...
pub2 = ...
C::
priv1 = ...
priv2 = ...
エイリアシング
型には別名(エイリアス)を付けることができます。これにより、レコード型など長い型を短く表現できます。
Id = Int
Point3D = {x = Int; y = Int; z = Int}
IorS = Int or Str
Vector = Array Int
またエラー表示の際にも、コンパイラは複合型(上の例の場合、1番目以外の右辺型)にエイリアスが定義されている場合なるべくそれを使用するようになります。
ただし同じ型のエイリアスは1つのモジュールにつき1つまでで、複数のエイリアスがある場合warningが出ます。 これは、違う目的の型は別々の型として新しく定義するべき、ということです。 また、すでにエイリアスのある型に重ねてエイリアスを付けることを防ぐ目的もあります。
Id = Int
UserId = Int # TypeWarning: duplicate aliases: Id and UserId
Ids = Array Id
Ints = Array Int # TypeWarning: duplicate aliases: Isd and Ints
IorS = Int or Str
IorSorB = IorS or Bool
IorSorB_ = Int or Str or Bool # TypeWarning: duplicate aliases: IorSorB and IorSorB_
Point2D = {x = Int; y = Int}
Point3D = {...Point2D; z = Int}
Point = {x = Int; y = Int; z = Int} # TypeWarning: duplicate aliases: Point3D and Point
トレイト
トレイトは、レコード型に型属性の要求を追加した記名型です。 Pythonでいう抽象基底クラス(Abstract Base Class, ABC)に類似しますが、代数的演算を行えるという特徴があります。
トレイトは別々のクラスを同一視したい場合などに使います。標準で定義されているトレイトの例にはEq
やAdd
などがあります。
Eq
は==
を実装することを要求します。Add
は+
(中置)を実装することを要求します。
これらを実装したクラスは全てトレイトのサブタイプとして(部分的に)同一視できるわけです。
例として、ベクトルのノルム(長さ)を計算するNorm
トレイトを定義してみましょう。
Norm = Trait {.norm = (self: Self) -> Int}
トレイトは宣言ができるのみで実装を持てないことに注意してください。 トレイトは以下のようにしてクラスに「実装」することができます。
Point2D = Class {.x = Int; .y = Int}
Point2D|<: Norm|.
norm self = self.x**2 + self.y**2
Point3D = Class {.x = Int; .y = Int; .z = Int}
Point3D|<: Norm|.
norm self = self.x**2 + self.y**2 + self.z**2
Point2D
とPoint3D
は全く別の型ですが、Norm
を実装したので、.norm
メソッドを持つ型として同一視出来ます。
norm x: Norm = x.norm()
assert norm(Point2D.new({x = 1; y = 2})) == 5
assert norm(Point3D.new({x = 1; y = 2; z = 3})) == 14
トレイトの実装では、要求属性を実装していないとエラーになります。実装していても型が合わない場合はやはりエラーになります。
Point3D = Class {.x = Int; .y = Int; .z = Int}
Point3D|<: Norm|.
foo self = 1
トレイトのうま味の一つは、後述するPatchでメソッドを自動定義できるという点です。
@Attach NotEqual
Eq = Trait {.`==` = (self: Self, other: Self) -> Bool}
NotEq = Patch Eq
NotEq.
`!=` self, other = not self.`==` other
NotEq
パッチにより、Eq
を実装する全てのクラスは自動で!=
も実装することになります。
トレイト上の演算
トレイトは構造型と同じく合成、置換、排除などの演算を適用できます(e.g. T and U
)。このようにしてできたトレイトをインスタントトレイトと呼びます。
T = Trait {.x = Int}
U = Trait {.y = Int}
V = Trait {.x = Int; y: Int}
assert Structural(T and U) == Structural V
assert Structural(V not U) == Structural T
W = Trait {.x = Ratio}
assert Structural(W) != Structural(T)
assert Structural(W) == Structural(T.replace {.x = Ratio})
トレイトは型でもあるので、通常の型指定にも使えます。
points: [Norm; 2] = [Point2D::new(1, 2), Point2D::new(3, 4)]
assert points.iter().map(x -> x.norm()).collect(Array) == [5, 25]
トレイトの包摂
関数Subsume
によって、あるトレイトを上位型として含むトレイトを定義できます。これをトレイトの 包摂(Subsumption) と呼びます。
下の例でいうと、BinAddSub
はBinAdd
とBinSub
を包摂しています。
これはクラスにおける継承(Inheritance)に対応しますが、継承と違い複数の基底型をand
で合成して指定できます。not
によって一部を除外したトレイトでもOKです。
Add R = Trait {
.Output = Type
.`_+_` = (self: Self, R) -> Self.Output
}
ClosedAdd = Subsume Add(Self)
Sub R = Trait {
.Output = Type
.`_-_` = (self: Self, R) -> Self.Output
}
ClosedSub = Subsume Sub(Self)
ClosedAddSub = Subsume ClosedAdd and ClosedSub
構造的トレイト
トレイトは構造化できます。こうすると、明示的に実装を宣言する必要がなくなります。Pythonにおけるダックタイピングを実現する機能と言えます。
SAdd = Structural Trait {
.`_+_` = (self: Self, other: Self) -> Self
}
# |A <: SAdd|は省略できない
add|A <: SAdd| x, y: A = x.`_+_` y
C = Class {i = Int}
C.
new i = Self.__new__ {i;}
# C|<: Add(C)|で明示的に実装したわけでないことに注意
`_+_` self, other: Self = Self.new {i = self::i + other::i}
assert add(C.new(1), C.new(2)) == C.new(3)
通常のトレイト、すなわち記名的トレイトは単に要求メソッドを実装しただけでは使えず、実装したことを明示的に宣言する必要があります。
以下の例では明示的な実装の宣言がないため、add
がC
型の引数で使えません。C = Class {i = Int}, Impl := Add
としなくてはならないのです。
Add = Trait {
.`_+_` = (self: Self, other: Self) -> Self
}
# |A <: Add|は省略できる
add|A <: Add| x, y: A = x.`_+_` y
C = Class {i = Int}
C.
new i = Self.__new__ {i;}
`_+_` self, other: Self = Self.new {i = self::i + other::i}
add C.new(1), C.new(2) # TypeError: C is not subclass of Add
# hint: inherit or patch 'Add'
構造的トレイトはこの実装の宣言がなくてもよいのですが、そのかわり型推論が効きません。使う際は型指定が必要です。
依存トレイト
トレイトは引数を取ることができます。これは依存型と同じです。
Mapper T: Type = Trait {
.MapIter = {Iterator}
.map = (self: Self, T -> U) -> Self.MapIter U
}
# ArrayIterator <: Mapper
# ArrayIterator.MapIter == ArrayMapper
# [1, 2, 3].iter(): ArrayIterator Int
# [1, 2, 3].iter().map(x -> "{x}"): ArrayMapper Str
assert [1, 2, 3].iter().map(x -> "\{x}").collect(Array) == ["1", "2", "3"]
トレイトにおけるオーバーライド
派生トレイトでは基底トレイトの型定義をオーバーライドできます。 この場合、オーバーライドするメソッドの型は、基底メソッドの型の部分型でなければなりません。
# `Self.(R) -> O`は`Self.(R) -> O or Panic`の部分型
Div R, O: Type = Trait {
.`/` = Self.(R) -> O or Panic
}
SafeDiv R, O = Subsume Div, {
@Override
.`/` = Self.(R) -> O
}
APIの重複するトレイトの実装と解決
実際のAdd
, Sub
, Mul
の定義はこのようになっています。
Add R = Trait {
.Output = Type
.`_+_` = (Self, R) -> .Output
}
Sub R = Trait {
.Output = Type
.`_-_` = (Self, R) -> .Output
}
Mul R = Trait {
.Output = Type
.`*` = (Self, R) -> .Output
}
.Output
という変数の名前が重複しています。これら複数のトレイトを同時に実装したい場合、以下のように指定します。
P = Class {.x = Int; .y = Int}
# P|Self <: Add(P)|はP|<: Add(P)|に省略できる
P|Self <: Add(P)|.
Output = P
`_+_` self, other = P.new {.x = self.x + other.x; .y = self.y + other.y}
P|Self <: Mul(Int)|.
Output = P
`*` self, other = P.new {.x = self.x * other; .y = self.y * other}
このようにして実装した重複のあるAPIは、使用時は殆どの場合型推論されますが、||
で明示的に型指定することで解決もできます。
print! P.Output # TypeError: ambiguous type resolution
print! P|<: Mul(Int)|.Output # <class 'P'>
Appendix: Rustのトレイトとの違い
ErgのトレイトはSchärli et al.の提唱したトレイトに忠実です。 代数演算を行えるようにするためトレイトは実装を持てないようにして、必要ならばパッチをあてる設計にしています。
クラス
Ergにおけるクラスは、大まかには自身の要素(インスタンス)を生成できる型と言えます。 以下は単純なクラスの例です。
Person = Class {.name = Str; .age = Nat}
# .newが定義されなかった場合、自動で`Person.new = Person::__new__`となる
Person.
new name, age = Self::__new__ {.name = name; .age = age}
john = Person.new "John Smith", 25
print! john # <Person object>
print! classof(john) # Person
Class
に与えられる型を要件型(この場合は{.name = Str; .age = Nat}
)といいます。
インスタンスはクラス名::__new__ {属性名 = 値; ...}
で生成できます。
{.name = "John Smith"; .age = 25}
は単なるレコードですが、Person.new
を通すことでPerson
インスタンスに変換されるわけです。
このようなインスタンスを生成するサブルーチンはコンストラクタと呼ばれます。
上のクラスでは、フィールド名等を省略できるように.new
メソッドを定義しています。
以下のように改行せず定義すると文法エラーになるので注意してください。
Person.new name, age = ... # SyntaxError: cannot define attributes directly on an object
省略記法
レコードでない型T
に対しC = Class T
とすると、これはC = Class {base = T}
と同じ意味になります。これはいわゆるnew typeパターンの定義を簡略化するためのものです。
また、コンストラクタ__new__
/new
もレコードで包まずT
型オブジェクトを直接渡せます。
Id = Class {base = Int}
i = Id.new {base = 1}
# ↓
Id = Class Int
i = Id.new 1
インスタンス属性、クラス属性
Pythonやその他の言語では、以下のようにブロック側でインスタンス属性を定義することが多いが、このような書き方はErgでは別の意味になるので注意が必要である。
# Python
class Person:
name: str
age: int
# Ergでこの書き方はクラス属性の宣言を意味する(インスタンス属性ではない)
Person = Class()
Person.
name: Str
age: Int
# 上のPythonコードに対応するErgコード
Person = Class {
.name = Str
.age = Nat
}
要素属性(レコード内で定義した属性)と型属性(クラスの場合は特にインスタンス属性/クラス属性とも呼ばれる)は全くの別物である。型属性は型自体の持つ属性である。型の要素は、自らの中に目当ての属性がないときに型属性を参照する。要素属性は要素が直接持つ固有の属性である。 なぜこのような区分けがされているか。仮に全てが要素属性だと、オブジェクトを生成した際に全ての属性を複製・初期化する必要があり、非効率であるためである。 また、このように分けたほうが「この属性は共用」「この属性は別々に持つ」などの役割が明確になる。
下の例で説明する。species
という属性は全てのインスタンスで共通なので、クラス属性とした方が自然である。だがname
という属性は各インスタンスが個々に持っておくべきなのでインスタンス属性とすべきなのである。
Person = Class {name = Str}
Person::
species = "human"
Person.
describe() =
log "species: \{Person::species}"
greet self =
log "Hello, My name is \{self::name}."
Person.describe() # species: human
Person.greet() # TypeError: unbound method Person.greet needs an argument
john = Person.new {name = "John"}
john.describe() # species: human
john.greet() # Hello, My name is John.
alice = Person.new {name = "Alice"}
alice.describe() # species: human
alice.greet() # Hello, My name is Alice.
因みに、インスタンス属性と型属性で同名、同型のものが存在する場合、コンパイルエラーとなる。これは混乱を避けるためである。
C = Class {.i = Int}
C.
i = 1 # AttributeError: `.i` is already defined in instance fields
Class, Type
「1
の型は何であるか?」という問いに対しては、少し長い答えが必要になります。
1
の生成元である クラス はNat
ただひとつです。オブジェクトの属するクラスはclassof(obj)
またはobj.__class__
で取得できます。
対して1
の属する 型 は無数にあります。例としては、{1}, {0, 1}, 0..12, Nat, Int, Num
などです。
このようにオブジェクトが複数の型に属しうるのは、Ergが部分型システムを持つためです。Nat
はInt
の部分型であり、Int
はNum
の部分型です。
構造型との違い
クラスは自身の要素を生成することができる型といいましたが、それだけは厳密な説明ではありません。実際はレコード型+パッチでも同じことができるからです。
Person = {.name = Str; .age = Nat}
PersonImpl = Patch Person
PersonImpl.
new name, age = {.name; .age}
john = Person.new("John Smith", 25)
クラスを使用するメリットは4つあります。 1つはコンストラクタの正当性が検査されること、2つ目は型検査が簡便であること、3つ目は記名的部分型(NST)が使用できること、4つ目は継承・オーバーライドができることです。
先程レコード型+パッチでもコンストラクタ(のようなもの)が定義できることを見ましたが、これはもちろん正当なコンストラクタとは言えません。.new
と名乗っていても全く関係のないオブジェクトを返すことができるからです。クラスの場合は、.new
が要件を満たすオブジェクトを生成するか静的に検査されます。
~
クラスの型検査は、単にオブジェクトの.__class__
属性を見るだけで完了します。なので、オブジェクトが型に属しているかの検査が高速です。
~
ErgではクラスでNSTを実現します。NSTの利点として、堅牢性などが挙げられます。 大規模なプログラムを書いていると、オブジェクトの構造が偶然一致することはままあります。
Dog = {.name = Str; .age = Nat}
DogImpl = Patch Dog
DogImpl.
bark = log "Yelp!"
...
Person = {.name = Str; .age = Nat}
PersonImpl = Patch Person
PersonImpl.
greet self = log "Hello, my name is \{self.name}."
john = {.name = "John Smith"; .age = 20}
john.bark() # "Yelp!"
Dog
とPerson
の構造は全く同一ですが、動物が挨拶したり人間が吠えたりできるようにするのは明らかにナンセンスです。
後者はともかく、前者は不可能なので適用できないようにする方が安全です。このような場合はクラスを使用すると良いでしょう。
Dog = Class {.name = Str; .age = Nat}
Dog.
bark = log "Yelp!"
...
Person = Class {.name = Str; .age = Nat}
Person.
greet self = log "Hello, my name is \{self.name}."
john = Person.new {.name = "John Smith"; .age = 20}
john.bark() # TypeError: `Person` object has no method `.bark`
もう一つ、パッチによって追加された型属性は仮想的なもので、実装するクラスが実体として保持している訳ではないという特徴があります。
つまり、T.x
, T.bar
は{i = Int}
と互換性のある型がアクセスできる(コンパイル時に結びつける)オブジェクトであり、{i = Int}
やC
に定義されているわけではありません。
対してクラス属性はクラス自身が保持しています。なので、構造が同じであっても継承関係にないクラスからはアクセスできません。
C = Class {i = Int}
C.
foo self = ...
print! dir(C) # ["foo", ...]
T = Patch {i = Int}
T.
x = 1
bar self = ...
print! dir(T) # ["bar", "x", ...]
assert T.x == 1
assert {i = 1}.x == 1
print! T.bar # <function bar>
{i = Int}.bar # TypeError: Record({i = Int}) has no method `.bar`
C.bar # TypeError: C has no method `.bar`
print! {i = 1}.bar # <method bar>
print! C.new({i = 1}).bar # <method bar>
データクラスとの違い
クラスには、レコードを要求型とするClass
を通した通常のクラスと、レコードを継承(Inherit
)したデータクラスがあります。
データクラスはレコードの機能を受け継いでおり、分解代入ができる、==
やhash
がデフォルトで実装されているなどの特徴があります。
逆に独自の同値関係やフォーマット表示を定義したい場合は通常のクラスを使用するとよいでしょう。
C = Class {i = Int}
c = C.new {i = 1}
d = C.new {i = 2}
print! c # <C object>
c == d # TypeError: `==` is not implemented for `C`
D = Inherit {i = Int}
e = D::{i = 1} # e = D.new {i = 1}と同じ
f = D::{i = 2}
print! e # D(i = 1)
assert e != f
Enum Class
Or型のクラスを定義しやすくするために、Enum
が用意されています。
X = Class()
Y = Class()
XorY = Enum X, Y
それぞれの型にはXorY.X
, XorY.Y
のようにしてアクセスでき、コンストラクタはX.new |> XorY.new
のようにして取得できます。
x1 = XorY.new X.new()
x2 = (X.new |> XorY.new())()
x3 = (Y.new |> XorY.new())()
assert x1 == x2
assert x1 != x3
包含関係
クラスは、要件型のサブタイプです。要件型のメソッド(パッチメソッド含む)を使用できます。
T = Trait {.foo = Foo}
C = Class(..., Impl: T)
C.
foo = foo
bar x = ...
assert C < T
assert C.foo == foo
assert not T < C
assert T.foo == Foo
継承
継承を使うと、既存のクラスに機能を加えたり特化したりした新しいクラスを定義できます。 継承はトレイトにおける包摂に似ています。継承してできたクラスは、もとのクラスのサブタイプになります。
Inherit
は継承クラスを定義する関数です。左辺の型をサブクラス、右辺のInherit
の引数型をスーパークラスと言います。
NewInt = Inherit Int
NewInt.
plus1 self = self + 1
assert NewInt.new(1).plus1() == 2
assert NewInt.new(1) + NewInt.new(1) == 2
Pythonと違い、定義されたErgのクラスはデフォルトで継承不可(final)です。
新しく定義するクラスを継承可能なクラスにしたい場合は、Inheritable
デコレータを付与する必要があります。
オプション引数Additional
を指定すると追加のインスタンス属性を持つことができます。ただし値クラスの場合はインスタンス属性を追加できません。
@Inheritable
Person = Class {name = Str}
Student = Inherit Person, Additional := {id = Int}
john = Person.new {name = "John"}
alice = Student.new {name = "Alice", id = 123}
MailAddress = Inherit Str, Additional := {owner = Str} # TypeError: instance variables cannot be added to a value class
Ergでは例外的にNever
型の継承はできない設計となっている。Never
は決してインスタンスを生成できない特異なクラスであるためである。
列挙クラスの継承
Or型をクラス化した列挙クラスも継承ができます。この際、オプション引数Excluding
を指定することで選択肢のどれか(or
で複数選択可)を外せます。
なお追加はできません。選択肢を追加したクラスは、元のクラスのサブタイプとはならないからです。
Number = Class Int or Float or Complex
Number.
abs(self): Float =
match self:
i: Int -> i.abs().into Float
f: Float -> f.abs()
c: Complex -> c.abs().into Float
# matchの選択肢でc: Complexは現れ得ない
RealNumber = Inherit Number, Excluding: Complex
同様に、篩型も指定できます。
Months = Class 0..12
MonthsNot31Days = Inherit Months, Excluding: {1, 3, 5, 7, 8, 10, 12}
StrMoreThan3 = Class StrWithLen N | N >= 3
StrMoreThan4 = Inherit StrMoreThan3, Excluding: StrWithLen N | N == 3
オーバーライド
元の型に新しいメソッドを追加できるところはパッチと同じですが、クラスはさらに「上書き」が可能です。
この上書きをオーバーライド(override: 上書き)といいます。オーバーライドを行うには3つの条件を満たす必要があります。
まず、オーバーライドはデフォルトではエラーとなるためOverride
デコレータを付ける必要があります。
さらに、オーバーライドによってメソッドの型を変えることはできません。元の型のサブタイプである必要があります。
そして、他のメソッドから参照されているメソッドをオーバーライドする場合、参照しているメソッドも全てオーバーライドする必要があります。
なぜこのような条件が必要なのでしょうか。それは、オーバーライドが単に一つのメソッドの挙動を変えるだけでなく、別のメソッドの挙動に影響を及ぼす可能性があるからです。
まず、1つ目の条件から解説します。この条件は「不測のオーバーライド」を防ぐためです。
つまり、たまたま派生クラス側で新しく定義したつもりだったメソッドの名前が基底クラスとかちあってしまうといったことを防ぐため、Override
デコレータで明示する必要があるのです。
次に、2つ目の条件について考えます。これは型の整合性を保つためです。派生クラスは基底クラスのサブタイプであるため、その振る舞いも基底クラスのものと互換性がなくてはなりません。
最後に、3つ目の条件について考えます。この条件はErg特有で、他のオブジェクト指向言語ではあまり見られないものですが、これも安全のためです。これがなかったとき、どんなまずいことが起こりうるか見てみましょう。
# Bad example
@Inheritable
Base! = Class {x = Int!}
Base!.
f! ref! self =
print! self::x
self.g!()
g! ref! self = self::x.update! x -> x + 1
Inherited! = Inherit Base!
Inherited!.
@Override
g! ref! self = self.f!() # InfiniteRecursionWarning: This code falls into an infinite loop
# OverrideError: method `.g` is referenced by `.f` but not overridden
継承クラスInherited!
では、.g!
メソッドをオーバーライドして処理を.f!
に転送しています。しかし基底クラスの.f!
メソッドはその処理を.g!
に転送しているので、無限ループが発生してしまっています。.f
はBase!
クラスでは問題の無いメソッドでしたが、オーバーライドによって想定外の使われ方をされ、壊れてしまったのです。
なので、オーバーライドの影響を受ける可能性のあるメソッドは一般に全て書き直す必要があるわけです。Ergはこのルールを仕様に組み込んでいます。
# OK
@Inheritable
Base! = Class {x = Int!}
Base!.
f! ref! self =
print! self::x
self.g!()
g! ref! self = self::x.update! x -> x + 1
Inherited! = Inherit Base!
Inherited!.
@Override
f! ref! self =
print! self::x
self::x.update! x -> x + 1
@Override
g! ref! self = self.f!()
しかし、この仕様はオーバーライドの問題を完全に解決するものではありません。コンパイラはオーバーライドで問題が修正されたか検知できないためです。 オーバーライドによる影響の修正は派生クラスを作成するプログラマの責任です。可能な限り別名のメソッドを定義するようにしましょう。
トレイトの差し替え(のように見えるもの)
継承時にトレイトを差し替えることはできませんが、一見それを行っているようにみえる例があります。
例えばReal
(Add()
を実装する)のサブタイプであるInt
ではAdd()
を再実装しているようにみえます。
Int = Class ..., Impl := Add() and ...
しかし実際はReal
のAdd()
はAdd(Real, Real)
の略で、Int
ではAdd(Int, Int)
で上書きしているだけです。
両者は別のトレイトです(Add
は共変なのでAdd(Real, Real) :> Add(Int, Int)
ではありますが)。
多重継承の禁止
Ergでは通常のクラス同士でIntersection(交差), Diff(除外), Complement(否定)が行えません。
Int and Str # TypeError: cannot unite classes
このルールにより、複数のクラスを継承すること、すなわち多重継承が行えません。
IntAndStr = Inherit Int and Str # SyntaxError: multiple inheritance of classes is not allowed
ただし、Pythonの多重継承されたクラスは使用可能です。
多層(多段)継承の禁止
Ergの継承は多層継承も禁止しています。すなわち、継承して作ったクラスを更に継承したクラスを定義することはできません。
ただし、Object
を継承している(Inheritable)クラスは例外的に継承可能です。
また、この場合もPythonの多層継承されたクラスは使用可能です。
継承元属性の書き換え禁止
Ergでは継承元の属性を書き換えることができません。これは2つの意味があります。
1つ目は、継承元のクラス属性に対する更新操作です。再代入はもちろん、.update!
メソッドなどによる更新もできません。
オーバーライドはより特化したメソッドで上書きする操作であるため書き換えとは異なります。オーバーライドの際も互換性のある型で置き換えなくてはなりません。
@Inheritable
Base! = Class {.pub = !Int; pri = !Int}
Base!.
var = !1
inc_pub! ref! self = self.pub.update! p -> p + 1
Inherited! = Inherit Base!:
Inherited!.
var.update! v -> v + 1
# TypeError: can't update base class variables
@Override
inc_pub! ref! self = self.pub + 1
# OverrideError: `.inc_pub!` must be subtype of `Self!.() => ()`
2つ目は、継承元の(可変)インスタンス属性に対する更新操作です。これも禁止されています。基底クラスのインスタンス属性は、基底クラスの用意したメソッドからのみ更新できます。 属性の可視性にかかわらず、直接更新はできません。ただし読み取りはできます。
@Inheritable
Base! = Class {.pub = !Int; pri = !Int}
Base!.
inc_pub! ref! self = self.pub.update! p -> p + 1
inc_pri! ref! self = self::pri.update! p -> p + 1
Inherited! = Inherit Base!:
Inherited!.
# OK
add2_pub! ref! self =
self.inc_pub!()
self.inc_pub!()
# NG, `Child`は`self.pub`と`self::pri`に触れられない
add2_pub! ref! self =
self.pub.update! p -> p + 2
畢竟(ひっきょう)、Ergの継承ができることは新規属性の追加と基底クラスメソッドのオーバーライドのみといえるでしょう。
継承の使い所
継承は正しく使えば強力な機能である反面、クラス同士の依存関係が複雑になりやすいという欠点もあり、特に多重継承・多層継承を使用した場合はその傾向が顕著となります。依存関係の複雑化はコードのメンテナンス性を下げる恐れがあります。 Ergが多重継承、多層継承を禁止したのはこの危険性を低減するためで、クラスパッチという機能を導入したのは、継承の「機能の追加」という側面を持ちながら依存関係の煩雑化を抑えるためです。
では逆に継承を使うべきところはどこでしょうか。一つの指標は、「基底クラスの意味論的なサブタイプがほしい」場合です。
Ergはサブタイプ判定の一部を型システムが自動で判定してくれます(e.g. 0以上のIntであるところのNat)。
しかし例えば、「有効なメールアドレスを表す文字列型」をErgの型システムのみに頼って作成することは困難です。通常の文字列にバリデーションを行うべきでしょう。そして、バリデーションが通った文字列オブジェクトには何らかの「保証書」を付加したいところです。それが継承クラスへのダウンキャストに相当するわけです。Strオブジェクト
をValidMailAddressStr
にダウンキャストすることは、文字列が正しいメールアドレスの形式であるか検証することと一対一対応します。
ValidMailAddressStr = Inherit Str
ValidMailAddressStr.
init s: Str =
validate s # メールアドレスの有効性
Self.new s
s1 = "invalid mail address"
s2 = "foo@gmail.com"
_ = ValidMailAddressStr.init s1 # panic: invalid mail address
valid = ValidMailAddressStr.init s2
valid: ValidMailAddressStr # 正しいメールアドレス形式であることの確認
もう一つの指標は、「記名的な多相=多態を実現したい」場合です。
例えば、以下に定義するgreet!
プロシージャは、Named
型のオブジェクトならば何でも受け付けます。
しかし、明らかにDog
型オブジェクトを適用するのは間違えています。そこで引数の型をPerson
クラスにします。
こうすれば、引数として受け付けるのはPerson
オブジェクトとそれを継承したクラス、Student
オブジェクトのみです。
この方が保守的で、不必要に多くの責任を負う必要がなくなります。
Named = {name = Str; ...}
Dog = Class {name = Str; breed = Str}
Person = Class {name = Str}
Student = Inherit Person, additional: {id = Int}
structural_greet! person: Named =
print! "Hello, my name is \{person::name}."
greet! person: Person =
print! "Hello, my name is \{person::name}."
max = Dog.new {name = "Max", breed = "Labrador"}
john = Person.new {name = "John"}
alice = Student.new {name = "Alice", id = 123}
structural_greet! max # Hello, my name is Max.
structural_greet! john # Hello, my name is John.
greet! alice # Hello, my name is Alice.
greet! max # TypeError:
記名的部分型 vs. 構造的部分型
Months = 0..12
# NST
MonthsClass = Class Months
MonthsClass.
name self =
match self:
1 -> "January"
2 -> "February"
3 -> "March"
...
# SST
MonthsImpl = Patch Months
MonthsImpl.
name self =
match self:
1 -> "January"
2 -> "February"
3 -> "March"
...
assert 12 in Months
assert 2.name() == "February"
assert not 12 in MonthsClass
assert MonthsClass.new(12) in MonthsClass
# クラスでラップしても構造型は使える
assert MonthsClass.new(12) in Months
# 両方ある場合クラスメソッドが優先される
assert MonthsClass.new(2).name() == "february"
結局、NSTとSSTどちらを使えばいいのか?
どちらにすればよいか判断がつかないときはNSTを推奨します。 SSTはどんなユースケースでも破綻しないコードを書く抽象化能力が要求されます。よい抽象化を実現できれば高い生産性を発揮できますが、間違った抽象化(見かけによる共通化)を行うと逆効果となってしまいます。NSTは抽象性をあえて抑え、このリスクを減らすことができます。あなたがライブラリの実装者でないならば、NSTのみでコーディングを行っても悪くはないでしょう。
パッチ
Ergでは、既存の型・クラスに手を加えることはできません。 クラスにメソッドを追加で定義することはできず、特殊化(specialization, 多相に宣言された型を単相化し専用のメソッドを定義する機能。C++などが持つ)も行えません。 しかし、既存の型・クラスに機能を追加したいという状況は多々あり、これを実現するためにパッチという機能があります。
StrReverse = Patch Str
StrReverse.
reverse self = self.iter().rev().collect(Str)
assert "abc".reverse() == "cba"
パッチの名前は、主に追加する機能を端的に表すものがよいでしょう。
こうすると、パッチされる型(Str
)のオブジェクトはパッチ(StrReverse
)のメソッドを使えるようになります。
実際、.reverse
はStr
のメソッドではなく、StrRReverse
に追加されたメソッドです。
ただし、パッチのメソッドは記名型(クラス)のメソッドより優先度が低く、既存のクラスのメソッドをオーバーライド(上書き)できません。
StrangeInt = Patch Int
StrangeInt.
`_+_` = Int.`_-_` # AssignError: .`_+_` is already defined in Int
オーバーライドしたければ、クラスを継承する必要があります。 ただし、基本的にはオーバーライドを行わず、別の名前のメソッドを定義することを推奨します。 オーバーライドは安全のためいくつかの制約が課されており、それほど気軽に行えるものではないからです。
StrangeInt = Inherit Int
StrangeInt.
# オーバーライドするメソッドにはOverrideデコレータを付与する必要がある
# さらに、Int.`_+_`に依存するIntのメソッドすべてをオーバーライドする必要がある
@Override
`_+_` = Super.`_-_` # OverrideError: Int.`_+_` is referenced by ..., so these method must also be overridden
パッチの選択
パッチは一つの型に対して複数定義し、まとめることもできます。
# foo.er
StrReverse = Patch(Str)
StrReverse.
reverse self = ...
StrMultiReplace = Patch(Str)
StrMultiReverse.
multi_replace self, pattern_and_targets: [(Pattern, Str)] = ...
StrToCamelCase = Patch(Str)
StrToCamelCase.
to_camel_case self = ...
StrToKebabCase = Patch(Str)
StrToKebabCase.
to_kebab_case self = ...
StrBoosterPack = StrReverse and StrMultiReplace and StrToCamelCase and StrToKebabCase
{StrBoosterPack;} = import "foo"
assert "abc".reverse() == "cba"
assert "abc".multi_replace([("a", "A"), ("b", "B")]) == "ABc"
assert "to camel case".to_camel_case() == "toCamelCase"
assert "to kebab case".to_kebab_case() == "to-kebab-case"
複数のパッチが定義できると、中には実装の重複が発生する可能性があります。
# foo.er
StrReverse = Patch(Str)
StrReverse.
reverse self = ...
# より効率的な実装
StrReverseMk2 = Patch(Str)
StrReverseMk2.
reverse self = ...
"hello".reverse() # PatchSelectionError: multiple choices of `.reverse`: StrReverse, StrReverseMk2
そのような場合は、メソッド形式ではなく関連関数形式とすることで一意化できます。
assert StrReverseMk2.reverse("hello") == "olleh"
また、選択的にインポートすることでも一意化できます。
{StrReverseMk2;} = import "foo"
assert StrReverseMk2.reverse("hello") == "olleh"
接着パッチ(Glue Patch)
パッチは型同士を関係付けることもできます。StrReverse
はStr
とReverse
を関係付けています。
このようなパッチは接着パッチ(Glue Patch)と呼ばれます。
Str
は組み込みの型であるため、ユーザーがトレイトを後付けするためには接着パッチが必要なわけです。
Reverse = Trait {
.reverse = Self.() -> Self
}
StrReverse = Patch Str, Impl := Reverse
StrReverse.
reverse self =
self.iter().rev().collect(Str)
接着パッチは一つの型とトレイトのペアに対して一つまでしか定義できません。 これは、仮に複数の接着パッチが同時に「見える」場合、どの実装を選択するか一意に決められなくなるからです。 ただし、別のスコープ(モジュール)に移る際にパッチを入れ替えることはできます。
NumericStr = Inherit Str
NumericStr.
...
NumStrRev = Patch NumericStr, Impl := Reverse
NumStrRev.
...
# DuplicatePatchError: NumericStr is already associated with `Reverse`
# hint: `Str` (superclass of `NumericStr`) is associated with `Reverse` by `StrReverse`
Appendix: Rustのトレイトとの関連
ErgのパッチはRustの(後付けの)implブロックに相当します。
#![allow(unused)] fn main() { // Rust trait Reverse { fn reverse(self) -> Self; } impl Reverse for String { fn reverse(self) -> Self { self.chars().rev().collect() } } }
RustのトレイトはErgのトレイトとパッチの機能を併せ持つ機能だと言えるでしょう。こう言うとRustのトレイトの方が便利に聞こえますが、実はそうとも限りません。
# Erg
Reverse = Trait {
.reverse = Self.() -> Self
}
StrReverse = Patch(Str, Impl := Reverse)
StrReverse.
reverse self =
self.iter().rev().collect(Str)
Ergではimplブロックがパッチとしてオブジェクト化されているため、他のモジュールから取り込む際に選択的な取り込みが可能になります。さらに副次的な効果として、外部構造体への外部トレイトの実装も可能となっています。 また、dyn traitやimpl traitといった文法も構造型によって必要なくなります。
# Erg
reversible: [Reverse; 2] = [[1, 2, 3], "hello"]
iter|T|(i: Iterable T): Iterator T = i.iter()
#![allow(unused)] fn main() { // Rust let reversible: [Box<dyn Reverse>; 2] = [Box::new([1, 2, 3]), Box::new("hello")]; fn iter<I>(i: I) -> impl Iterator<Item = I::Item> where I: IntoIterator { i.into_iter() } }
全称パッチ
パッチはある特定の型ひとつだけではなく、「関数の型全般」などに対しても定義できます。
この場合、自由度を与えたい項を引数にします(下の場合はT: Type
)。このようにして定義したパッチを全称パッチといいます。
見れば分かる通り、全称パッチは正確にはパッチを返す関数ですが、それ自体もパッチとみなすことが可能です。
FnType T: Type = Patch(T -> T)
FnType(T).
type = T
assert (Int -> Int).type == Int
構造的パッチ
さらにパッチは、ある構造を満たす型すべてに定義することもできます。 ただしこれは記名的なパッチやクラスメソッドより優先度は低くなっています。
以下のように拡張によって成り立たなくなる性質もあるので、構造的パッチを定義する際は慎重に設計してください。
# これはStructuralにするべきではない
Norm = Structural Patch {x = Int; y = Int}
Norm.
norm self = self::x**2 + self::y**2
Point2D = Class {x = Int; y = Int}
assert Point2D.new({x = 1; y = 2}).norm() == 5
Point3D = Class {x = Int; y = Int; z = Int}
assert Point3D.new({x = 1; y = 2; z = 3}).norm() == 14 # AssertionError:
値型
値型はErg組み込み型のうちコンパイル時評価が可能な型で、具体的には以下のものです。
Value = (
Int
or Nat
or Ratio
or Float
or Complex
or Bool
or Str
or NoneType
or Array Const
or Tuple Const
or Set Const
or ConstFunc(Const, _)
or ConstProc(Const, _)
or ConstMethod(Const, _)
)
値型のオブジェクト・定数、およびそれにコンパイル時サブルーチンを適用したものを 定数式 と呼びます。
1, 1.0, 1+2im, True, None, "aaa", [1, 2, 3], Fib(12)
サブルーチンについては注意が必要です。サブルーチンは値型であるものとそうでないものがあります。 サブルーチンの実体は単なるポインタであるためすべて値として扱っても良い1のですが、コンパイル時サブルーチンでないものを定数文脈で使えてもあまり意味がないため、値型とはなっていません。
値型に分類される型は、将来的には追加される可能性があります。
1 Ergにおける値型という用語は、他の言語での定義とは異なっています。純粋なErgの意味論内でメモリという概念は存在せず、スタックに置かれるから値型であるとか、実体としてポインタだから値型ではない、といった言明は正しくありません。あくまで、値型はValue
型もしくはそのサブタイプであるという意味しか持ちません。↩
属性型
属性型は、レコードおよびデータクラス、パッチ、モジュールなどが含まれる型です。 属性型に属する型は値型ではありません。
レコード型の合成
合成されたレコード型は平坦化できます。例えば、{..::{.name = Str; .age = Nat}; ..::{.name = Str; .id = Nat}}
は{.name = Str; .age = Nat; .id = Nat}
となります。
区間型
Range
オブジェクトの最も基本的な使い方は、イテレータとしての使用です。
for! 0..9, i =>
print! i
Pythonと違い、末尾の数字は含まれることに注意してください。
しかし、Range
オブジェクトの使い道はこれだけではありません。型としても使うことが出来ます。このような型を区間型(Interval type)と呼びます。
i: 0..10 = 2
Nat
型は0..<Inf
と等価な型で、Int
とRatio
型は-Inf<..<Inf
と等価な型です。
0..<Inf
は0.._
と書くことも出来ます。_
は、Int
型の任意のインスタンスを意味します。
イテレータとしても使えるため、10..0
などのように逆順で指定することも出来ますが、
<..
, ..<
, <..<
の向きは逆転できません。
a = 0..10 # OK
b = 0..<10 # OK
c = 10..0 # OK
d = 10<..0 # Syntax error
e = 10..<0 # Syntax error
f = 10<..<0 # Syntax error
範囲演算子(range operator)は、Ord
な不変型であるならば数値以外の型にも使用できます。
Alphabet = "A".."z"
列挙型
列挙型(Enum type)はSetによって生成されます。 列挙型はそのままでも型指定で使えますが、クラス化したりパッチを定義することで更にメソッドを定義できます。 列挙型による部分型システムを列挙的部分型付けといいます。
Bool = {True, False}
Status = {"ok", "error"}
1..7
は{1, 2, 3, 4, 5, 6, 7}
と書き換えられるので、要素が有限の場合は本質的に列挙型と区間型は等価です。
Binary! = Class {0, 1}!.
invert! ref! self =
if! self == 0:
do!:
self.set! 1
do!:
self.set! 0
b = Binary!.new !0
b.invert!()
因みに、Ergの列挙型は他言語でよくある列挙型を包摂する概念です。
#![allow(unused)] fn main() { // Rust enum Status { Ok, Error } }
# Erg
Status = {"Ok", "Error"}
Rustとの相違点は、構造的部分型(SST)を採用しているというところにあります。
#![allow(unused)] fn main() { // StatusとExtraStatusの間には何も関係がない enum Status { Ok, Error } enum ExtraStatus { Ok, Error, Unknown } // メソッドを実装できる impl Status { // ... } impl ExtraStatus { // ... } }
# Status > ExtraStatusであり、Statusの要素はExtraStatusのメソッドを使える
Status = Trait {"Ok", "Error"}
# ...
ExtraStatus = Trait {"Ok", "Error", "Unknown"}
# ...
patchingによってメソッドの追加もできます。
明示的に包含関係を示したい場合、または既存のEnum型に選択肢を追加したい場合はor
演算子を使います。
ExtraStatus = Status or {"Unknown"}
要素の属するクラスがすべて同一である列挙型を等質(homogenous)な列挙型といいます。 デフォルトでは、等質な列挙型を要件型とするクラスは、要素が属しているクラスのサブクラスとして扱えます。 あえてそうしたくない場合は、ラッパークラスとするとよいでしょう。
Abc = Class {"A", "B", "C"}
Abc.new("A").is_uppercase()
OpaqueAbc = Class {inner = {"A", "B", "C"}}.
new inner: {"A", "B", "C"} = Self.new {inner;}
OpaqueAbc.new("A").is_uppercase() # TypeError
篩型
Refinement type(篩型、ふるいがた)は、述語式によって制約付けられた型です。列挙型や区間型は篩型の一種です。
篩型の標準形は{Elem: Type | (Pred)*}
です。これは、述語式Pred
を満たすElem
を要素とする型である、という意味です。
Type
に使えるのはValue型のみです。
Nat = 0.._
Odd = {N: Int | N % 2 == 1}
Char = StrWithLen 1
# StrWithLen 1 == {_: StrWithLen N | N == 1}
[Int; 3] == {_: Array Int, N | N == 3}
Array3OrMore == {A: Array _, N | N >= 3}
複数のPredがあるとき、;
かand
, or
で区切れます。;
とand
は同じ意味です。
Odd
の要素は1, 3, 5, 7, 9, ...
です。
篩にかけるように既存の型の一部を要素とする型になることから篩型と呼ばれます。
Pred
は(左辺)述語式と呼ばれます。これは代入式と同じく意味のある値を返すものではなく、左辺にはパターンしか置けません。
すなわち、X**2 - 5X + 6 == 0
のような式は篩型の述語式としては使えません。この点において、右辺式の述語式とは異なります。
{X: Int | X**2 - 5X + 6 == 0} # SyntaxError: the predicate form is invalid. Only names can be on the left-hand side
あなたが二次方程式の解法を知っているならば、上の篩型は{2, 3}
と同等になるだろうと予想できるはずです。
しかしErgコンパイラは代数学の知識をほとんど持ち合わせていないので、右の述語式を解決できないのです。
篩型の部分型付け規則
全ての篩型は、Type
部で指定された型の部分型です。
{I: Int | I <= 0} <: Int
その他、現在のErgは整数の比較に関する部分型規則を持っています。
{I: Int | I <= 5} <: {I: Int | I <= 0}
スマートキャスト
Odd
を定義したのはいいですが、このままではリテラル以外ではあまり使えないようにみえます。通常のInt
オブジェクトの中の奇数をOdd
に昇格させる、つまりInt
をOdd
にダウンキャストするためには、Odd
のコンストラクタを通す必要があります。
篩型の場合、通常のコンストラクタ.new
はパニックする可能性があり、.try_new
というResult
型を返す補助的なコンストラクタもあります。
i = Odd.new (0..10).sample!() # i: Odd (or Panic)
また、match
中で型指定として使用することもできます。
# i: 0..10
i = (0..10).sample!()
match i:
o: Odd ->
log "i: Odd"
n: Nat -> # 0..10 < Nat
log "i: Nat"
ただし、Ergは現在のところOdd
でなかったからEven
、などといった副次的な判断はできません。
列挙型、区間型と篩型
今まで紹介した列挙型と区間型は、篩型の糖衣構文です。
{a, b, ...}
は{I: Typeof(a) | I == a or I == b or ... }
に、a..b
は{I: Typeof(a) | I >= a and I <= b}
に脱糖されます。
{1, 2} == {I: Int | I == 1 or I == 2}
1..10 == {I: Int | I >= 1 and I <= 10}
1..<10 == {I: Int | I >= 1 and I < 10} == {I: Int | I >= 1 and I <= 9}
篩パターン
_: {X}
をX
と書き換えられるように(定数パターン)、_: {X: T | Pred}
はX: T | Pred
と書き換えることができます。
# メソッド.mは長さ3以上の配列に定義される
Array(T, N | N >= 3)
.m(ref self) = ...
代数演算型
代数演算型は、型を代数のようにみなして演算することで生成される型のことです。 代数演算型が扱う演算は、Union, Intersection, Diff, Complementなどがあります。 通常のクラスはUnionのみが行えて、他の演算は型エラーになります。
合併型
Union型では型について複数の可能性を与える事ができる。名前の通り、or
演算子で生成されます。
代表的なUnionはOption
型です。Option
型はT or NoneType
のpatch typeで、主に失敗するかもしれない値を表現します。
IntOrStr = Int or Str
assert dict.get("some key") in (Int or NoneType)
Option T = T or NoneType
Union型は可換ですが結合的ではないことに注意してください。すなわち、X or Y or Z
は(X or Y) or Z
であってX or (Y or Z)
とはなりません。
これを認めると、例えばInt or Option(Str)
とOption(Int) or Str
とOption(Int or Str)
が同じ型になってしまいます。
交差型
Intersection型は型同士をand
演算で結合して得られます。
Num = Add and Sub and Mul and Eq
先述したように通常のクラス同士ではand
演算で結合できません。インスタンスは唯一つのクラスに属するからです。
除外型
Diff型はnot
演算で得られます。
英文に近い表記としてはand not
とした方が良いですが、and
, or
と並べて収まりが良いのでnot
だけで使うのが推奨されます。
CompleteNum = Add and Sub and Mul and Div and Eq and Ord
Num = CompleteNum not Div not Ord
True = Bool not {False}
OneTwoThree = {1, 2, 3, 4, 5, 6} - {4, 5, 6, 7, 8, 9, 10}
否定型
Complement型はnot
演算で得られますが、これは単項演算です。not T
型は{=} not T
の短縮記法です。
not T
型によるIntersectionはDiffと同等で、not T
型によるDiffはIntersectionと同等です。
しかしこのような書き方は推奨されません。
# 最も単純な非ゼロ数型の定義
NonZero = Not {0}
# 非推奨のスタイル
{True} == Bool and not {False} # 1 == 2 + - 1
Bool == {True} not not {False} # 2 == 1 - -1
真の代数演算型
代数演算型には、簡約可能な見かけ上の代数演算型とそれ以上簡約できない「真の代数演算型」があります。
そうではない「見かけの代数型」には、Enum型やInterval型、レコード型のor
やand
があります。
これらは簡約が可能なので真の代数演算型ではなく、型指定に使うとWarningが出ます。Warningを消すためには簡約化するか型定義を行うかする必要があります。
assert {1, 2, 3} or {2, 3} == {1, 2, 3}
assert {1, 2, 3} and {2, 3} == {2, 3}
assert -2..-1 or 1..2 == {-2, -1, 1, 2}
i: {1, 2} or {3, 4} = 1 # TypeWarning: {1, 2} or {3, 4} can be simplified to {1, 2, 3, 4}
p: {x = Int, ...} and {y = Int; ...} = {x = 1; y = 2; z = 3}
# TypeWaring: {x = Int, ...} and {y = Int; ...} can be simplified to {x = Int; y = Int; ...}
Point1D = {x = Int; ...}
Point2D = Point1D and {y = Int; ...} # == {x = Int; y = Int; ...}
q: Point2D = {x = 1; y = 2; z = 3}
真の代数演算型には、Or
型、And
型があります。クラス同士のor
などはOr
型です。
assert Int or Str == Or(Int, Str)
assert Int and Marker == And(Int, Marker)
Diff, Complement型は必ず簡約できるので真の代数演算型ではありません。
依存型
依存型はErgの最大の特徴とも言っても良い機能です。 依存型とは、値を引数に取る型です。通常の多相型は型のみを引数に取れますが、その制限を緩めたのが依存型といえます。
依存型は、[T; N]
(Array(T, N)
)などがそれに相当します。
この型は、中身の型T
だけでなく、中身の個数N
にも依存して決まる型です。N
にはNat
型のオブジェクトが入ります。
a1 = [1, 2, 3]
assert a1 in [Nat; 3]
a2 = [4, 5, 6, 7]
assert a1 in [Nat; 4]
assert a1 + a2 in [Nat; 7]
関数引数で渡した型オブジェクトが戻り値型に関連する場合は、以下のように記述します。
narray: |N: Nat| {N} -> [{N}; N]
narray(N: Nat): [N; N] = [N; N]
assert narray(3) == [3, 3, 3]
依存型を定義する際は、型引数が全て定数でなくてはなりません。
依存型そのものは既存の言語にも存在するものですが、Ergでは依存型にプロシージャルメソッドを定義できるという特徴があります。
x = 1
f x =
print! f::x, module::x
# Phantom型は型引数と同じ値になるPhantomという属性を持つ
T X: Int = Class Impl := Phantom X
T(X).
x self = self::Phantom
T(1).x() # 1
可変依存型の型引数はメソッドの適用によって遷移させることができます。
遷移指定は~>
で行います。
# `Id`は不変型なので遷移させることはできないことに注意する
VM!(State: {"stopped", "running"}! := _, Id: Nat := _) = Class(..., Impl := Phantom! State)
VM!().
# 変わらない変数は`_`を渡せば省略可能, デフォルト引数にしておけば書く必要すらない
start! ref! self("stopped" ~> "running") =
self.initialize_something!()
self::set_phantom!("running")
# 型引数ごとに切り出すこともできる(定義されたモジュール内でのみ)
VM!.new() = VM!(!"stopped", 1).new()
VM!("running" ~> "running").stop! ref! self =
self.close_something!()
self::set_phantom!("stopped")
vm = VM!.new()
vm.start!()
vm.stop!()
vm.stop!() # TypeError: VM!(!"stopped", 1) doesn't have .stop!()
# hint: VM!(!"running", 1) has .stop!()
既存の型を組み込んだり継承して依存型を作ることもできます。
MyArray(T, N) = Inherit [T; N]
# .arrayと連動してself: Self(T, N)の型が変わる
MyStruct!(T, N: Nat!) = Class {.array: [T; !N]}
実体指定
動的配列arr: [T; !N]
について、処理を進めていくうちにN
の情報が失われてしまったとします。
この情報はassert arr.__len__() == X
とすることで回復させることができます。
arr: [Int; !_]
assert arr.__len__() == 3
arr: [Int; !3]
これは型パラメータの 実体指定 によって可能となっています。配列型Array(T, N)
は以下のように定義されています。
Array T <-> Union Self.map(x -> Typeof x), N <-> Self.__len__() = ...
<->
は依存型のパラメータのみで使える特別な記号で、そのパラメータに対する実体を指示します。実体であるところの右辺式は、コンパイル時に計算可能でなくても構いません。コンパイル時情報であるN
と実行時情報であるSelf.__len__()
が実体指定を通してリンクされる訳です。
実体指定に沿った方法でassertionが行われると、型パラメータの情報が復活します。すなわち、assert arr.__len__() == N
とするとN
の情報が復活します。ただしこの場合のN
はコンパイル時計算可能でなくてはなりません。
実体指定はassert
以外にmatch
でも活用されます。
arr: [Obj; _]
match! arr:
pair: [Obj; 2] => ...
ints: [Int; _] => ...
_ => ...
型変数、量化型
型変数はサブルーチン引数の型指定などに使用する変数で、その型が任意である(単相化しない)ことを示します。
まず、型変数を導入するモチベーションとして、入力をそのまま返すid
関数について考えましょう。
id x: Int = x
入力をそのまま返すid
関数がInt
型に対して定義されていますが、この関数は明らかに任意の型に対して定義できます。
最大のクラスを表すObject
を使用してみましょう。
id x: Object = x
i = id 1
s = id "foo"
b = id True
確かに任意の型を受け付けるようになりましたが、1つ問題があります。戻り値の型がObject
に拡大されてしまうのです。
入力がInt
型ならInt
型、Str
型ならStr
型が返るようになっていてほしいですね。
print! id 1 # <Object object>
id(1) + 1 # TypeError: cannot add `Object` and `Int`
入力の型と戻り値の型が同じであるようにするには、 型変数 を使います。
型変数は||
(型変数リスト)中で宣言します。
id|T: Type| x: T = x
assert id(1) == 1
assert id("foo") == "foo"
assert id(True) == True
これを関数の 全称量化(全称化) と呼びます。細かい違いはありますが、他言語でジェネリクスと呼ばれる機能に相当します。そして全称量化された関数を 多相関数 と呼びます。 多相関数の定義は、全ての型に対して同じ形の関数を定義するようなものです(Ergはオーバーロードを禁止しているので、下のコードは実際には書けません)。
id|T: Type| x: T = x
# 疑似コード
# ==
id x: Int = x
id x: Str = x
id x: Bool = x
id x: Ratio = x
id x: NoneType = x
...
また、型変数T
は型指定で使用されているため、Type
型と推論できます。なので、|T: Type|
は単に|T|
に省略できます。
また、|T, N| foo: [T; N]
など型オブジェクト以外の場合でも推論できる(T: Type, N: Nat
)ならば省略できます。
また、任意の型では大きすぎる場合、制約を与えることも出来ます。 制約を与えることにはメリットもあり、例えばサブタイプ指定をすると、特定のメソッドを使えるようになります。
# T <: Add
# => TはAddのサブクラス
# => 加算ができる
add|T <: Add| l: T, r: T = l + r
この例では、T
はAdd
型のサブクラスであると要求され、実際に代入されるl
とr
の型は同じでなくてはなりません。
この場合、T
を満たすのはInt
やRatio
などです。Int
とStr
の加算などは定義されていないので弾かれるわけです。
このような型付けもできます。
f|
Y, Z: Type
X <: Add Y, O1
O1 <: Add Z, O2
O2 <: Add X, _
| x: X, y: Y, z: Z =
x + y + z + x
注釈リストが長くなる場合は、事前宣言するとよいでしょう。
f: |Y, Z: Type, X <: Add(Y, O1), O1 <: Add(Z, O2), O2 <: Add(X, O3)| (X, Y, Z) -> O3
f|X, Y, Z| x: X, y: Y, z: Z =
x + y + z + x
ジェネリクスを持つ多くの言語と違い、宣言した型変数はすべて、仮引数リスト内(x: X, y: Y, z: Z
の部分)か他の型変数の引数内かで使用されていなければなりません。
これは、型変数はすべて実引数から推論可能であるというErgの言語設計からの要求です。
なので、戻り値の型など推論ができない情報は、実引数から渡します。Ergは型を実引数から渡すことができるのです。
Iterator T = Trait {
# 戻り値の型を引数から渡している
# .collect: |K: Type -> Type| Self(T).({K}) -> K(T)
.collect(self, K: Type -> Type): K(T) = ...
...
}
it = [1, 2, 3].iter().map i -> i + 1
it.collect(Array) # [2, 3, 4]
型変数が宣言できるのは||
の間のみである。ただし、宣言した後はスコープを抜けるまで任意の場所で使用できる。
f|X|(x: X): () =
y: X = x.clone()
log X.__name__
log X
f 1
# Int
# <class Int>
以下のようにして、使用時に明示的に単相化もできます。
f: Int -> Int = id|Int|
その場合、実引数の型よりも指定された型の方が優先されます(合致していないと実引数の型が間違っているという型エラーになる)。 すなわち、実際に渡されたオブジェクトが指定された型に変換可能ならば変換され、そうでなければコンパイルエラーとなります。
assert id(1) == 1
assert id|Int|(1) in Int
assert id|Ratio|(1) in Ratio
# キーワード引数も使える
assert id|T: Int|(1) == 1
id|Int|("str") # TypeError: id|Int| is type `Int -> Int` but got Str
この文法が内包表記とバッティングする際は()
で囲む必要があります。
# {id|Int| x | x <- 1..10}だと{id | ...}だと解釈される
{(id|Int| x) | x <- 1..10}
既に存在する型と同名の型変数は宣言出来ません。これは、型変数がすべて定数であるためです。
I: Type
# ↓ invalid type variable, already exists
f|I: Type| ... = ...
メソッド定義における型引数
左辺における型引数はデフォルトで束縛型変数として扱われます。
K(T: Type, N: Nat) = ...
K(T, N).
foo(x) = ...
別の型変数名を使用すると警告が出ます。
K(T: Type, N: Nat) = ...
K(U, M). # Warning: K's type variable names are 'T' and 'N'
foo(x) = ...
定数は定義以降すべての名前空間で同一なので、当然型変数名にも使用できません。
N = 1
K(N: Nat) = ... # NameError: N is already defined
L(M: Nat) = ...
# M == N == 1のときのみ定義される
L(N).
foo(self, x) = ...
# 任意のM: Natに対して定義される
L(M).
.bar(self, x) = ...
型引数ごとに多重定義することはできませんが、型引数を代入していない依存型(非原始カインド)と代入した依存型(原始カインド)は関係がないので同名のメソッドを定義できます。
K(I: Int) = ...
K.
# Kは真の型(原始カインド)ではないので、メソッドを定義できない
# これはメソッドではない(スタティックメソッドに近い)
foo(x) = ...
K(0).
foo(self, x): Nat = ...
全称型
前章で定義したid
関数は任意の型になれる関数です。では、「id
関数自体の型」は何なのでしょうか?
print! classof(id) # |T: Type| T -> T
|T: Type| T -> T
という型が得られました。これは 閉じた全称量化型/全称型(closed universal quantified type/universal type) と呼ばれるもので、MLでは['a. ...]
、Haskellではforall t. ...
という形式で提供される型に相当します。なぜ「閉じた」という形容詞がつくのかは後述します。
閉じた全称型には制約があり、全称化できる、すなわち左の節に置けるのはサブルーチン型のみです。しかしこれで十分です。Ergではサブルーチンがもっとも基本的な制御構造ですから、「任意のXを扱いたい」というとき、すなわち「任意のXを扱えるサブルーチンがほしい」という意味になります。なので、全称型は多相関数型と同じ意味になります。以降は基本的に、この種の型を多相関数型と呼ぶことにします。
無名関数と同じく、多相関数型には型変数名の任意性がありますが、これらはすべて同値となります。
assert (|T: Type| T -> T) == (|U: Type| U -> U)
ラムダ計算でいうところのα同値であるときに等号が成立します。型上の演算にはいくつかの制約があるので、同値性の判定は(停止性を考えなければ)常に可能です。
多相関数型の部分型付け
多相関数型は、任意の関数型になれます。これは、任意の関数型と部分型関係があるということです。この関係について詳しくみていきましょう。
OpenFn T: Type = T -> T
のような「型変数が左辺で定義され、右辺で使用されている型」を 開いた全称型(open universal type) と呼びます。
対してClosedFn = |T: Type| T -> T
など「型変数が右辺で定義・使用されている型」を 閉じた全称型(closed universal type) と呼びます。
開いた全称型は、同形な全ての「真の型」のスーパータイプになります。対して、閉じた全称型は、同形な全ての「真の型」のサブタイプになります。
(|T: Type| T -> T) < (Int -> Int) < (T -> T)
閉じている方が小さい/開いている方が大きい、と覚えるとよいでしょう。 しかし、どうしてそうなるのでしょうか。理解を深めるため、それぞれのインスタンスを考えてみます。
# id: |T: Type| T -> T
id|T|(x: T): T = x
# iid: Int -> Int
iid(x: Int): Int = x
# 任意の関数をそのまま返す
id_arbitrary_fn|T|(f1: T -> T): (T -> T) = f
# id_arbitrary_fn(id) == id
# id_arbitrary_fn(iid) == iid
# 多相関数をそのまま返す
id_poly_fn(f2: (|T| T -> T)): (|T| T -> T) = f
# id_poly_fn(id) == id
id_poly_fn(iid) # TypeError
# Int型関数をそのまま返す
id_int_fn(f3: Int -> Int): (Int -> Int) = f
# id_int_fn(id) == id|Int|
# id_int_fn(iid) == iid
|T: Type| T -> T
型であるid
はInt -> Int
型のパラメータf3
に代入できているため、(|T| T -> T) < (Int -> Int)
と考えることができそうです。
その逆、Int -> Int
型であるiid
は(|T| T -> T)
型のパラメータf2
に代入できていませんが、T -> T
型のパラメータf1
に代入できているため、(Int -> Int) < (T -> T)
です。
よって、確かに(|T| T -> T) < (Int -> Int) < (T -> T)
となっています。
全称型と依存型
依存型と全称型(多相関数型)はどんな関係があり、何が違うのでしょうか。 依存型は引数を取る型であり、全称型は(全称化するサブルーチンの)引数に任意性を与える型だと言えます。
重要なのは、閉じた全称型自体には型引数が存在しないというところです。例えば、多相関数型|T| T -> T
は多相関数 だけ を取る型であり、その定義は閉じています。その型引数T
を使ったメソッド等の定義はできません。
Ergでは型自体も値であるため、引数を取る型、例えば関数型なども須らく依存型になります。つまり、多相関数型は全称型でかつ依存型でもあるといえます。
PolyFn = Patch(|T| T -> T)
PolyFn.
type self = T # NameError: cannot find 'T'
DepFn T = Patch(T -> T)
DepFn.
type self =
log "by DepFn"
T
assert (Int -> Int).type() == Int # by DepFn
assert DepFn(Int).type() == Int # by DepFn
部分型付け
Ergでは、クラス同士の包含関係は比較演算子<
, >
で判定可能です。
Nat < Int
Int < Object
1.._ < Nat
{1, 2} > {1}
{=} > {x = Int}
{I: Int | I >= 1} < {I: Int | I >= 0}
<:
演算子とは別の意味を持つことに注意してください。左辺のクラスが右辺の型のサブタイプであると宣言するもので、コンパイル時にのみ意味を持ちます。
C <: T # T: StructuralType
f|D <: E| ...
assert F < G
また、多相型の部分型指定について、例えばSelf(R, O) <: Add(R, O)
などの場合、Self <: Add
と指定することもできます。
構造型、クラスの型関係
構造型は構造的型付けを実現するための型であり、構造が同じならば同じオブジェクトとみなされます。
T = Structural {i = Int}
U = Structural {i = Int}
assert T == U
t: T = {i = 1}
assert t in T
assert t in U
対してクラスは記名的型付けを実現するための型であり、型およびインスタンスを構造的に比較することができません。
C = Class {i = Int}
D = Class {i = Int}
assert C == D # TypeError: cannot compare classes
c = C.new {i = 1}
assert c in C
assert not c in D
サブルーチンの部分型付け
サブルーチンの引数、戻り値は、単一のクラスのみを取る。 すなわち、構造型やトレイトを関数の型として直接指定することはできない。 部分型指定を使って「その型のサブタイプである単一のクラス」として指定する必要がある。
# OK
f1 x, y: Int = x + y
# NG
f2 x, y: Add = x + y
# OK
# Aは何らかの具体的なクラス
f3<A <: Add> x, y: A = x + y
サブルーチンの型推論もこのルールに従っている。サブルーチン中の変数で型が明示されていないものがあったとき、コンパイラはまずその変数がいずれかのクラスのインスタンスでないかチェックし、そうでない場合はスコープ中のトレイトの中から適合するものを探す。それでも見つからない場合、コンパイルエラーとなる。このエラーは構造型を使用することで解消できるが、無名型を推論するのはプログラマの意図しない結果である可能性があるため、プログラマが明示的にStructural
で指定する設計となっている。
クラスのアップキャスト
i: Int
i as (Int or Str)
i as (1..10)
i as {I: Int | I >= 0}
キャスト
アップキャスト
Pythonはダックタイピングを採用する言語のため、キャストという概念はありません。アップキャストはする必要がなく、ダウンキャストも基本的にはありません。
しかしErgは静的に型付けされるため、キャストを行わなければいけない場合があります。
簡単な例では、1 + 2.0
が挙げられます。Ergの言語仕様上は+
(Int, Ratio)、すなわちInt(<: Add(Ratio, Ratio))の演算は定義されていません。というのも、Int <: Ratio
であるため、1はRatioのインスタンスである1.0にアップキャストされるからです。
Erg拡張バイトコードはBINARY_ADDに型情報を加えますが、この際の型情報はRatio-Ratioとなります。この場合はBINARY_ADD命令がIntのキャストを行うため、キャストを指定する特別な命令は挿入されません。なので、例えば子クラスでメソッドをオーバーライドしても、親を型に指定すれば型強制(type coercion)が行われ、親のメソッドで実行されます(コンパイル時に親のメソッドを参照するように名前修飾が行われます)。コンパイラが行うのは型強制の妥当性検証と名前修飾のみです。ランタイムがオブジェクトをキャストすることはありません(現在のところ。実行最適化のためにキャスト命令が実装される可能性はあります)。
@Inheritable
Parent = Class()
Parent.
greet!() = print! "Hello from Parent"
Child = Inherit Parent
Child.
# オーバーライドする際にはOverrideデコレータが必要
@Override
greet!() = print! "Hello from Child"
greet! p: Parent = p.greet!()
parent = Parent.new()
child = Child.new()
greet! parent # "Hello from Parent"
greet! child # "Hello from Parent"
この挙動はPythonとの非互換性を生むことはありません。そもそもPythonでは変数に型が指定されないので、いわば全ての変数が型変数で型付けされている状態となります。型変数は適合する最小の型を選ぶので、Ergで型を指定しなければPythonと同じ挙動が達成されます。
@Inheritable
Parent = Class()
Parent.
greet!() = print! "Hello from Parent"
Child = Inherit Parent
Child.
greet!() = print! "Hello from Child"
greet! some = some.greet!()
parent = Parent.new()
child = Child.new()
greet! parent # "Hello from Parent"
greet! child # "Hello from Child"
継承関係にある型同士では.from
, .into
が自動実装されるので、それを使うこともできます。
assert 1 == 1.0
assert Ratio.from(1) == 1.0
assert 1.into<Ratio>() == 1.0
ダウンキャスト
ダウンキャストは一般に安全ではなく、変換方法も自明ではないため、代わりにTryFrom.try_from
の実装で実現します。
IntTryFromFloat = Patch Int
IntTryFromFloat.
try_from r: Float =
if r.ceil() == r:
then: r.ceil()
else: Error "conversion failed"
可変型
Warning: この項の情報は古く、一部に間違いを含みます。
Ergではデフォルトですべての型が不変型、すなわち内部状態を更新できないようになっています。
しかし可変な型ももちろん定義できます。可変型は!
を付けて宣言します。
Person! = Class({name = Str; age = Nat!})
Person!.
greet! ref! self = print! "Hello, my name is \{self::name}. I am \{self::age}."
inc_age! ref! self = self::name.update! old -> old + 1
正確には、可変型・または可変型を含む複合型を基底型とする型は型名の最後に!
を付けなくてはなりません。!
を付けない型も同一の名前空間に存在してよく、別の型として扱われます。
上の例では、.age
属性は可変で、.name
属性は不変となっています。一つでも可変な属性がある場合、全体として可変型になります。
可変型はインスタンスを書き換えるプロシージャルメソッドを定義できますが、プロシージャルメソッドを持つからと言って可変型になるとは限りません。例えば配列型[T; N]
には要素をランダムに選ぶsample!
メソッドが実装されていますが、これはもちろん配列に破壊的変更を加えたりはしません。
可変型オブジェクトの破壊的操作は、主に.update!
メソッドを介して行います。.update!
メソッドは高階プロシージャで、self
に関数f
を適用して更新します。
i = !1
i.update! old -> old + 1
assert i == 2
.set!
メソッドは単に古い内容を捨てて新しい値に差し替えます。.set! x = .update! _ -> x
です。
i = !1
i.set! 2
assert i == 2
.freeze_map
メソッドは値を不変化して操作を行います。
a = [1, 2, 3].into [Nat; !3]
x = a.freeze_map a: [Nat; 3] -> a.iter().map(i -> i + 1).filter(i -> i % 2 == 0).collect(Array)
多相不変型において型の型引数T
は暗黙に不変型であると仮定されます。
# ImmutType < Type
K T: ImmutType = Class ...
K! T: Type = Class ...
標準ライブラリでは、可変型(...)!
型は不変型(...)
型を基底としている場合が多いです。しかしT!
型とT
型に言語上特別な関連はなく、そのように構成しなくても構いません1。
T = (...)
のとき単にT! = (...)!
となる型(...)
を単純構造型と呼びます。単純構造型は(意味論上)内部構造を持たない型ともいえます。
配列、タプル、セット、辞書、レコード型は単純構造型ではありませんが、Int型やStr型は単純構造型です。
以上の説明から、可変型とは自身が可変であるものだけでなく、内部に持つ型が可変であるものも含まれるということになります。
{x: Int!}
や[Int!; 3]
などの型は、内部のオブジェクトが可変であり、インスタンス自身が可変なわけではない内部可変型です。
Cell! T
Intや配列などの不変型に対しては、既に可変型が定義されています。しかし、このような可変型はどのようにして定義されたのでしょうか?例えば、{x = Int; y = Int}
型に対しては{x = Int!; y = Int!}
型などが対応する可変型です。
しかしInt!
型はどうやってInt
型から作られたのでしょうか?あるいはInt!
型はどのようにしてInt
型と関係付けられているのでしょうか?
それらに対する答えがCell!
型です。Cell! T
型はT
型オブジェクトを格納する箱のような型です。
IntOrStr = Inr or Str
IntOrStr! = Cell! IntOrStr
x = IntOrStr!.new 1
assert x is! 1 # `Int or Str` cannot compare with `Int` directly, so use `is!` (this compares object IDs) instead of `==`.
x.set! "a"
assert x is! "a"
Cell! T
型の重要な性質として、T
型の部分型になるというものがあります。これより、Cell! T
型のオブジェクトはT
型のメソッドを全て使うことができます。
# definition of `Int!`
Int! = Cell! Int
...
i = !1
assert i == 1 # `i` is casted to `Int`
1 T!
型とT
型に言語上の特別な関係がないのは意図的な設計です。関連があったとすると、例えば名前空間にT
/T!
型が存在するときに別のモジュールからT!
/T
型を導入できなくなるなどの不都合が生じます。また、不変型に対し可変型は一意に定まりません。T = (U, V)
という定義があった際、(U!, V)
と(U, V!)
という可変サブタイプがT!
としてあり得えます。↩
型境界
型境界は型指定に条件を加えるものである。これを実現する機能がガード(ガード節)である。 関数シグニチャ、無名関数シグニチャのほか、篩型でもこの機能を利用できる。 ガードは戻り値型の後に記述する。
述語式
変数の満たす条件を、Bool
を返す式(述語式)で指定できる。
使用できるのは値オブジェクトと演算子だけである。コンパイル時関数は今後のバージョンで対応される可能性がある。
f a: [T; N] | T, N, N > 5 = ...
g a: [T; N | N > 5] | T, N = ...
Odd = {I: Int | I % 2 == 1}
R2Plus = {(L, R) | L, R: Ratio; L > 0 and R > 0}
GeneralizedOdd = {I | U; I <: Div(Nat, U); I % 2 == 0}
複合型
タプル型
(), (X,), (X, Y), (X, Y, Z), ...
上のタプル型は構文糖であり、(X, Y) == Tuple [X, Y]
である。
タプルには、中の型だけでなく長さについての部分型規則が存在する。
任意のタプルT
, U
について、以下が成り立つ。
* T <: () (ユニット規則)
* forall N in 0..<Len(T) (Len(T) <= Len(U)), U.N == T.N => U <: T (忘却規則)
例えば、(Int, Str, Bool) <: (Int, Str)
である。
ただし、これらの規則は関数型のタプル(に見える)部分には適用されない。この部分は実際タプルではないためである。
(Int, Int) -> Int !<: (Int,) -> Int
また、ユニット型の戻り値は無視できるが、その他のタプル型の戻り値は無視できない。
配列型
[], [X; 0], [X; 1], [X; 2], ..., [X; _] == [X]
上の配列型は構文糖であり、[X; N] == Array X, N
である。
配列に関してもタプルと同様の部分型規則が存在する。
* T <: [] (ユニット規則)
* forall N in 0..<Len(T) (Len(T) <= Len(U)), U[N] == T[N] => U <: T (忘却規則)
下のような配列は型として有効ではない。配列の要素は等質化されていることを強調するための意図的な設計である。
[Int, Str]
このために、各要素の詳細な情報は失われてしまう。これを保つためには篩型を使う。
a = [1, "a"]: {A: [Int or Str; 2] | A[0] == Int}
a[0]: Int
セット型
{}, {T; _}, ...
上のセット型は構文糖であり、{T; N} == Set T, N
である。
セット型は長さの情報を持つが、あまり使われない。セットでは要素の重複は排除されるが、重複の判定は一般にコンパイル時には出来ないためである(そのため、長さは確定できず、多くの場合でパラメータは消去される)。そもそもセットにおいて長さの情報はあまり意味をなさない。
{}
は空集合であり、すべての型のサブタイプである。{T}
とすると定数T
のみを含む型となるので注意。
辞書型
{:}, {X: Y}, {X: Y, Z: W}, ...
上の形の辞書型はすべてDict K, V
のサブタイプである。Dict K, V
は均質化された辞書型を意味する。
{K: V} <: Dict K, V
であり、{X: Y, Z: W} <: Dict X or Z, Y or W
である。
レコード型
{=}, {i = Int}, {i = Int; j = Int}, {.i = Int; .j = Int}, ...
上の形のレコード型はすべてRecord
のサブタイプである。
属性が非公開のレコード型は公開のレコード型のスーパータイプである。
属性は非公開化できるが、逆はできないということである。
{.i = Int} <: {i = Int}
関数型
() -> ()
Int -> Int
(Int, Str) -> Bool
(x: Int, y: Int) -> Int
(x := Int, y := Int) -> Int
(*objs: Obj) -> Str
(Int, Ref Str!) -> Int
|T: Type|(x: T) -> T
|T: Type|(x: T := NoneType) -> T # |T: Type|(x: T := X, y: T := Y) -> T (X != Y) is invalid
バインドされたメソッド型
Int.() -> Int
Int.(other: Int) -> Int
# e.g. 1.__add__: Int.(Int) -> Int
C.(T) -> U
の型はT -> U
のサブタイプである。実際両者はほとんど変わらないが、C.(T) -> U
はC
をレシーバ型とするメソッドの型であり、__self__
という属性からレシーバにアクセスできる。
高度な型
以降は更に高度な型システムを解説します。入門者の方はすべての項を読まなくても問題ありません。
一般化代数的データ型
ErgはOr型をクラス化することで一般化代数的データ型(GADTs)を作成出来ます。
Nil T = Class(Impl := Phantom T)
Cons T = Class {head = T; rest = List T}, Impl := Unpack
List T: Type = Class(Nil T or Cons T)
List.
nil|T|() = Self(T).new Nil(T).new()
cons head, rest | T = Self(T).new Cons(T).new(head, rest)
head self = match self:
{head; *}: Cons _ -> head
_: Nil -> panic "empty list"
{nil; cons} = List
print! cons(1, cons(2, nil())).head() # 1
print! nil.head() # RuntimeError: "empty list"
List(T).nil() = ...
ではなくList.nil|T|() = ...
としているのは、使用時に型指定が不要になるからです。
i = List.nil()
_: List Int = cons 1, i
ここで定義したList T
はGADTsですが、素朴な実装であり、GADTsの真価を発揮していません。
例えば、上の.head
メソッドはもし中身が空なら実行時エラーを出しますが、この検査はコンパイル時に行うことができます。
List: (Type, {"Empty", "Nonempty"}) -> Type
List T, "Empty" = Class(Impl := Phantom T)
List T, "Nonempty" = Class {head = T; rest = List(T, _)}, Impl := Unpack
List.
nil|T|() = Self(T, "Empty").new Nil(T).new()
cons head, rest | T = Self(T, "Nonempty").new {head; rest}
List(T, "Nonempty").
head {head; *} = head
{nil; cons} = List
print! cons(1, cons(2, nil())).head() # 1
print! nil().head() # TypeError
巷でよく説明されるGADTsの例は、以上のように中身が空か否か型で判定できるリストです。 Ergではさらに精密化して、長さを持つリストを定義できます。
List: (Type, Nat) -> Type
List T, 0 = Class(Impl := Phantom T)
List T, N = Class {head = T; rest = List(T, N-1)}, Impl := Unpack
List.
nil|T|() = Self(T, 0).new Nil(T).new()
cons head, rest | T, N = Self(T, N).new {head; rest}
List(_, N | N >= 1).
head {head; *} = head
List(_, N | N >= 2).
pair {head = first; rest = {head = second; *}} = [first, second]
{nil; cons} = List
print! cons(1, cons(2, nil)).pair() # [1, 2]
print! cons(1, nil).pair() # TypeError
print! cons(1, nil).head() # 1
print! nil.head() # TypeError
デフォルト引数付きの関数型
まず、デフォルト引数の使用例を見る。
f: (Int, Int, z := Int) -> Int
f(x, y, z := 0) = x + y + z
g: (Int, Int, z := Int, w := Int) -> Int
g(x, y, z := 0, w := 1) = x + y + z + w
fold: ((Int, Int) -> Int, [Int], acc := Int) -> Int
fold(f, [], acc) = acc
fold(f, arr, acc := 0) = fold(f, arr[1..], f(acc, arr[0]))
assert fold(f, [1, 2, 3]) == 6
assert fold(g, [1, 2, 3]) == 8
:=
以降の引数はデフォルト引数である。
部分型付け規則は以下の通り。
((X, y := Y) -> Z) <: (X -> Z)
((X, y := Y, ...) -> Z) <: ((X, ...) -> Z)
1番目は、デフォルト引数のある関数は、ない関数と同一視できる、という意味である。 2番目は、任意のデフォルト引数は省略できる、という意味である。
デフォルト引数の型は、引数を渡した場合と渡さなかった場合で変えることができる。
具体的には、if
関数の型などが良い例である。
if: |T: Type, U: Type|(then: () -> T, else: () -> U := () -> NoneType) -> T or U
if
関数は、else
引数が与えられなければT or NoneType
を返す。
型消去
型消去とは、型引数に_
を指定し、その情報をあえて捨てることです。型消去は多相型を持つ言語の多くが併せて持つ機能ですが、Ergの文法に即して言えば型引数消去といった方が正確でしょう。
もっともよく見られる型消去された型の例は[T, _]
でしょう。配列はコンパイル時にその長さが分からない場合もあります。例えば、コマンドライン引数を指すsys.argv
は[Str, _]
型です。コマンドライン引数の長さをErgのコンパイラは知りようがないため、長さに関する情報は諦めなくてはならないのです。
しかし、型消去された型は、されていない型のスーパータイプになる(e.g. [T; N] < [T; _]
)ため、より多くのオブジェクトを受け取れるようになります。
[T; N]
型のオブジェクトはもちろん[T; _]
型のメソッドを使用できますが、使用後n
の情報は消去されます。長さが変わってしまっているかもしれないからです。長さが変わらないならばシグネチャで示さなくてはなりません。
# 配列の長さが変わらないことが保証される関数(sortなど)
f: [T; N] -> [T; N]
# 長さが保障されない関数(filterなど)
g: [T; n] -> [T; _]
型指定自体で_
を使うとその型はObject
までアップキャストされます。
型でない型引数(Int, Bool型など)の場合、_
としたパラメータは未定義になります。
i: _ # i: Object
[_; _] == [Object; _] == Array
型消去は型指定の省略とは違います。一度型引数情報を消去してしまうと、再びアサーションしなければ情報は戻りません。
implicit = (1..5).iter().map(i -> i * 2).to_arr()
explicit = (1..5).iter().map(i -> i * 2).into(Array(Nat))
Rustでは以下のコードに対応します。
#![allow(unused)] fn main() { let partial = (1..6).iter().map(|i| i * 2).collect::<Vec<_>>(); }
Ergでは型の部分省略はできず、代わりに高階カインド多相を使用します。
# collectはカインドを受け取る高階カインドのメソッド
hk = (1..5).iter().map(i -> i * 2).collect(Array)
hk: Array(Int)
存在型
∀に対応する全称型があるならば、∃に対応する存在型があると考えるのが自然です。 存在型は難しいものではありません。そうと意識していないだけで、既にあなたは存在型を知っています。
T: Trait
f x: T = ...
上のトレイトT
は存在型として使われています。
対して下の場合のT
はトレイトでしかなく、X
は全称型です。
f|X <: T| x: X = ...
実際、存在型は全称型に置き換えられます。ではなぜ存在型などというものが存在するのでしょうか。 まず、上で見たように存在型は型変数を伴わないので、型指定をシンプルにできます。 また、型変数を除去できるので全称型ならランク2を超えてしまうような型も構成できます。
show_map f: (|T| T -> T), arr: [Show; _] =
arr.map x ->
y = f x
log y
y
しかし、見ればわかるように存在型は元の型を忘却・拡大してしまうので、戻り値の型を広げたくない場合などは全称型を使う必要があります。 逆に、引数として受け取るだけで戻り値に関係のない型は存在型で記述して構いません。
# id(1): Intが期待される
id|T|(x: T): T = x
# |S <: Show|(s: S) -> ()は冗長
show(s: Show): () = log s
ちなみに、クラスは存在型とは呼びません。予めその要素となるオブジェクトが定められているためです。 存在型はあるトレイトを満たすすべての型という意味で、実際にどのような型が代入されるか知るところではないのです。
キーワード引数付き関数型
h(f) = f(y: 1, x: 2)
h: |T: Type|((y: Int, x: Int) -> T) -> T
キーワード引数付き関数の部分型付け規則は以下の通り。
((x: T, y: U) -> V) <: ((T, U) -> V) # x, yは任意のキーワードパラメータ
((y: U, x: T) -> V) <: ((x: T, y: U) -> V)
((x: T, y: U) -> V) <: ((y: U, x: T) -> V)
これは、キーワード引数は消去ないし入れ替えができるということを意味する。
しかし、両者を同時に行うことはできない。
すなわち、(x: T, y: U) -> V
を(U, T) -> V
にキャストすることはできない。
なお、キーワード引数がつくのはトップレベルのタプル内のみで、配列やネストしたタプルでキーワード引数は付かない。
Valid: [T, U] -> V
Invalid: [x: T, y: U] -> V
Valid: (x: T, ys: (U,)) -> V
Invalid: (x: T, ys: (y: U,)) -> V
カインド
Ergでは全てが型付けられている。型自体も例外ではない。「型の型」を表すのが カインド(種) である。例えば1
がInt
に属しているように、Int
はType
に属している。Type
は最もシンプルなカインドである 原子カインド(Atomic kind) である。型理論的の記法では、Type
は*
に対応する。
カインドという概念で実用上重要なのは1項以上のカインド(多項カインド)である。1項のカインドは、例えばOption
などがそれに属する。1項カインドはType -> Type
と表される1。Array
やOption
などの コンテナ は特に型を引数に取る多項カインドのことなのである。
Type -> Type
という表記が示す通り、実はOption
はT
という型を受け取ってOption T
という型を返す関数である。ただし、この関数は通常の意味での関数ではないため、1項カインド(unary kind)と普通は呼称される。
なお、無名関数演算子である->
自体も型を受け取って型を返す場合カインドとみることができる。
また、原子カインドでないカインドは型ではないことに注意してほしい。-1
は数値だが-
は数値ではないのと同じように、Option Int
は型だがOption
は型ではない。Option
などは型構築子と呼ばれることもある。
assert not Option in Type
assert Option in Type -> Type
なので、以下のようなコードはエラーになる。
Ergではメソッドを定義できるのは原子カインドのみで、メソッドの第一引数以外の場所でself
という名前を使えない。
# Kは単項の一種です
K: Type -> Type
K T = Class ...
K.
foo x = ... # OK、これはいわゆるスタティックメソッドのようなもの
bar self, x = ... # TypeError: cannot define a method to a non-type object
K(T).
baz self, x = ... # OK
2項以上のカインドの例としては{T: U}
(: (Type, Type) -> Type
), (T, U, V)
(: (Type, Type, Type) -> Type
), ...などが挙げられる。
0項のカインド() -> Type
も存在する。これは型理論的には原子カインドと同一視されることもあるが、Ergでは区別される。例としてはClass
などがある。
Nil = Class()
カインドの包含関係
多項カインド間にも部分型関係、もとい部分カインド関係があります。
K T = ...
L = Inherit K
L <: K
すなわち、任意のT
に対しL T <: K T
ならばL <: K
であり、その逆も成り立ちます。
∀T. L T <: K T <=> L <: K
高階カインド
高階カインド(higher-order kind)というものもある。これは高階関数と同じコンセプトのカインドで、カインド自体を受け取るカインドである。(Type -> Type) -> Type
などが高階カインドである。高階カインドに属するオブジェクトを定義してみよう。
IntContainerOf K: Type -> Type = K Int
assert IntContainerOf Option == Option Int
assert IntContainerOf Result == Result Int
assert IntContainerOf in (Type -> Type) -> Type
多項カインドの束縛変数はK, L, ...などと表されるのが通例である(KはKindのK)。
セットカインド
型理論において、レコードという概念がある。これはErgのレコードとほぼ同じものである2。
# これは`レコード`であり、型理論でいうところの`レコード`に相当するものである
{x = 1; y = 2}
レコードの値が全て型であるとき、それはレコード型といって型の一種であった。
assert {x = 1; y = 2} in {x = Int; y = Int}
レコード型はレコードを型付けする。察しの良い方は、レコード型を型付けする「レコードカインド」があるはずだと考えたかもしれない。実際に、それは存在する。
log Typeof {x = Int; y = Int} # {{x = Int; y = Int}}
{{x = Int; y = Int}}
のような型がレコードカインドである。これは特別な記法ではない。単に、{x = Int; y = Int}
のみを要素に持つ列挙型である。
Point = {x = Int; y = Int}
Pointy = {Point}
レコードカインドの重要な特性は、T: |T|
であり、U <: T
であるとき、U: |T|
であるという点にある。
これは列挙型が実際には篩型の糖衣構文であることからもわかる。
# 通常のオブジェクトでは{c} == {X: T | X == c}だが、
# 型の場合等号が定義されない場合があるので|T| == {X | X <: T}となる
{Point} == {P | P <: Point}
型制約中のU <: T
は、実はU: |T|
の糖衣構文である。
このような型のセットであるカインドは一般にセットカインドと呼ばれる。セットカインドはIteratorパターンでも現れる。
Iterable T = Trait {
.Iterator = {Iterator}
.iter = (self: Self) -> Self.Iterator T
}
多項カインドの型推論
Container K: Type -> Type, T: Type = Patch K(T, T)
Container(K).
f self = ...
Option T: Type = Patch T or NoneType
Option(T).
f self = ...
Fn T: Type = Patch T -> T
Fn(T).
f self = ...
Fn2 T, U: Type = Patch T -> U
Fn2(T, U).
f self = ...
(Int -> Int).f() # どちらが選択されるだろうか?
上の例で、メソッドf
はどのパッチが選ばれるのだろうか。
素朴に考えてFn T
が選ばれるように思われるが、Fn2 T, U
もあり得るし、Option T
はT
そのままを含むので任意の型が該当し、Container K, T
も `->`(Int, Int)
すなわちContainer(`->`, Int)
としてInt -> Int
にマッチする。なので、上の4つのパッチすべてが選択肢としてありえる。
この場合、以下の優先基準に従ってパッチが選択される。
- 任意の
K(T)
(e.g.T or NoneType
)はType
よりもType -> Type
に優先的にマッチする。 - 任意の
K(T, U)
(e.g.T -> U
)はType
よりも(Type, Type) -> Type
に優先的にマッチする。 - 3項以上のカインドについても同様の基準が適用される。
- 置換する型変数が少なく済むものが選択される。例えば
Int -> Int
はK(T, T)
(置換する型変数: K, T)やT -> U
(置換する型変数: T, U)よりもT -> T
(置換する型変数: T)が優先的にマッチする。 - 置換数も同じ場合は選択不能としてエラー。
1 型理論の記法では*=>*
↩
2 可視性などの微妙な違いはある。↩
マーカートレイト
マーカートレイトは、要求属性のないトレイトである。すなわち、メソッドを実装せずにImplすることができる。 要求属性がないと意味がないように思えるが、そのトレイトに属しているという情報が登録されるので、パッチメソッドを使ったり、コンパイラが特別扱いしたりできる。
すべてのマーカートレイトはMarker
トレイトに包摂される。
標準で提供されているLight
はマーカートレイトの一種である。
Light = Subsume Marker
Person = Class {.name = Str; .age = Nat} and Light
M = Subsume Marker
MarkedInt = Inherit Int, Impl := M
i = MarkedInt.new(2)
assert i + 1 == 2
assert i in M
マーカークラスはExcluding
引数で外すことも可能である。
NInt = Inherit MarkedInt, Impl := N, Excluding: M
可変構造型
T!
型は任意のT
型オブジェクトを入れられて差し替え可能なボックス型であると説明した。
Particle! State: {"base", "excited"} = Class(..., Impl := Phantom State)
Particle!.
# このメソッドはStateを"base"から"excited"に遷移させる
apply_electric_field!(ref! self("base" ~> "excited"), field: Vector) = ...
T!
型は、データの差し替えは行えるが、その構造を変えることはできない。
より現実のプログラムの振舞いに近い言い方をすれば、(ヒープ上の)サイズを変更できない。このような型を、不変構造(可変)型と呼ぶ。
実は、不変構造型では表すことのできないデータ構造が存在する。
例えば、可変長配列である。[T; N]!
型は任意の[T; N]
であるオブジェクトを入れることができるが、[T; N+1]
型オブジェクトなどに差し替えることはできない。
すなわち、長さを変えられないのである。長さを変えるためには、型自体の構造を変化させなくてはならない。
それを実現するのが可変構造(可変)型である。
v = [Str; 0]!.new()
v.push! "Hello"
v: [Str; 1]!
可変構造型では可変化する型引数に!
を付ける。上の場合は、[Str; 0]!
型を[Str; 1]!
型などに変更することができる。すなわち、長さを変更できる。
因みに、[T; N]!
型はArray!(T, N)
型の糖衣構文である。
可変構造型はもちろんユーザー定義も可能である。ただし、不変構造型とは構成法に関していくつか違いがあるので注意が必要である。
Nil T = Class(Impl := Phantom T)
List! T, 0 = Inherit Nil T
List! T, N: Nat = Class {head = T; rest = List!(T, N-1)}
List!(T, N).
push! ref! self(N ~> N+1, ...), head: T =
self.update! old -> Self.new {head; old}
新規型パターン
ここでは、Rustでよく使われるnewtypeパターンのErg版を紹介します。
Ergはでは以下のように型のエイリアスを定義できますが、これはあくまで同じ型を指します。
UserId = Int
なので、例えばUserId
型の数値は8桁の正数、という仕様があったとしても、Int
型と同じなので10でも-1でも入れられてしまうわけです。Nat
にすれば-1は弾くことができますが、8桁の数という性質はErgの型システムのみでは表現できません。
また、例えばあるデータベースのシステムを設計する時、いくつかの種類のIDがあったとします。ユーザーID, 商品ID, 注文IDなどとIDの種類が増えてくると、関数に違う種類のIDを渡すというバグが発生する可能性があります。ユーザーIDと商品IDなどは構造的に等価であっても、意味論的には異なるわけです。
newtypeパターンはこのような場合に適したデザインパターンです。
UserId = Class {id = Nat}
UserId.
new id: Nat =
assert id.dights().len() == 8, else: "UserId must be a positive number with length 8"
UserId::__new__ {id;}
i = UserId.new(10000000)
print! i # <__main__.UserId object>
i + UserId.new(10000001) # TypeError: + is not implemented between `UserId` and `UserId`
コンストラクタが8桁の数という事前条件を保証してくれます。
このUserId
はNat
の持つメソッドをすべて失ってしまうので、必要な演算を都度再定義する必要があります。
再定義するコストが見合わない場合は、継承を使う方がよいでしょう。逆にメソッドがなくなるという性質が望ましい場合もあるので、状況に応じて適切な方法を選んでください。
オーバーロード
Ergでは アドホック多相 をサポートしない。すなわち、関数・カインドの多重定義(オーバーロード)ができない。が、トレイトクラスとパッチを組み合わせることでオーバーロードの挙動を再現できる。
トレイトクラスのかわりにトレイトを使用しても良いが、その場合.add1
を実装している型全てが対象になってしまう。
Add1 = Trait {
.add1: Self.() -> Self
}
IntAdd1 = Patch Int, Impl := Add1
IntAdd1.
add1 self = self + 1
RatioAdd1 = Patch Ratio, Impl := Add1
RatioAdd1.
add1 self = self + 1.0
add1|X <: Add1| x: X = x.add1()
assert add1(1) == 2
assert add1(1.0) == 2.0
このような、ある型のサブタイプすべてを受け入れることによる多相を サブタイピング多相 と呼ぶ。Ergにおけるサブタイピング多相は列多相も含む。
各型での処理が完全に同じなら下のように書くこともできる。上の書き方は、クラスによって挙動を変える(が、戻り値型は同じ)場合に使う。 型引数を使う多相を パラメトリック多相 という。パラメトリック多相は下のように部分型指定と併用する場合が多く、その場合はパラメトリック多相とサブタイピング多相の合わせ技ということになる。
add1|T <: Int or Str| x: T = x + 1
assert add1(1) == 2
assert add1(1.0) == 2.0
また、引数の数が違うタイプのオーバーロードはデフォルト引数で再現できる。
C = Class {.x = Int; .y = Int}
C.
new(x, y := 0) = Self::__new__ {.x; .y}
assert C.new(0, 0) == C.new(0)
引数の数によって型が違うなど全く挙動が変わる関数は定義できないが、そもそも振る舞いが異なるならば別の名前を付けるべきであるというスタンスをErgは取る。
結論として、Ergがオーバーロードを禁止してサブタイピング+パラメトリック多相を採用したのは以下の理由からである。
まず、オーバーロードされた関数は定義が分散する。このため、エラーが発生した際に原因となる箇所を報告するのが難しい。 また、サブルーチンをインポートすることによって、すでに定義されたサブルーチンの挙動が変わる恐れもある。
{id;} = import "foo"
...
id x: Int = x
...
id x: Ratio = x
...
id "str" # TypeError: id is not implemented for Str
# しかし、このエラーはどこから来たのだろうか?
次に、デフォルト引数との相性が悪い。デフォルト引数のある関数がオーバーロードされているとき、どれが優先されるかという問題がある。
f x: Int = ...
f(x: Int, y := 0) = ...
f(1) # どちらが選択されるだろうか?
さらに、宣言との相性が悪い。
宣言f: Num -> Num
は、どちらの定義のことを指しているのか特定できない。Int -> Ratio
とRatio -> Int
は包含関係がないためである。
f: Num -> Num
f(x: Int): Ratio = ...
f(x: Ratio): Int = ...
そして、文法の一貫性を損なう。Ergは変数の再代入を禁止するが、オーバーロードの文法は再代入のように見えてしまう。 無名関数に置換することもできない。
# `f = x -> body`と同じ
f x = body
# 以下は同じ...ではない
f x: Int = x
f x: Ratio = x
幽霊型
幽霊型は、コンパイラに注釈を与えるためだけに存在するマーカートレイトである。 幽霊型の使い方として、リストの構成をみる。
Nil = Class()
List T, 0 = Inherit Nil
List T, N: Nat = Class {head = T; rest = List(T, N-1)}
このコードはエラーとなる。
3 | List T, 0 = Inherit Nil
^^^
TypeConstructionError: since Nil does not have a parameter T, it is not possible to construct List(T, 0) with Nil
hint: use 'Phantom' trait to consume T
このエラーはつまり、List(_, 0).new Nil.new()
とされたときにT
の型推論ができないという文句である。Ergでは型引数を未使用のままにすることができないのである。
このような場合は何でもよいのでT
型を右辺で消費する必要がある。サイズが0の型、例えば長さ0のタプルならば実行時のオーバーヘッドもなく都合がよい。
Nil T = Class((T; 0))
List T, 0 = Inherit Nil T
List T, N: Nat = Class {head = T; rest = List(T, N-1)}
このコードはコンパイルを通る。だが少しトリッキーで意図が分かりづらい上に、型引数が型のとき以外では使えない。
このようなときにちょうどよいのが幽霊型である。幽霊型はサイズ0の型を一般化した型である。
Nil T = Class(Impl := Phantom T)
List T, 0 = Inherit Nil T
List T, N: Nat = Class {head = T; rest = List(T, N-1)}
nil = Nil(Int).new()
assert nil.__size__ == 0
Phantom
がT
型を保持する。しかし実際にはPhantom T
型のサイズは0であり、T
型のオブジェクトを保持してはいない。
また、Phantom
は型以外にも任意の型引数を消費することができる。以下の例ではState
というStr
のサブタイプオブジェクトである型引数をPhantom
が保持している。
この場合も、state
はオブジェクトの実体に現れないハリボテの型変数である。
VM! State: {"stopped", "running"}! = Class(..., Impl := Phantom! State)
VM!("stopped").
start ref! self("stopped" ~> "running") =
self.do_something!()
self::set_phantom!("running")
state
はupdate_phantom!
メソッドかset_phantom!
メソッドを介して更新する。
これはPhantom!
(Phantom
の可変版)の標準パッチが提供するメソッドで、使い方は可変型のupdate!
, set!
と同じである。
射影型
射影型は、次のコードにおけるSelf.AddO
のような型を表します。
Add R = Trait {
.`_+_` = Self, R -> Self.AddO
.AddO = Type
}
AddForInt = Patch(Int, Impl := Add Int)
AddForInt.
AddO = Int
Add(R)
型は何らかのオブジェクトとの加算が定義されている型といえます。メソッドは型属性であるべきなので、+
の型宣言はインデント以下に記述します。
Add
型のミソとなるのが.AddO = Type
という宣言で、射影型である.AddO
型の実体は、Add
のサブタイプである型が持ちます。例えば、Int.AddO = Int
, Odd.AddO = Even
です。
assert Int < Add
assert Int.AddO == Int
assert Odd < Add
assert Odd.AddO == Even
量化依存型
Ergには量化型、依存型が存在します。すると当然、その二つを組み合わせた型を作ることができます。それが量化依存型です。
NonNullStr = |N: Nat| StrWithLen N | N != 0 # N: Nat; S: StrWithLen N; N != 0}と同じ
NonEmptyArray = |N: Nat| [_; N | N > 0] # N: Nat; A: Array(_, N); N > 0}と同じ
量化依存型の標準形はK(A, ... | Pred)
です。K
は型構築子、A, B
は型引数、Pred
は条件式です。
左辺値としての量化依存型は、元の型と同じモジュール内でのみメソッドを定義出来ます。
K A: Nat = Class ...
K(A).
...
K(A | A >= 1).
method ref! self(A ~> A+1) = ...
右辺値としての量化依存型は、使用する型変数を型変数リスト(||
)で宣言する必要がある。
# Tは具体的な型
a: |N: Nat| [T; N | N > 1]
共有参照
共有参照は気をつけて扱わねばならない言語機能の一つです。 例えばTypeScriptでは以下のようなコードが型検査を通ってしまいます。
class NormalMember {}
class VIPMember extends NormalMember {}
let vip_area: VIPMember[] = []
let normal_area: NormalMember[] = vip_area
normal_area.push(new NormalMember())
console.log(vip_area) # [NormalMember]
一般会員がVIPエリアに侵入してしまっています。これは明らかなバグですが、何がいけなかったのでしょうか。
原因は共有参照の変性です。normal_area
はvip_area
をコピーして作成されていますが、その際に型が変わってしまっています。
しかしVIPMember
はNormalMember
を継承しているのでVIPMember[] <: NormalMember[]
となり、これは問題ないとされてしまっているのです。
VIPMember[] <: NormalMember[]
という関係は、不変オブジェクトの場合は問題ありません。しかし上のように破壊的な操作を行ってしまうと、綻びが発生します。
Ergでは、所有権システムのおかげでこのようなコードは弾かれます。
NormalMember = Class()
VIPMember = Class()
vip_area = [].into [VIPMember; !_]
normal_area: [NormalMember; !_] = vip_area
normal_area.push!(NormalMember.new())
log vip_area # OwnershipError: `vip_room` was moved to `normal_room`
しかし、オブジェクトの所有権が一箇所にしかない状態は不便である場合もあります。
そのためにErgはSharedCell! T!
という型があり、これが共有状態を表します。
$p1 = SharedCell!.new(!1)
$p2 = $p1.mirror!()
$p3 = SharedCell!.new(!1)
# $p1 == $p2とすると、中身の型Int!の比較が行われる
assert $p1 == $p2
assert $p1 == $p3
# $p1と$p2が同じものを指しているかは、`.addr!`で確認する
assert $p1.addr!() == $p2.addr!()
assert $p1.addr!() != $p3.addr!()
$p1.add! 1
assert $p1 == 2
assert $p2 == 2
assert $p3 == 1
SharedCell!
型のオブジェクトは先頭に$
を付ける必要があります。また、その性質上、定数にすることはできません。
SharedCell! T!
型はT!
型のサブタイプでもあり、T!
型のメソッドを呼び出すことができます。SharedCell! T!
型固有のメソッドは.addr!
と.mirror!
、.try_take
のみです。
重要な事実として、SharedCell! T!
は非変(non-variant)です。すなわち、型引数の違いによる包含関係が定義されません。
$vip_area = SharedCell!.new([].into [VIPMember; !_])
$normal_area: SharedCell!([NormalMember; !_]) = $vip_area.mirror!() # TypeError: expected SharedCell!([NormalMember; !_]), but got SharedCell!([VIPMember; !_])
# hint: SharedCell!(T) is non-variant, which means it cannot have a supertype or a subtype.
しかし、以下のコードは問題ありません。最後の行では、型変換されたのは引数のVIPMember
の方です。
$normal_area = SharedCell!.new([].into [NormalMember; !_])
$normal_area.push!(NormalMember.new()) # OK
$normal_area.push!(VIPMember.new()) # OK
特殊型
Self
は自身の型を表します。単にエイリアスとして使うことも出来ますが、派生型中では意味が変わる(自身の型を指す)ので注意してください。
@Inheritable
C = Class()
C.
new_self() = Self.new()
new_c() = C.new()
D = Inherit C
classof D.new_self() # D
classof D.new_c() # C
Super
は基底クラスの型を表します。メソッド自体は基底クラスのものを参照しますが、インスタンスは自身の型を使います。
@Inheritable
C = Class()
D = Inherit(C)
D.
new_super() = Super.new()
new_c() = C.new()
classof D.new_super() # D
classof D.new_c() # C
特殊型変数
Self
, Super
は、構造型・トレイト中では型変数として使用できます。これは、その型のサブタイプであるところのクラスを指します。すなわち、型T
中でSelf
はSelf <: T
を意味します。
Add R = Trait {
.AddO = Type
.`_+_`: Self, R -> Self.AddO
}
ClosedAdd = Subsume Add(Self)
ClosedAddForInt = Patch(Int, Impl := ClosedAdd)
ClosedAddForInt.
AddO = Int
assert 1 in Add(Int, Int)
assert 1 in ClosedAdd
assert Int < Add(Int, Int)
assert Int < ClosedAdd
Typeof, classof
Typeof
はErgの型推論システムを覗くことができる関数であり、その挙動は複雑である。
assert Typeof(1) == {I: Int | I == 1}
i: 1..3 or 5..10 = ...
assert Typeof(i) == {I: Int | (I >= 1 and I <= 3) or (I >= 5 and I <= 10)}
C = Class {i = Int}
I = C.new {i = 1}
assert Typeof(I) == {X: C | X == I}
J: C = ...
assert Typeof(J) == {i = Int}
assert {X: C | X == I} < C and C <= {i = Int}
Typeof
関数ではオブジェクトのクラスではなく構造型が返される。
なので、C = Class T
なるクラスのインスタンスI: C
に対してはTypeof(I) == T
となる。
値クラスに関しては本来対応するレコード型が存在しない。この問題を解消するため、値クラスは__valueclass_tag__
属性を持っているレコード型ということになっている。
なお、この属性にアクセスすることはできず、ユーザー定義型で__valueclass_tag__
属性を定義することもできない。
i: Int = ...
assert Typeof(i) == {__valueclass_tag__ = Phantom Int}
s: Str = ...
assert Typeof(s) == {__valueclass_tag__ = Phantom Str}
Typeof
で出力されるのは構造型のみである。構造型には属性型と篩型、(真の)代数演算型があると説明した。
これらは独立な型(推論の優先順位が存在する)であり、推論の重解は発生しない。
属性型、代数演算型は複数のクラスにまたがる可能性があるが、篩型は単一のクラスのサブタイプである。
Ergは可能な限りオブジェクトの型を篩型として推論し、それができなくなった際は篩型のベースクラスを構造化(後述)した型に拡大する。
構造化
すべてのクラスは構造型に変換することができる。これを 構造化 という。クラスの構造化された型はStructure
関数で取得できる。
クラスがC = Class T
で定義されているとき(すべてのクラスはこの形式で定義されている)、Structure(C) == T
になる。
C = Class {i = Int}
assert Structure(C) == {i = Int}
D = Inherit C
assert Structure(D) == {i = Int}
Nat = Class {I: Int | I >= 0}
assert Structure(Nat) == {I: Int | I >= 0}
Option T = Class (T or NoneType)
assert Structure(Option Int) == Or(Int, NoneType)
assert Structure(Option) # TypeError: only monomorphized types can be structurized
# 実際には__valueclass_tag__を持つレコードは定義できないが、概念上はこうなる
assert Structure(Int) == {__valueclass_tag__ = Phantom Int}
assert Structure(Str) == {__valueclass_tag__ = Phantom Str}
assert Structure((Nat, Nat)) == {__valueclass_tag__ = Phantom(Tuple(Nat, Nat))}
assert Structure(Nat -> Nat) == {__valueclass_tag__ = Phantom(Func(Nat, Nat))}
# マーカークラスも__valueclass_tag__を持つレコード型になる
M = Inherit Marker
assert Structure(M) == {__valueclass_tag__ = Phantom M}
D = Inherit(C and M)
assert Structure(D) == {i = Int; __valueclass_tag__ = Phantom M}
E = Inherit(Int and M)
assert Structure(E) == {__valueclass_tag__ = Phantom(And(Int, M))}
F = Inherit(E not M)
assert Structure(F) == {__valueclass_tag__ = Phantom Int}
変性
Ergは多相型のサブタイピングを行えるが、一部注意しなくてはならない点がある。
まずは通常の多相型の包含関係を考える。一般に、コンテナK
と代入する型A, B
があり、A < B
のとき、K A < K B
となる。
例えば、Option Int < Option Object
となる。よって、Option Object
で定義されているメソッドは、Option Int
でも使用可能である。
典型的な多相型であるArray!(T)
型について考える。
今回は要素の数を問題にしないのでArray!(T, N)
ではないことに注意してほしい。
さて、Array!(T)
型には.push!
と.pop!
というメソッドが存在し、それぞれ、要素の追加・取り出しを意味する。型はこうである。
Array.push!: Self(T).(T) => NoneType Array.pop!: Self(T).() => T
直感的に理解できることとして、
s: Str
のときArray!(Object).push!(s)
はOK(Str
をObject
にアップキャストすれば良い)o: Object
のときArray!(Str).push!(o)
はNGArray!(Object).pop!().into(Str)
はNGArray!(Str).pop!().into(Object)
はOK
である。これは、型システム的には
- (Self(Object).(Object) => NoneType) < (Self(Str).(Str) => NoneType)
- (Self(Str).() => Str) < (Self(Object).() => Object)
を意味する。
前者は奇妙に思えるかもしれない。Str < Object
なのに、それを引数に取る関数は包含関係が逆転している。
型理論では、このような関係(.push!
の型関係)を反変(contravariant)といい、その逆、.pop!
の型関係は共変(covariant)という。
つまり、関数型は引数の型に関して反変であり、戻り値の型に関して共変である、といえる。
複雑に聞こえるが、先程見た通り実例に当てはめて考えれば合理的なルールである。
それでもいまいちピンと来ない場合は次のように考えるとよい。
Ergの設計方針に、「入力の型は大きく、出力の型は小さく」というのがある。これはまさに関数の変性から言える。 上のルールを見れば、入力型は大きい方が全体として小さい型になる。 汎用の関数は明らかに専用の関数より希少だからである。 そして出力型は小さい方が全体として小さくなる。
結果として上の方針は「関数の型を最小化せよ」と言っているのに等しい。
非変性
Ergにはもう一つ変性がある。それは非変性(non-variance)である。
これは組み込み型ではSharedCell! T!
などが持っている変性である。これは、T! != U!
なる2つの型T!, U!
に関して、例え包含関係があったとしてもSharedCell! T!
とSharedCell! U!
間でキャストができないことを意味する。
これは、SharedCell! T!
が共有参照であることに由来する。詳しくは共有参照を参照。
変性指定された全称型
全称型の型変数は、その上限・下限を指定できます。
|A <: T| K(A)
|B :> T| K(B)
型変数リスト内では型変数の 変性指定 を行っています。上の変性指定において、型変数A
は型T
に対する任意のサブクラスであり、型変数B
は型T
に対する任意のスーパークラスであると宣言されています。
このとき、T
をA
に対する上限型、B
に対する下限型ともいいます。
変性指定は重ねがけすることもできます。
# U<A<T
|A<: T, A :> U| ...
以下に変性指定を使ったコードの例を示します。
show|S <: Show| s: S = log s
Nil T = Class(Impl=Phantom T)
Cons T = Class(Nil T or List T)
List T = Class {head = T; rest = Cons T}
List(T).
push|U <: T|(self, x: U): List T = Self.new {head = x; rest = self}
upcast(self, U :> T): List U = self
変性指定
List T
の例については注意が必要なので、もう少し詳しく説明します。
上のコードを理解するためには多相型の変性について知っておく必要があります。変性についてはこの項で詳しく解説していますが、さしあたって必要となる事実は以下の3つです:
- 通常の多相型、
List T
などはT
に対して共変(U > T
のときList U > List T
) - 関数
T -> U
は引数型T
に対して反変(S > T
のとき(S -> U) < (T -> U)
) - 関数
T -> U
は戻り値型U
に対して共変(U > S
のとき(T -> U) > (T -> S)
)
例えば、List Int
はList Object
にアップキャスト可能、Obj -> Obj
はInt -> Obj
にアップキャスト可能であるということです。
ここで、メソッドの変性指定を省略した場合どうなるか考えます。
...
List T = Class {head = T; rest = Cons T}
List(T).
# List T can be pushed U if T > U
push|U|(self, x: U): List T = Self.new {head = x; rest = self}
# List T can be List U if T < U
upcast(self, U): List U = self
この場合でも、ErgコンパイラはU
の上限・下限型をよしなに推論してくれます。
ただし、Ergコンパイラはメソッドの意味を理解しないことに注意してください。コンパイラはただ変数・型変数の使われ方に従って機械的に型関係を推論・導出します。
コメントに書いてある通り、List T
のhead
に入れられる型U
はT
のサブクラス(T: Int
ならばNat
など)です。すなわち、U <: T
と推論されます。この制約は.push{U}
の引数型を変更するアップキャスト(List(T), U) -> List(T) to (List(T), T) -> List(T)
(e.g. List(Int).push{Object}
)を禁止します。ただし、U <: T
という制約は関数の型の包含関係を改変しているわけではないことに注意してください。(List(Int), Object) -> List(Int) to (List(Int), Int) -> List(Int)
である事実は変わらず、ただ.push
メソッドにおいてはそのようなアップキャストを実行できないという意味になります。
同様に、List T
からList U
へのキャストはU :> T
という制約のもとで可能なので、そのように変性指定が推論されます。この制約は、.upcast(U)
の戻り値型を変更するアップキャストList(T) -> List(T) to List(T) -> List(T)
(e.g. List(Object).upcast(Int)
)を禁止します。
では、このアップキャストを許可するようにした場合はどうなるか考えます。 変性指定を反転させてみましょう。
...
List T = Class {head = T; rest = Cons T}
List(T).
push|U :> T|(self, x: U): List T = Self.new {head = x; rest = self}
upcast(self, U :> T): List U = self
# TypeWarning: `U` in the `.push` cannot take anything other than `U == T`. Replace `U` with `T`. Or you may have the wrong variance specification.
# TypeWarning: `U` in the `.upcast` cannot take anything other than `U == T`. Replace `U` with `T`. Or you may have the wrong variance specification.
U <: T
という制約とU :> T
という変性指定の両方を充足するのはU == T
のときだけです。なので、この指定にはほとんど意味がありません。
実際は「U == T
であるようなアップキャスト」=「U
の箇所については変えないアップキャスト」のみが許可されています。
付録: ユーザー定義型の変性
ユーザー定義型の変性は、デフォルトでは非変である。しかし、Inputs/Outputs
というマーカートレイトで変性を指定することもできる。
Inputs(T)
と指定すると、その型はT
に関して反変となる。
Outputs(T)
と指定すると、その型はT
に関して共変となる。
K T = Class(...)
assert not K(Str) <= K(Object)
assert not K(Str) >= K(Object)
InputStream T = Class ..., Impl := Inputs(T)
# Objectを受け入れるストリームは、Strを受け入れるともみなせる
assert InputStream(Str) > InputStream(Object)
OutputStream T = Class ..., Impl := Outputs(T)
# Strを出力するストリームは、Objectを出力するともみなせる
assert OutputStream(Str) < OutputStream(Object)
型拡大
例えば以下のような多相関数を定義する。
ids|T|(x: T, y: T) = x, y
同じクラスのインスタンスペアを代入する分には何の問題もない。 包含関係にある別のクラスのインスタンスペアを代入すると、大きい方にアップキャストされて同じ型になる。 また、包含関係にない別のクラスを代入するとエラーになるのも容易に理解できる。
assert ids(1, 2) == (1, 2)
assert ids(1, 2.0) == (1.0, 2.0)
ids(1, "a") # TypeError
さて、では別の構造型を持つ型の場合はどうなるのだろうか。
i: Int or Str
j: Int or NoneType
ids(i, j) # ?
これの説明を行う前に、Ergの型システムが実は(実行時の)クラスを見ていないという事実に注目しなくてはならない。
1: {__valueclass_tag__ = Phantom Int}
2: {__valueclass_tag__ = Phantom Int}
2.0: {__valueclass_tag__ = Phantom Ratio}
"a": {__valueclass_tag__ = Phantom Str}
ids(1, 2): {__valueclass_tag__ = Phantom Int} and {__valueclass_tag__ = Phantom Int} == {__valueclass_tag__ = Phantom Int}
ids(1, 2.0): {__valueclass_tag__ = Phantom Int} and {__valueclass_tag__ = Phantom Ratio} == {__valueclass_tag__ = Phantom Ratio} # Int < Ratio
ids(1, "a"): {__valueclass_tag__ = Phantom Int} and {__valueclass_tag__ = Phantom Str} == Never # TypeError
クラスを見ていないというのは、正確には見られない場合があるからで、これはErgにおいてオブジェクトのクラスは実行時情報に属するためである。
例えば、Int or Str
型オブジェクトのクラスはInt
またはStr
であるが、これがどちらなのかは実行してはじめてわかることである。
もちろんInt
型のオブジェクトのクラスはInt
で確定であるが、この場合も型システムから見えるのはInt
の構造型{__valueclass_tag__ = Int}
である。
さて、別の構造型の例に戻ろう。結論から言うと上のコードは型があっていないとしてTypeErrorになる。 しかし型注釈で型拡大を行えばコンパイルが通る。
i: Int or Str
j: Int or NoneType
ids(i, j) # TypeError: types of i and j not matched
# hint: try type widening (e.g. ids<Int or Str or NoneType>)
ids<Int or Str or NoneType>(i, j) # OK
A and B
は以下の可能性がある。
A and B == A
:A <: B
またはA == B
のとき。A and B == B
:A :> B
またはA == B
のとき。A and B == {}
:!(A :> B)
かつ!(A <: B)
のとき。
A or B
は以下の可能性がある。
A or B == A
:A :> B
またはA == B
のとき。A or B == B
:A <: B
またはA == B
のとき。A or B
は簡約不能(独立した型):!(A :> B)
かつ!(A <: B)
のとき。
サブルーチン定義での型拡大
Ergでは、戻り値型が一致しない場合デフォルトでエラーとなる。
parse_to_int s: Str =
if not s.is_numeric():
do parse_to_int::return error("not numeric")
... # Intオブジェクトを返す
# TypeError: mismatch types of return values
# 3 | do parse_to_int::return error("not numeric")
# └─ Error
# 4 | ...
# └ Int
これを解決するためには、戻り値型を明示的にOr型と指定する必要がある。
parse_to_int(s: Str): Int or Error =
if not s.is_numeric():
do parse_to_int::return error("not numeric")
... # Intオブジェクトを返す
これは、サブルーチンの戻り値型に意図せず別の型を混入させないようにという設計である。
ただし、戻り値型の選択肢がInt
かNat
など包含関係がある型であった場合、大きい方に揃えられる。
イテレータ
イテレータは、コンテナの要素を取り出すためのオブジェクトです。
for! 0..9, i =>
print! i
このコードは0から9までの数字を出力します。
それぞれの数字(=Intオブジェクト)はi
に代入され、=>
以下の動作(=print! i
)が実行されます。このような繰り返し実行のことを イテレーション といいます。
ではここでfor!
プロシージャの型シグネチャを見てみましょう。
for!: |T: Type, I <: Iterable T| (I, T => None) => None
第一引数はIterable
という型のオブジェクトを受け付けるようです。
Iterable
は.Iterator
属性, .iter
メソッドを要求メソッドに持つ型です。
Iterable T = Trait {
.Iterator = {Iterator}
.iter = (self: Self) -> Self.Iterator T
}
.Iterator
属性の型{Iterator}
はいわゆるセットカインド(カインドはこちらで説明されています)です。
assert [1, 2, 3] in Iterable(Int)
assert 1..3 in Iterable(Int)
assert [1, 2, 3].Iterator == ArrayIterator
assert (1..3).Iterator == RangeIterator
log [1, 2, 3].iter() # <ArrayIterator object>
log (1..3).iter() # <RangeIterator object>
ArrayIterator
とRangeIterator
はどちらもIterator
を実装するクラスで、Array
, Range
にイテレーション機能を与えるためだけに存在します。
このようなデザインパターンをコンパニオンクラス1と呼びます。
そしてIteratorImpl
パッチがイテレーション機能のコアです。Iterator
は.next
メソッド1つだけを要求し、IteratorImpl
は実に数十個のメソッドを提供します。ArrayIterator
やRangeIterator
は.next
メソッドを実装するだけでIteratorImpl
の実装メソッドを使うことができるわけです。この利便性から、標準ライブラリでは多数のイテレータが実装されています。
classDiagram
class Array~T~ {
...
iter() ArrayIterator~T~
}
class Range~T~ {
...
iter() RangeIterator~T~
}
class Iterable~T~ {
<<trait>>
iter() Iterator~T~
}
Iterable~T~ <|.. Array~T~: Impl
Iterable~T~ <|.. Range~T~: Impl
class ArrayIterator~T~ {
array: Array~T~
next() T
}
class RangeIterator~T~ {
range: Range~T~
next() T
}
class Iterator~T~ {
<<trait>>
next() T
}
Iterator~T~ <|.. ArrayIterator~T~: Impl
Iterator~T~ <|.. RangeIterator~T~: Impl
Array <-- ArrayIterator
Range <-- RangeIterator
Iterable
のような、トレイト(この場合はIterator
)を静的ディスパッチでありながら統一的に扱えるインターフェースを提供する型をコンパニオンクラスアダプターと呼びます。
1 このパターンには統一された名前がないようであるが、Rustでは[companion struct pattern](https://gist.github.com/qnighy/be99c2ece6f3f4b1248608a04e104b38# :~:text=%E3%82%8F%E3%82%8C%E3%81%A6%E3%81%84%E3%82%8B%E3%80%82-,companion%20struct,-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%A8%E3%80%81%E3%81%9D%E3%81%AE)と呼ばれており、それになぞらえて命名した。↩
可変性
すでに見たように、Ergの変数は全て不変です。しかし、Ergのオブジェクトには可変性という概念があります。 以下のコードを例にします。
a = [1, 2, 3]
a = a + [4, 5, 6]
print! a # [1, 2, 3, 4, 5, 6]
上のコードは実際にはErgでは実現できません。再代入不可だからです。 このコードは実行できます。
b = ![1, 2, 3]
b.concat! [4, 5, 6]
print! b # [1, 2, 3, 4, 5, 6]
a, b
は、最終的な結果は同じように見えますが、その意味は大きく異なります。
a
はNat
の配列を示す変数ですが、1行目と2行目では指しているオブジェクトが異なります。a
という名前が同じだけで、中身はさし変わっているのです。
a = [1, 2, 3]
print! id! a # 0x000002A798DFE940
_a = a + [4, 5, 6]
print! id! _a # 0x000002A798DFE980
id!
プロシージャはオブジェクトが存在するメモリ上のアドレスを返します。
b
はNat
の「動的」配列です。オブジェクトの中身は変わりますが、変数の指すものは同じです。
b = [1,2,3].into [Int; !3]
print! id! b # 0x000002A798DFE220
b.concat! [4, 5, 6]
print! id! b # 0x000002A798DFE220
i = !0
if! True:
do! i.inc!() # or i.add!(1)
do pass
print! i # 1
!
は 可変化演算子(mutation operator) とよばれる特殊な演算子です。引数の不変オブジェクトを可変化して返します。
!
がついたオブジェクトの振る舞いはカスタム可能です。
Point = Class {.x = Int; .y = Int}
# この場合.xは可変化し、yは不変のまま
Point! = Class {.x = Int!; .y = Int}
Point!.inc_x! ref! self = self.x.update! x -> x+1
p = Point!.new {.x = !0; .y = 0}
p.inc_x!()
print! p.x # 1
定数
変数と違い、すべてのスコープで同じものを指すのが定数です。
定数は=
演算子で宣言します。
PI = 3.141592653589
match! x:
PI => print! "this is pi"
定数はグローバル以下のすべてのスコープで同一であり、上書きができません。よって、=
による再定義はできません。この制限により、パターンマッチで使うことができます。
True
やFalse
がパターンマッチで使えるのは、この2つが定数だからなのです。
また、定数は必ず不変オブジェクトを指しています。Str!
型などは定数となれません。
組み込み型がすべて定数なのは、コンパイル時に決定されているべきだからです。定数でない型も生成可能ですが、型指定には使えず、単なるレコードのようにしか使えません。逆に言えば、型はコンパイル時に内容が決定されているレコードとも言えるでしょう。
変数、名前、識別子、シンボル
ここで、Ergでの変数に関する用語を整理しておきましょう。
変数(Variable)はオブジェクトに名前(Name)をつけ、再利用できるようにする仕組み(またはその名前を指す)です。 識別子(Identifier)は変数を指定する文法要素です。 シンボルは名前を表すための文法要素、トークンです。
記号でない文字だけがシンボルであり、記号は演算子として識別子足り得ますが、シンボルとは呼びません。
例えば、x
は識別子でシンボルです。x.y
も識別子ですが、これはシンボルとは言いません。x
とy
はシンボルです。
またx
が何のオブジェクトに紐づけられていなかったとしても、x
は相変わらずSymbolかつIdentifierですが、Variableとは言いません。
x.y
という形の識別子はフィールドアクセサと言います。
また、x[y]
という形の識別子は添字アクセサと言います。
変数と識別子の違いですが、Ergの文法論的な意味での変数をいうのならば、実質この二つは同じです。 変数と識別子が等価でない言語は、C言語などがあげられます。C言語では、型や関数は変数に代入できません。int, mainは識別子ですが変数ではないのです(厳密には代入出来る場合もありますが、制約があります)。 しかし、Ergでは「全てがオブジェクト」です。関数や型は勿論、演算子でさえ変数に代入可能です。
所有権システム
ErgはPythonをホスト言語にした言語であるため、メモリ管理の方法はPythonの処理系に依存しています。 しかし、意味論的にはErgのメモリ管理はPythonのそれとは別物です。顕著な違いは、所有権システムと循環参照の禁止に現れています。
所有権
ErgはRustから影響を受けた所有権システムを持っています。 Rustの所有権システムは一般的に難解だと言われていますが、Ergのそれは直感的になるよう簡略化されています。 Ergでは 可変オブジェクト に所有権がついており、所有権を失った後はそのオブジェクトを参照できません。
v = [1, 2, 3].into [Int; !3]
push! vec, x =
vec.push!(x)
vec
# vの中身([1, 2, 3])の所有権はwに移る
w = push! v, 4
print! v # error: v was moved
print! w # [1, 2, 3, 4]
所有権の移動はオブジェクトをサブルーチンに渡したときなどに発生します。 渡した後も所有権をまだ持っていたい場合は、複製(cloning)、凍結(freeze)、または借用(borrowing)をする必要があります。 ただし、後述するように借用はできる場面が限られています。
複製
オブジェクトを複製してその所有権を移します。実引数に.clone
メソッドを適用することで行います。
複製したオブジェクトは複製元のオブジェクトと全く同一になりますが、互いに独立しているので、変更の影響は受けません。
複製はPythonのディープコピーに相当し、同一のオブジェクトをまるごと作り直すので、凍結・借用と比べて一般に計算コスト、メモリコストが高くなります。 オブジェクトを複製する必要があるようなサブルーチンは、「引数を消費する」サブルーチンといいます。
capitalize s: Str! =
s.capitalize!()
s
s1 = !"hello"
s2 = capitalize s1.clone()
log s2, s1 # !"HELLO hello"
凍結
不変オブジェクトは複数の場所から参照できることを利用して、可変オブジェクトを不変オブジェクトに変換します。
これを凍結といいます。凍結は可変配列からイテレータを作るときなどで使われます。
可変配列からは直接イテレータを作ることができないので、不変配列に変換します。
配列を壊したくない場合は、.freeze_map
メソッド等を使います。
# イテレータが出す値の合計を計算する
sum|T <: Add + HasUnit| i: Iterator T = ...
x = [1, 2, 3].into [Int; !3]
x.push!(4)
i = x.iter()
assert sum(i) == 10
y # この後もyは触れられる
借用
借用は複製や凍結よりも低コストです。 以下のような単純な場合では、借用を行えます。
peek_str ref(s: Str!) =
log s
s = !"hello"
peek_str s
借用した値は元のオブジェクトに対する 参照 と呼ばれます。 参照をまた別のサブルーチンに渡す「又貸し」はできますが、借りているだけなので消費することはできません。
steal_str ref(s: Str!) =
# log関数は引数を借用するだけなので、又貸しできる
log s
# discard関数は引数を消費するので、エラー
discard s # OwnershipError: cannot consume a borrowed value
# hint: use `clone` method
steal_str ref(s: Str!) =
# これもエラー(=は右辺を消費する)
x = s # OwnershipError: cannot consume a borrowed value
x
Ergの参照はRustより制約が強いです。参照は言語上第一級のオブジェクトですが、明示的に生成することはできず、ref
/ref!
によって実引数の渡し方として指定できるのみです。
これは、参照を配列に詰めたり参照を属性とするクラスを作ったりはできないということを意味します。
とはいえ、このような制約はそもそも参照のない言語では当たり前の仕様であり、そこまで不便となることはありません。
循環参照
Ergでは意図せずメモリリークを起こせないように設計されており、メモリーチェッカーが循環参照を検知するとエラーを出します。ほとんどの場合、このエラーは弱参照Weak
で解消できます。しかし、これでは巡回グラフなどの循環構造を持つオブジェクトを生成できないため、unsafe操作として循環参照を生成できるAPIを実装予定です。
可視性
Ergの変数には 可視性 という概念が存在します。
今まで見てきた変数は全て プライベート変数(非公開変数) と呼ばれます。これは、外部から不可視の変数です。
例えばfoo
モジュールで定義したプライベート変数は、別のモジュールから参照できないのです。
# foo.er
x = "this is an invisible variable"
# bar.er
foo = import "foo"
foo.x # AttributeError: Module 'foo' has no attribute 'x' ('x' is private)
対して、 パブリック(公開)変数 というものもあり、こちらは外部から参照できます。
公開変数は.
を付けて定義します。
# foo.er
.x = "this is a visible variable"
# bar.er
foo = import "foo"
assert foo.x == "this is a visible variable"
非公開変数には何も付ける必要はないのですが、非公開であることを明示するために::
またはself::
(型などならSelf::
)を付けることもできます。またモジュールならmodule::
とすることもできます。
::x = "this is a invisible variable"
assert ::x == x
assert self::x == ::x
assert module::x == ::x
単なる逐次実行の文脈では、プライベート変数はローカル変数とほぼ同義です。内側のスコープからは参照することが出来ます。
::x = "this is a private variable"
y =
x + 1 # 正確にはmodule::x
::
を使うことで、スコープ内の同名変数の区別ができます。
参照したい変数のスコープを左側に指定します。トップレベルの場合はmodule
を指定します。
指定しなかった場合は通常の場合と同じく最も内側の変数が参照されます。
::x = 0
assert x == 0
y =
::x = 1
assert x == 1
z =
::x = 2
assert ::x == 2
assert z::x == 2
assert y::x == 1
assert module::x == 0
無名サブルーチンのスコープではself
で自身のスコープを指定します。
x = 0
f = x ->
log module::x, self::x
f 1 # 0 1
::
は、プライベートインスタンス属性にアクセスするという役割も持っています。
x = 0
C = Class {x = Int}
C.
# トップレベルのxが参照される(module::xにするようwarningが出る)
f1 self = x
# インスタンス属性のxが参照される
f2 self = self::x
外部モジュールでの可視性
あるモジュールで定義されたクラスは、実は外部モジュールからでもメソッドを定義できます。
# foo.er
.Foo = Class()
.Bar = Class()
# bar.er
{Foo;} = import "foo"
Foo::
private self = pass
Foo.
public self = self::private()
.f() =
foo = Foo.new()
foo.public()
foo::private() # AttributeError
ただし、そのメソッドを使えるのはどちらもそのモジュール内でのみです。
外部で定義された非公開メソッドは、定義モジュール内でのみFoo
クラスのメソッドから参照できます。
公開メソッドはクラスの外には公開されますが、モジュール外までは公開されません。
# baz.er
{Foo;} = import "foo"
foo = Foo.new()
foo.public() # AttributeError: 'Foo' has no attribute 'public' ('public' is defined in module 'bar')
また、Re-exportする型にメソッドを定義することはできません。 インポート元のモジュールによってメソッドが見つかったり見つからなかったりといった混乱を防ぐためです。
# bar.er
{.Foo;} = import "foo"
.Foo::
private self = pass # Error
.Foo.
public self = self::private() # Error
このようなことを行いたい場合はパッチを定義します。
# bar.er
{Foo;} = import "foo"
FooImpl = Patch Foo
FooImpl :=:
private self = pass
FooImpl.
public self = self::private()
# baz.er
{Foo;} = import "foo"
{FooImpl;} = import "bar"
foo = Foo.new()
foo.public()
制限公開変数
変数の可視性は完全な公開・非公開しかないわけではありません。 制限付きで公開することもできます。
.
の後に[]
を付け、その中に「公開する最大の名前空間1の識別子」を指定します。
下の例では、.[.record]
は.record
の名前空間内でのみ、.[module]
はモジュール内でのみ公開されます。
# foo.er
.record = {
.a = {
.[.record]x = 0
.[module]y = 0
.z = 0
}
_ = .a.x # OK
_ = .a.y # OK
_ = .a.z # OK
}
func x =
_ = .record.a.x # VisibilityError
_ = .record.a.y # OK
_ = .record.a.z # OK
None
_ = .record.a.x # VisibilityError
_ = .record.a.y # OK
_ = .record.a.z # OK
foo = import "foo"
_ = foo.record.a.x # VisibilityError
_ = foo.record.a.y # VisibilityError
_ = foo.record.a.z # OK
名前空間はコンマ区切りで複数指定することも出来ます。
.[.record, func]x = 0
ところで、クラスのプライベート属性はサブクラスからアクセス出来ません。
C = Class {i = Int}
D = Inherit C
D.
f self = self::i # VisibilityError
あるサブクラスからアクセスできるようにしたい場合は、以下のように指定します。
C = Class {.[D]i = Int}
D = Inherit C
D.
f self = self.i
サブクラス全体に公開する場合は、.[<: Self]
とします。
これは他言語ではprotected
に相当するものです。
C = Class {.[<: C]i = Int}
1 Ergにおいて名前空間は、名前とオブジェクトの対応の集合を指す。インスタントスコープを作る変数の識別子やモジュール・関数・クラス・レコードが名前空間と同一視される。関数・クラス・レコードは識別子に束縛せずに生成することができるため、これらは本来無名名前空間を作る。しかし識別子に束縛されると、識別子と同名の名前で上書きされる。↩
命名規則
変数を定数式として使いたい場合は、必ず大文字で始めます。二文字以降は小文字でもよいです。
i: Option Type = Int
match i:
t: Type -> log "type"
None -> log "None"
副作用のあるオブジェクトは、必ず!
で終わります。プロシージャとプロシージャルメソッド、そして可変型です。
ただし、Proc
型自体は可変型ではありません。
# Callable == Func or Proc
c: Callable = print!
match c:
p! -> log "proc" # 自明なので`: Proc`を省略できる
f -> log "func"
属性を外部に公開したい場合は、初めに.
をつけて定義します。.
を初めにつけなかった場合は非公開になります。混乱を避けるため同一のスコープ内で共存はできません。
o = {x = 1; .x = 2} # SyntaxError: private and public variables with the same name cannot coexist
リテラル識別子
以上の規則は、文字列をシングルクォート('')で囲むと回避できます。すなわち、プロシージャルオブジェクトも!
をつけずに代入することができます。ただしこの場合、値が定数式でも定数とはみなされません。
このようにシングルクォートで囲まれた文字列による識別子をリテラル識別子といいます。
これは、Pythonなど他言語のAPI(FFI)を呼び出す際に使います。
bar! = pyimport("foo").'bar'
Ergでも有効な識別子の場合は、''で囲む必要はありません。
さらに、リテラル識別子中では記号も空白も入れることができるため、通常は識別子として使えない文字列を識別子として使うことができます。
'∂/∂t' y
'test 1: pass x to y'()
無名関数
無名関数は、関数オブジェクトを名付けずその場で生成するための文法です。
# `->`は無名関数演算子
# same as `f x, y = x + y`
f = (x, y) -> x + y
# same as `g(x, y: Int): Int = x + y`
g = (x, y: Int): Int -> x + y
引数が1つの場合は()
を省略できます。
assert [1, 2, 3].map_collect(i -> i + 1) == [2, 3, 4]
assert ((i, j) -> [i, j])(1, 2) == [1, 2]
下の場合0..9, (i -> ...)
であって(0..9, i) -> ...
ではありません。
->
は左辺に一つだけ引数をとります。複数の引数は一つのタプルとして受け取ります。
for 0..9, i: Int ->
...
無名関数では、空白による構文解釈の差異が存在します。
# この場合は`T(() -> Int)`と解釈される
i: T () -> Int
# この場合は(U()) -> Intと解釈される
k: U() -> Int
無名関数は引数なしでも使えます。
# `=>`は無名プロシージャ演算子
p! = () => print! "`p!` was called"
# `() ->`, `() =>`には`do`, `do!`という糖衣構文がある
# p! = do! print! "`p!` was called"
p!() # `p!` was called
引数なし関数は遅延初期化に使えます。
time = import "time"
date = import "datetime"
now = if! True:
do!:
time.sleep! 1000
date.now!()
do date.new("1970", "1", "1", "00", "00")
型付け、パターンマッチもできます。このため、match
関数はほとんど無名関数の力で実現されています。
match
関数の引数に与える無名関数は上から順番にトライされます。なので、上の方は特殊なケースを、下に行くほど一般的なケースを記述する必要があります。順番を間違えると(可能な限り)コンパイラがWarningを出します。
n = (Complex or Ratio or Int).sample!()
i = match n:
PI -> PI # 定数PIに等しい場合
(i: 1..10) -> i # 1~10のIntの場合
(i: Int) -> i # Intの場合
(c: Complex) -> c.real() # Complexの場合。Int < Complexだが、フォールバックできる
_ -> panic "cannot convert to Int" # 以上のいずれにも該当しない場合。matchは全パターンを網羅していなくてはならない
エラーハンドリングも?
かmatch
を使用して行うのが一般的です。
res: ParseResult Int
match res:
i: Int -> i
err: Error -> panic err.msg
res2: Result Int, Error
match res2:
ok: Not Error -> log Typeof ok
err: Error -> panic err.msg
無名多相関数
# same as id|T| x: T = x
id = |T| x: T -> x
サブルーチンシグネチャ
関数
some_func(x: T, y: U) -> V
some_func: (T, U) -> V
プロシージャ
some_proc!(x: T, y: U) => V
some_proc!: (T, U) => V
関数メソッド
メソッド型は、外部からはSelf
で指定できません。
.some_method(self, x: T, y: U) => ()
# Self.(T, U) => ()はselfの所有権を奪う
.some_method: (Ref Self, T, U) => ()
プロシージャメソッド(依存)
以下で、型T!
はN: Nat
という型引数を取るとします。外部から指定する場合は型変数を使用します。
K!: Nat ~> Type
# ~>は適用前後の型引数の状態を示す(このときselfは可変参照でなくてはならない)
K!(N).some_method!: (self: Ref!(K! N ~> N+X), X: Nat) => ()
注意として、.some_method
の型は|N, X: Nat| (self: Ref!(K! N ~> N+X), {X}) => ()
となります。
ref!
がついていない、すなわち適用後所有権が奪われるメソッドでは、型引数の遷移(~>
)を使用できません。
所有権が奪われる場合は以下のようになります。
# Nを使用しないならば_で省略できる
# .some_method!: |N, X: Nat| (T!(N+X), {X}) => T!(N+X)
.some_method!|N, X: Nat|(self: T!(N), X: Nat) => T!(N+X)
演算子
``で囲むことで通常の関数と同じように定義できます。
and
やor
などの中置アルファベット演算子は囲むことで中置演算子として定義できます。
and(x, y, z) = x and y and z
`_+_`(x: Foo, y: Foo) = x.a + y.a
`-_`(x: Foo) = Foo.new(-x.a)
クロージャ
Ergのサブルーチンには、外部変数を捕捉する「クロージャ」という機能があります。
outer = 1
f x = outer + x
assert f(1) == 2
不変オブジェクトと同じく、可変オブジェクトも捕捉できます。
sum = !0
for! 1..10, i =>
sum.add! i
assert sum == 45
p! x =
sum.add! x
p!(1)
assert sum == 46
しかし、関数は可変オブジェクトを捕捉できないので注意が必要です。 仮に可変オブジェクトが関数内で参照できると、以下のようなコードが書けてしまいます。
# !!! このコードは実際にはエラーになる !!!
i = !0
f x = i + x
assert f 1 == 1
i.add! 1
assert f 1 == 2
関数は同じ引数に対して同じ値を返すべきですが、その前提が破れてしまっています。
i
は呼び出し時に初めて評価されることに注意してください。
関数定義時点での可変オブジェクトの内容がほしい場合は.clone
を呼び出します。
i = !0
immut_i = i.clone().freeze()
f x = immut_i + x
assert f 1 == 1
i.add! 1
assert f 1 == 1
可変状態の回避、関数型プログラミング
# Erg
sum = !0
for! 1..10, i =>
sum.add! i
assert sum == 45
上と同等のプログラムは、Pythonでは以下のように記述できます。
# Python
sum = 0
# 手続き型スタイル
for i in range(1, 10):
sum += i
assert sum == 45
しかし、Ergではもっとシンプルな書き方を推奨します。 サブルーチンと可変オブジェクトを使って状態を持ち回す代わりに、関数を使用する状態を局所化するスタイルを使います。これは関数型プログラミングと呼ばれます。
# 関数型スタイル
sum = (1..10).sum()
assert sum == 45
上のコードは先程と全く同じ結果になりますが、こちらのほうが遥かにシンプルであることが見て取れます。
fold
関数を使用すれば、合計以外にも多様な操作を行うことができます。
fold
はイテレータのメソッドで、各イテレーションごとに引数f
を実行します。
結果を蓄積するカウンタの初期値はinit
で指定し、acc
に蓄積されていきます。
# 初期値として0から始まり、結果として45が返る
sum = (1..10).fold(init: 0, f: (acc, i) -> acc + i)
assert sum == 45
不変オブジェクトによるプログラミングで自然と簡潔な記述となるように、Ergは設計されています。
モジュール
Ergでは、ファイル自体を1つのレコードとみなすことができます。これをモジュールと呼びます。
# foo.er
.i = 1
# fooモジュールを定義するのはこのレコードを定義するのとほとんど同じ
foo = {.i = 1}
# bar.er
foo = import "foo"
print! foo # <module 'foo'>
assert foo.i == 1
モジュール型はレコード型でもあるので、分解代入が可能です。
モジュールの場合は最後の...
を省略できます。
# same as {sin; cos; ...} = import "math"
{sin; cos} = import "math"
モジュールの可視性
ファイルだけでなく、ディレクトリもモジュールとなりえます。
ただしデフォルトでErgはディレクトリをErgモジュールとしては認識しません。認識させるには、__init__.er
という名前のファイルを作成します。
__init__.er
はPythonの__init__.py
と同じようなものです。
└─┬ bar
└─ __init__.er
これで、bar
ディレクトリはモジュールとして認識されます。bar
内にあるファイルが__init__.er
だけならばあまりディレクトリ構造にする意味はありませんが、複数のモジュールを束ねて一つのモジュールとしたい場合は便利です。すなわち、このような場合です。
└─┬ bar
├─ __init__.er
├─ baz.er
└─ qux.er
bar
ディレクトリの外側からは以下のようにして使用できます。
bar = import "bar"
bar.baz.p!()
bar.qux.p!()
__init__.er
は単にディレクトリをモジュールとして機能させるだけのマーカーではなく、モジュールの可視性を制御する役割も持ちます。
# __init__.er
# `./`はカレントディレクトリを指す。なくても良い
.baz = import "./baz"
qux = import "./qux"
.f x =
.baz.f ...
.g x =
qux.f ...
外からbar
モジュールをインポートしたとき、baz
モジュールはアクセス可能ですが、qux
モジュールはアクセス不可能になります。
モジュールの循環参照
Ergでは、モジュール間の循環的な依存関係を定義することができます。
# foo.er
bar = import "bar"
print! bar.g 1
.f x = x
# bar.er
foo = "foo "をインポート
print! foo.f 1
.g x = x
しかし、手続き呼び出しによって作られた変数は、循環参照モジュールで定義することはできません。 これは、Ergが依存関係に従って定義の順番を並べ替えるからです。
# foo.er
bar = import "bar"
print! bar.x
.x = g!(1) # ModuleError: 手続き呼び出しで作られた変数は、循環参照モジュールで定義できない
# bar.er
foo = import "foo"
print! foo.x
.x = 0
また、エントリポイントであるErgモジュール(すなわち __name__ == "__main__"
であるモジュール)は循環参照の対象になることはできません。
対象体
変数に代入できる全てのデータです。Object
クラスの持つ属性は以下の通りです。
.__repr__
: オブジェクトの(リッチでない)文字列表現を返します.__sizeof__
: オブジェクトのサイズ(ヒープ確保分含む)を返します.__dir__
: オブジェクトの属性を一覧にして返します.__hash__
: オブジェクトのハッシュ値を返します.__getattribute__
: オブジェクトの属性を取得して返します.clone
: オブジェクトのクローン(メモリ上に独立な実体を持つ)を生成して返します.copy
: オブジェクトのコピー(メモリ上で同じものをさす)を返します
レコード
レコードリテラル({attr = value; ...}
)で生成されるオブジェクトです。
このオブジェクトは.clone
や.__sizeof__
などの基本的なメソッドを持ちます。
obj = {.x = 1}
assert obj.x == 1
obj2 = {*x; .y = 2}
assert obj2.x == 1 and obj2.y == 2
属性
オブジェクトと関連付けられたオブジェクトです。特に自身(self
)を暗黙の第一引数にとるサブルーチン属性はメソッド(method)と呼ばれます。
# private_attrには`.`がないことに注意が必要になる
record = {.public_attr = j; private_attr = 2; .method = self -> self.i + 1}
record.public_attr == 2
record.private_attr # AttributeError: private_attr is private
assert record.method() == 3
要素
特定の型に属するオブジェクト(e.g. 1
はInt
型の要素)です。全てのオブジェクトは、少なくとも{=}
型の要素です。
クラスの要素の場合特にインスタンス(Instance)と呼ぶこともあります。
サブルーチン
関数またはプロシージャのインスタンスであるオブジェクトを示す(メソッドも含む)。サブルーチンを表すクラスはSubroutine
です。
より一般に.__call__
を実装するオブジェクトはCallable
(呼び出し可能オブジェクト)と呼ばれます。
呼び出し可能オブジェクト
.__call__
を実装するオブジェクトです。Subroutine
のスーパークラスでもあります。
型
要求属性を定義し、オブジェクトを共通化するオブジェクトです。
大きく分けて多相型(Polymorphic Type)と単相型(Monomorphic Type)の2つがあります。典型的な単相型はInt
, Str
などで、多相型にはOption Int
, [Int; 3]
などがあります。
さらにオブジェクトの状態変更をするメソッドを定義した型は可変型(Mutable type)と呼ばれ、可変な属性に!
をつける必要があります(e.g. 動的配列: [T; !_]
)。
クラス
.__new__
, .__init__
メソッドなどを持つ型です。クラスベースのオブジェクト指向を実現します。
関数
外部変数(静的変数除く)のread権限はありますが、外部変数のread/write権限がないサブルーチンです。つまり、外部に副作用を及ぼせません。 Ergの関数(Function)は副作用を許さないので、Pythonのそれとは定義が異なります。
手続き
外部変数のread権限およびself
、静的変数のread/write権限があり、全てのサブルーチンの使用が許可されています。外部に副作用を及ぼせます。
メソッド
第一引数にself
を暗黙的にとるサブルーチンです。単なる関数/プロシージャとは別の型となっています。
エンティティ
サブルーチンおよび型ではないオブジェクトです。
単相型エンティティ(1
, "a"
など)は値オブジェクト、多相型エンティティ([1, 2, 3], {"a": 1}
)はコンテナオブジェクトとも呼ばれます。
パターンマッチ、論駁可能性
Ergで使用可能なパターン
変数パターン
# 基本的な代入
i = 1
# function
fn x = x + 1
# (無名)関数
fn = x -> x + 1
型宣言パターン
i: Int = 1
j: {1, 2, 3} = 2
(k: Int, s: Str) = 1, "a"
リテラルパターン
# もし`i`がコンパイル時に1と判断できない場合は、TypeErrorが発生する。
# `_: {1} = i`を省略したもの
1 = i
# 簡易的なパターンマッチ
match x:
1 -> "1"
2 -> "2"
_ -> "other"
# フィボナッチ関数
fib 0 = 0
fib 1 = 1
fib n: Nat = fib n-1 + fib n-2
定数パターン
cond = False
match! cond:
True => print! "cond is True"
_ => print! "cond is False"
PI = 3.141592653589793
E = 2.718281828459045
num = PI
name = match num:
PI -> "pi"
E -> "e"
_ -> "unnamed"
篩パターン
# この2つは同じ
Array(T, N: {N | N >= 3})
Array(T, N | N >= 3)
f M, N | M >= 0, N >= 1 = ...
f(1, 0) # TypeError: N (2nd parameter) must be 1 or more
破棄(ワイルドカード)パターン
_ = 1
_: Int = 1
zero _ = 0
right(_, r) = r
文脈によって制約付けられていない場合、_
はObj
型となる。
可変長パターン
後述するタプル/配列/レコードパターンと組み合わせて使います。
[i, ...j] = [1, 2, 3, 4]
assert j == [2, 3, 4]
first|T|(fst: T, ...rest: T) = fst
assert first(1, 2, 3) == 1
タプルパターン
(i, j) = (1, 2)
((k, l), _) = ((1, 2), (3, 4))
# ネストしていないなら()を省略可能(1, 2は(1, 2)として扱われる)
m, n = 1, 2
f(x, y) = ...
配列パターン
[i, j] = [1, 2]
[[k, l], _] = [[1, 2], [3, 4]]
length [] = 0
length [_, ...rest] = 1 + length rest
レコードパターン
record = {i = 1; j = 2; k = 3}
{j; ...} = record # i, kが解放される
{sin; cos; tan; ...} = import "math"
{*} = import "math" # 全てインポートする
person = {name = "John Smith"; age = 20}
age = match person:
{name = "Alice"; _} -> 7
{_; age} -> age
f {x: Int; y: Int} = ...
データクラスパターン
Point = Inherit {x = Int; y = Int}
p = Point::{x = 1; y = 2}
Point::{x; y} = p
Nil T = Class Impl := Phantom T
Cons T = Inherit {head = T; rest = List T}
List T = Enum Nil(T), Cons(T)
List T.
first self =
match self:
Cons::{head; ...} -> x
_ -> ...
second self =
match self:
Cons::{rest=Cons::{head; ...}; ...} -> head
_ -> ...
列挙パターン
※実際には単なる列挙型
match x:
i: {1, 2} -> "one or two: \{i}"
_ -> "other"
範囲パターン
※実際には単なる区間型
# 0 < i < 1
i: 0<..<1 = 0.5
# 1 < j <= 2
_: {[I, J] | I, J: 1<..2} = [1, 2]
# 1 <= i <= 5
match i
i: 1..5 -> ...
パターンではないもの、パターン化できないもの
パターンは一意に指定できるものです。この点においてパターンマッチは通常の条件分岐とは異なります。
条件の指定は一意ではありません。例えば、数n
が偶数か判定する場合、n % 2 == 0
とするのがオーソドックスですが、(n / 2).round() == n / 2
とも書けます。
一意でない形式は、正しく作動するのか、別の条件と同等であるか自明ではありません。
セット
セットのパターンはありません。なぜなら、セットは要素を一意に取り出す方法がないからです。 イテレータで取り出すことはできますが、順番は保証されません。
内包表記
[expr | (name <- iterable)+ (predicate)*]
で配列、
{expr | (name <- iterable)+ (predicate)*}
でセット、
{key: value | (name <- iterable)+ (predicate)*}
でDictが作れます。
|
で区切られた節のうち最初の部分をレイアウト節(配置節)といい、2番目の部分をバインド節(束縛節)、3番目の部分をガード節(条件節)という。
ガード節は省略可能ですがバインド節は省略できず、バインド節より先にガード節を置くことはできません。
内包表記の例
# レイアウト節はi
# バインド節はi <- [0, 1, 2]
assert [i | i <- [0, 1, 2]] == [0, 1, 2]
# レイアウト節はi / 2
# バインド節はi <- 0..2
assert [i / 2 | i <- 0..2] == [0.0, 0.5, 1.0]
# レイアウト節は(i, j)
# バインド節はi <- 0..2, j <- 0..2
# ガード節は(i + j) % 2 == 0
assert [(i, j) | i <- 0..2; j <- 0..2; (i + j) % 2 == 0] == [(0, 0), (0, 2), (1, 1), (2, 0), (2, 2)]
assert {i % 2 | i <- 0..9} == {0, 1}
assert {k: v | k <- ["a", "b"]; v <- [1, 2]} == {"a": 1, "b": 2}
Ergの内包表記はHaskellに影響を受けていますが、若干の違いがあります。 Haskellのリスト内包表記の場合、変数の順番は結果に違いをもたらしますが、Ergでは関係がありません。
-- Haskell
[(i, j) | i <- [1..3], j <- [3..5]] == [(1,3),(1,4),(1,5),(2,3),(2,4),(2,5),(3,3),(3,4),(3,5)]
[(i, j) | j <- [3..5], i <- [1..3]] == [(1,3),(2,3),(3,3),(1,4),(2,4),(3,4),(1,5),(2,5),(3,5)]
# Erg
assert [(i, j) | i <- 1..<3; j <- 3..<5] == [(i, j) | j <- 3..<5; i <- 1..<3]
この仕様はPythonのものと同じです。
# Python
assert [(i, j) for i in range(1, 3) for j in range(3, 5)] == [(i, j) for j in range(3, 5) for i in range(1, 3)]
篩型
内包表記と似たものに、篩型があります。篩型は{Name: Type | Predicate}
という形式で作られる型(列挙型)です。
篩型の場合、Nameは1つまででレイアウトは指定できず(ただしタプル型などにすれば複数の値は扱えます)、Predicateはコンパイル時計算できるもの、つまり定数式のみが指定できます。
Nat = {I: Int | I >= 0}
# 述語式がandだけの場合、;で代替できる
# Nat2D = {(I, J): (Int, Int) | I >= 0; J >= 0}
Nat2D = {(I, J): (Int, Int) | I >= 0 and J >= 0}
展開代入
分解代入において、変数の前に*
を置くと残りの要素を全てその変数に展開できます。これを展開代入と呼びます。
[x, *y] = [1, 2, 3]
assert x == 1
assert y == [2, 3]
x, *y = (1, 2, 3)
assert x == 1
assert y == (2, 3)
抽出代入
抽出代入は、モジュールやレコード内にある特定の属性をローカルに持ってくる際に便利な構文です。
{sin; cos; tan} = import "math"
このようにすると、以降はローカルでsin, cos, tan
が使用できます。
レコードでも同じようにできます。
record = {x = 1; y = 2}
{x; y} = record
全て展開したい場合は{*} = record
とします。OCamlなどでいうopen
です。
record = {x = 1; y = 2}
{*} = record
assert x == 1 and y == 2
デコレータ(修飾子)
デコレータは型や関数に特定の状態や振る舞いを追加したり明示するために使われます。 デコレータの文法は以下の通りです。
@deco
X = ...
デコレータは、競合しない限り複数つけることができます。
デコレータは特別なオブジェクトではなく、その実体は単なる1引数関数です。デコレータは以下の疑似コードと等価です。
X = ...
X = deco(X)
Ergでは変数の再代入が出来ないので、上のようなコードは通りません。
単なる変数の場合はX = deco(...)
と同じなのですが、インスタントブロックやサブルーチンの場合はそうすることができないので、デコレータが必要になってきます。
@deco
f x =
y = ...
x + y
# コードが横長になるのを防ぐこともできる
@LongNameDeco1
@LongNameDeco2
C = Class ...
以下に、頻出の組み込みデコレータを紹介します。
Inheritable
定義する型が継承可能クラスであることを示します。引数scope
に"public"
を指定すると、外部モジュールのクラスでも継承できるようになります。デフォルトでは"private"
になっており、外部からは継承できません。
Final
メソッドをオーバーライド不能にします。クラスに付けると継承不能クラスになりますが、デフォルトなので意味はありません。
Override
属性をオーバーライドする際に使用します。Ergではデフォルトで基底クラスと同じ属性を定義しようとするとエラーになります。
Impl
引数のトレイトを実装することを示します。
Add = Trait {
.`_+_` = Self.(Self) -> Self
}
Sub = Trait {
.`_-_` = Self.(Self) -> Self
}
C = Class({i = Int}, Impl := Add and Sub)
C.
@Impl Add
`_+_` self, other = C.new {i = self::i + other::i}
@Impl Sub
`_-_` self, other = C.new {i = self::i - other::}
Attach
トレイトにデフォルトで付属するアタッチメントパッチを指定します。 これによって、Rustのトレイトと同じ挙動を再現できます。
# foo.er
Add R = Trait {
.AddO = Type
.`_+_` = Self.(R) -> Self.AddO
}
@Attach AddForInt, AddForOdd
ClosedAdd = Subsume Add(Self)
AddForInt = Patch(Int, Impl := ClosedAdd)
AddForInt.AddO = Int
AddForOdd = Patch(Odd, Impl := ClosedAdd)
AddForOdd.AddO = Even
こうすると、他のモジュールからトレイトをインポートした際に、アタッチメントパッチが自動で適用されます。
# 本来IntIsBinAdd, OddIsBinAddも同時にインポートする必要があるが、アタッチメントパッチなら省略できる
{BinAdd;} = import "foo"
assert Int.AddO == Int
assert Odd.AddO == Even
内部的にはトレイトの.attach
メソッドを使って結びつけているだけです。コンフリクトする場合はトレイトの.detach
メソッドで外すことができます。
@Attach X
T = Trait ...
assert X in T.attaches
U = T.detach(X).attach(Y)
assert X not in U.attaches
assert Y in U.attaches
Deprecated
変数の仕様が古く非推奨であることを示します。
Test
テスト用サブルーチンであることを示します。テスト用サブルーチンはerg test
コマンドで実行されます。
エラーハンドリングシステム
主にResult型を使用します。 ErgではError型オブジェクトを捨てる(トップレベルで対応しない)とエラーが発生します。
例外、Pythonとの相互運用
Ergは例外機構(Exception)を持ちません。Pythonの関数をインポートする際は
- 戻り値を
T or Error
型とする T or Panic
型(実行時エラーを出す可能性がある)とする
の2つの選択肢があり、pyimport
ではデフォルトで後者となる。前者としてインポートしたい場合は、
pyimport
のexception_type
でError
を指定する(exception_type: {Error, Panic}
)。
例外とResult型
Result
型はエラーかもしれない値を表現します。Result
によるエラーハンドリングはいくつかの点で例外機構よりも優れています。
まず第一に、サブルーチンがエラーを出すかもしれないと型定義から分かり、実際に使用するときも一目瞭然です。
# Python
try:
x = foo().bar()
y = baz()
qux()
except e:
print(e)
上の例では、例外がどの関数から送出されたものなのか、このコードだけでは分かりません。関数定義まで遡っても、その関数が例外を出すかを判別するのは難しいです。
# Erg
try!:
do!:
x = foo!()?.bar()
y = baz!()
qux!()?
e =>
print! e
反対に、こちらの例ではfoo!
とqux!
がエラーを出しうるとわかります。
正確にはy
もResult
型である可能性がありますが、中の値を使用するためにはいずれ対処しなくてはなりません。
Result
型を使用するメリットはそれだけではありません。Result
型はスレッドセーフでもあります。これは、エラー情報を並列実行中に(容易に)受け渡しできるということを意味します。
Context
Error
/Result
型単体では副作用が発生しないので、例外と違い送出場所などの情報(Context、文脈)を持てませんが、.context
メソッドを使えばError
オブジェクトに情報を付加できます。.context
メソッドはError
オブジェクト自身を消費して新しいError
オブジェクトを作るタイプのメソッドです。チェイン可能であり、複数のコンテクストを保持できます。
f() =
todo() \
.context "to be implemented in ver 1.2" \
.context "and more hints ..."
f()
# Error: not implemented yet
# hint: to be implemented in ver 1.2
# hint: and more hints ...
なお、.msg
や.kind
などのError
の属性は副次的なものではないのでcontextではなく、最初に生成されたときのまま上書きできません。
スタックトレース
Result
型はその利便性から他言語でも多く取り入れられていますが、例外機構と比較してエラーの発生元がわかりにくくなるというデメリットがあります。
そこで、ErgではError
オブジェクトに.stack
という属性を持たせており、擬似的に例外機構のようなスタックトレースを再現しています。
.stack
は呼び出し元オブジェクトの配列です。Errorオブジェクトはreturn
(?
によるものも含む)されるたびにその呼出元サブルーチンを.stack
に積んでいきます。
そしてreturn
ができないコンテクストで?
されるなり.unwrap
されるなりすると、トレースバックを表示しながらパニックします。
f x =
...
y = foo.try_some(x)?
...
g x =
y = f(x)?
...
i = g(1)?
# Traceback (most recent call first):
# ...
# Foo.try_some, line 10, file "foo.er"
# 10 | y = foo.try_some(x)?
# module::f, line 23, file "foo.er"
# 23 | y = f(x)?
# module::g, line 40, file "foo.er"
# 40 | i = g(1)?
# Error: ...
パニック
Ergには回復不能なエラーへの対処として パニッキング という機構も存在します。 回復不能なエラーとは、例えばソフト/ハードウェアの不具合など外的要因によるエラーや、それ以上コードを実行し続けても意味がないほど致命的なエラー、あるいはプログラム作成者の想定だにしないエラーなどです。これが発生した場合、プログラマの努力によって正常系に復帰させることができないため、その場でプログラムを終了させます。これを「パニックさせる」といいます。
パニックはpanic
関数で行います。
panic "something went wrong!"
パイプライン演算子
パイプライン演算子は、次のように使います。
assert f(g(x)) == (x |> g() |> f())
assert f(g(x, y)) == (x |> g(y) |> f())
つまり、Callable(object)
という順序をobject |> Callable()
に変えられます。
パイプライン演算子はメソッドに対しても使えます。メソッドの場合、object.method(args)
がobject |>.method(args)
と変わります。
単に|>
が増えただけにも見えるが、結合強度が低めなので()
の量を減らせる場合があります。
rand = -1.0..1.0 |>.sample!()
log rand # 0.2597...
1+1*2 |>.times do log("a", end := "") # aaa
evens = 1..100 |>.iter() |>.filter i -> i % 2 == 0 |>.collect Array
# パイプライン演算子を使わずに実装する場合、
_evens = (1..100).iter().filter(i -> i % 2 == 0).collect(Array)
# または
__evens = 1..100 \
.iter() \
.filter i -> i % 2 == 0 \
.collect Array
Pythonとの連携
Pythonへのexport
Ergスクリプトをコンパイルすると.pycファイルが生成されますが、これは単純にPythonのモジュールとして読み込むことができます。 ただし、Erg側で非公開に設定した変数はPythonからもアクセスできません。
# foo.er
.public = "this is a public variable"
private = "this is a private variable"
erg --compile foo.er
import foo
print(foo.public)
print(foo.private) # AttributeError:
Pythonからのimport
Pythonから取り込んだオブジェクトはデフォルトですべてObject
型になります。このままでは比較もできないので、型の絞り込みを行う必要があります。
標準ライブラリの型指定
Python標準ライブラリにあるAPIは、すべてErg開発チームにより予め型が指定されています。なので、pyimport
でそのまま呼び出すことが出来ます。
time = pyimport "time"
time.sleep! 1
ユーザースクリプトの型指定
Pythonスクリプトの型ヒント(type hint)をErgは関知しません。
Pythonのfoo
モジュールに型を付けるfoo.d.er
ファイルを作成します。
# foo.py
X = ...
def bar(x):
...
def baz():
...
class C:
...
# foo.d.er
.X: Int
.bar!: Int => Int
.foo! = baz!: () => Int # aliasing
.C!: Class
d.er
内では宣言と定義(エイリアシング)以外の構文は使えません。
Pythonの関数はすべてプロシージャとして、クラスはすべて可変クラスとしてしか登録できないことに注意してください。
foo = pyimport "foo"
assert foo.bar!(1) in Int
これは、実行時に型チェックを行うことで型安全性を担保しています。チェック機構は概念的には以下のように動作します。
decl_proc proc!: Proc, T =
x =>
assert x in T.Input
y = proc!(x)
assert y in T.Output
y
これは実行時オーバーヘッドとなるので、PythonスクリプトをErgの型システムで静的に型解析するプロジェクトが進められています。
パッケージシステム
Ergのパッケージはアプリケーションであるappパッケージとライブラリであるlibパッケージに大別できます。
appパッケージのエントリポイントはsrc/app.er
です。app.er
内に定義されたmain
関数が実行されます。
libパッケージのエントリポイントはsrc/lib.er
です。パッケージをインポートすることはlib.er
をインポートすることと等価になります。
パッケージにはモジュールという下位構造があります。Ergにおいてモジュールとはすなわち、Ergファイルもしくはそれで構成されたディレクトリです。外部のErgファイル/ディレクトリはモジュールオブジェクトとして操作可能な対象になるのです。
ディレクトリをモジュールとして認識させるには、ディレクトリ内に__init__.er
ファイルを置く必要があります。
これはPythonの__init__.py
と同じようなものです。
例として、以下のようなディレクトリ構成を考えてみましょう。
└─┬ ./src
├─ app.er
├─ foo.er
└─┬ bar
├─ __init__.er
├─ baz.er
└─ qux.er
app.er
ではfoo
モジュールとbar
モジュールをインポートできます。bar
ディレクトリがモジュールとして認識できるのは__init__.er
ファイルがあるためです。
foo
モジュールはファイルからなるモジュールで、bar
モジュールはディレクトリからなるモジュールです。bar
モジュールはさらにbaz
, qux
モジュールを内部に持ちます。
このモジュールは単にbar
モジュールの属性であり、app.er
からは以下のようにアクセスできます。
# app.er
foo = import "foo"
bar = import "bar"
baz = bar.baz
# または`baz = import "bar/baz"`
main args =
...
サブモジュールにアクセスするための区切り文字が/
であることに注意してください。これは、bar.baz.er
のようなファイル名があり得るためです。
しかしこのようなファイル名は推奨されません。Ergでは.er
の直前の識別子、プレフィックスが意味を持つためです。
例えば、テスト用のモジュールです。.test.er
で終わるファイルは(ホワイトボックス)テスト用のモジュールであり、テスト実行時に@Test
でデコレーションされたサブルーチンが実行されます。
└─┬ ./src
├─ app.er
├─ foo.er
└─ foo.test.er
# app.er
foo = import "foo"
main args =
...
また、__init__.er
内でre-importされていないモジュールはプライベートモジュールであり、同一ディレクトリ内のモジュールからしかアクセスできません。
└─┬
├─ foo.er
└─┬ bar
├─ __init__.er
├─ baz.er
└─ qux.er
# __init__.py
.qux = import "qux" # this is public
# foo.er
bar = import "bar"
bar.qux
bar.baz # AttributeError: module 'baz' is private
# qux.er
baz = import "baz"
ジェネレータ
ジェネレータは、ブロック中でyield!
プロシージャを使う特殊なプロシージャです。
g!() =
yield! 1
yield! 2
yield! 3
yield!
はサブルーチンのブロックで定義されるプロシージャで、self!.yield!
を呼び出します。
これはreturn
と同じく渡された値を戻り値として返すものですが、その時点でのブロックの実行状態を保存し、もう一度呼び出された際に続きから実行するという特徴があります。
ジェネレータはプロシージャでありながらイテレータでもあります。Pythonのジェネレータはイテレータを生成する関数ですが、Ergは直接イテレートします。プロシージャ自体は一般に可変オブジェクトではありません(!
が付かない)が、ジェネレータは実行ごとに自身の内容が変わる得るので可変オブジェクトです。
# Generator! < Proc
g!: Generator!((), Int)
assert g!() == 1
assert g!() == 2
assert g!() == 3
Pythonスタイルのジェネレータは以下のようにして定義できます。
make_g() = () =>
yield! 1
yield! 2
yield! 3
make_g: () => Generator!((), Int)
Ergの文法一覧 (ver 0.1.0, provisional)
special_op ::= '=' | '->' | '=>' | '.' | ',' | ':' | '::' | '|>' | '&'
separator ::= ';' | '\n'
escape ::= '\'
comment_marker ::= '#'
reserved_symbol ::= special_op | separator | comment_marker
number ::= [0-9]
first_last_dight ::= number
dight ::= number | '_'
bin_dight ::= [0-1]
oct_dight ::= [0-8]
hex_dight ::= [0-9]
| [a-f]
| [A-F]
int ::= first_last_dight
| first_last_dight dight* first_last_dight
| '0' ('b' | 'B') binary_dight+
| '0' ('o' | 'O') octa_dight+
| '0' ('x' | 'X') hex_dight+
ratio ::= '.' dight* first_last_dight
| first_last_dight dight* '.' dight* first_last_dight
bool ::= 'True' | 'False'
none ::= 'None'
ellipsis ::= 'Ellipsis'
not_implemented ::= 'NotImplemented'
parenthesis ::= '(' | ')'
bracket ::= '{' | '}'
square_bracket ::= '[' | ']'
enclosure ::= parenthesis | bracket | square_bracket
infix_op ::= '+' | '-' | '*' | '/' | '//' | '**'
| '%' | '&&' | '||' | '^^' | '<' | '<=' | '>' | '>='
| 'and' | 'or' | 'is' | 'as' | 'isnot' | 'in' | 'notin' | 'dot' | 'cross'
prefix_op ::= '+' | '-' | '*' | '**' | '..' | '..<' | '~' | '&' | '!'
postfix_op ::= '?' | '..' | '<..'
operator ::= infix_op | prefix_op | postfix_op
char ::= /* ... */
str ::= '\"' char* '\"
symbol_head ::= /* char except dight */
symbol ::= symbol_head /* char except (reserved_symbol | operator | escape | ' ') */
subscript ::= accessor '[' expr ']'
attr ::= accessor '.' symbol
accessor ::= symbol | attr | subscript
literal ::= int | ratio | str | bool | none | ellipsis | not_implemented
pos_arg ::= expr
kw_arg ::= symbol ':' expr
arg ::= pos_arg | kw_arg
enc_args ::= pos_arg (',' pos_arg)* ','?
args ::= '()' | '(' arg (',' arg)* ','? ')' | arg (',' arg)*
var_pattern ::= accessor | `...` accessor | '[' single_patterns ']'
var_decl_opt_t = var_pattern (':' type)?
var_decl = var_pattern ':' type
param_pattern ::= symbol | `...` symbol | literal | '[' param_patterns ']'
param_decl_opt_t = param_pattern (':' type)?
param_decl = param_pattern ':' type
params_opt_t ::= '()' (':' type)?
| '(' param_decl_opt_t (',' param_decl_opt_t)* ','? ')' (':' type)?
| param_decl_opt_t (',' param_decl_opt_t)*
params ::= '()' ':' type
| '(' param_decl (',' param_decl)* ','? ')' ':' type
subr_decl ::= accessor params
subr_decl_opt_t ::= accessor params_opt_t
decl ::= var_decl | subr_decl
decl_opt_t = var_decl_opt_t | subr_decl_opt_t
body ::= expr | indent line+ dedent
def ::= ('@' decorator '\n')* decl_opt_t '=' body
call ::= accessor args | accessor call
decorator ::= call
lambda_func ::= params_opt_t '->' body
lambda_proc ::= params_opt_t '=>' body
lambda ::= lambda_func | lambda_proc
normal_array ::= '[' enc_args ']'
array_comprehension ::= '[' expr | (generator)+ ']'
array ::= normal_array | array_comprehension
record ::= '{' '=' '}'
| '{' def (';' def)* ';'? '}'
set ::= '{' '}'
| '{' expr (',' expr)* ','? '}'
dict ::= '{' ':' '}'
| '{' expr ':' expr (',' expr ':' expr)* ','? '}'
tuple ::= '(' ')'
| '(' expr (',' expr)* ','? ')'
indent ::= /* ... */
expr ::= accessor | literal
| prefix | infix | postfix
| array | record | set | dict | tuple
| call | def | lambda
line ::= expr separator+
program ::= expr? | (line | comment)*
索引
この索引にないAPIについてはこちらを参照してください。
用語の意味についてはこちらを参照。
記号
- ! → side effect
- !-type → mutable type
- ? → error handling
- # → コメント
- $ → shared
- %
- &
- &&
- ′ (single quote)
- " (double quote)
- () → Tuple
- *
- + (前置) → operator
- +_ → + (前置)
- + (中置) → operator
- + (中置) → Trait
- ,
- − (前置)
- −_ → − (前置)
- − (中置) → operator
- − (中置) → Trait
- −> → anonymous function
- . → Visibility
- /
- :
- : → Colon application style
- : → Declaration
- : → Keyword Arguments
- :: → visibility
- := → default parameters
- ;
- <
- <: → Subtype specification
- <<
- <=
- = → Variable
- ==
- => → procedure
- >
- >>
- >=
- @ → decorator
- [] → Array
- \ → Indention
- \ → Str
- ^
- ^^
- _ → Type erasure
- _+_ → + (infix)
- _-_ → − (infix)
- `` (back quote)
- {}
- {:}
- {=} → Type System
- |
- || → Type variable list
- ~
アルファベット
A
- [Add]
- alias
- Aliasing
- algebraic type
- [And]
- [and]
- anonymous function
- Anonymous polycorrelation coefficient
- anonymous type → Type System
- Array
- [assert]
- Attach
- attribute
- Attribute definitions
- Attribute Type
B
C
- Cast
- Comments
- Complex Object
- Compile-time functions
- circular references
- Class
- Class Relationship
- Class upcasting
- Colon application style
- Closure
- Compound Literals
- Complement
- Comprehension
- constant
- Constants
- Context
D
- Data type
- Declaration
- decorator
- Default parameters
- Del
- Dependent Type
- Deconstructing a record
- Deprecated
- Dict
- Diff
- Difference from Data Class
- Difference from structural types
- distinct
- Downcasting
E
- Empty Record
- Enum Class
- Enum type
- Enumerated, Interval and Refinement Types
- error handling
- Existential type
- Exponential Literal
- Extract assignment
F
- False → Boolean Object
- Float&sbsp;Object
- for
- For-All Patch
- For all types
- freeze
- Function
- Function definition with multiple patterns
G
H
I
- id
- if
- import
- impl
- [in]
- Indention
- Instant Block
- Instance and class attributes
- Implementing and resolving duplicate traits in the API
- inheritable
- inheritance
- Inheritance of Enumerated Classes
- Int
- Integration with Python
- Interval Type
- Intersection
- Iterator
J
K
L
- lambda → anonymous function
- let-polymorphism → [rank 1 polymorphism]
- Literal Identifiers
- log → side effect
M
- [match]
- Marker Trait
- Method
- Modifier → decorator
- module
- Multiple Inheritance
- Multi-layer (multi-level) Inheritance
- Mutable Type
- Mutable Structure Type
- Mutability
N
- Nat
- [Never]
- New type
- Heterogeneous Dict
- None → [None Object]
- [None Object]
- Nominal Subtyping → Class
- [Not]
- [not]
O
- Object
- [Option]
- [Or]
- [or]
- [Ord]
- ownership system
- Overloading
- Overriding
- Override in Trait
P
- Panic
- Patch
- Pattern match
- Phantom class
- pipeline operator
- Predicate
- [print!]
- Procedures
- Projection Type
- Python → Integration with Python
Q
R
- Range Object
- [ref]
- [ref!]
- Record
- Recursive functions
- Refinement pattern
- Refinement Type
- replication
- Replacing Traits
- Result → error handling
- Rewriting Inherited Attributes
- rootobj
S
- Script
- Selecting Patches
- self
- Self
- Shared Reference
- side-effect
- Smart Cast
- Spread assignment
- special type variables
- Stack trace
- Structure type
- Structural Patch
- Structural Trait
- Structural Subtyping
- Structural types and class type relationships
- Str
- Subtyping
- Subtyping of subroutines
- Subtype specification
- Subtyping of Polymorphic Function Types
- Subroutine Signatures
T
- Test
- Traits
- Trait inclusion
- True → Boolean Object
- True Algebraic type
- [Type]
- type
- Type arguments in method definitions
- Type Bound
- Type Definitions
- Type erasure
- Type Inference System
- Type specification
- Type System
- Type Widening
- Tuple
U
V
W
- [while]
X
Y
Z
あ行
- [アサーション]
- 値オブジェクト
- アタッチメントパッチ
- アドホック多相 → オーバーロードの禁止
- アトリビュート → [属性]
- アリティ
- 依存型
- イミュータブル → [不変]
- 引数(いんすう) → [引数(ひきすう)]
- インスタンス
- インスタントブロック
- インデックス
- インデント
- エイリアス
- エラー
- [エラーハンドリング]
- 演算子
- [演算子の結合強度]
- オーバーライド
- オーバーロードの禁止
- オフサイドルール → インデント
- [オブジェクト]
- オブジェクト指向
- オペランド → 被演算子
- オペレーター → 演算子
か行
- カインド
- [可視性]
- [型]
- [ガード]
- カプセル化
- [可変]
- [可変オブジェクト]
- [可変型]
- [可変参照]
- [可変配列]
- [可変長引数]
- 関数
- 基底型
- 記名
- キャプチャ → [クロージャ]
- [共変]
- [キーワード引数]
- 空集合 → [{}]
- 区間
- 区間型
- 区間演算子
- 組み込み
- [組み込み型]
- 組み込み関数
- 組み込みプロシージャ
- クラス
- [クロージャ]
- [グローバル変数]
- [クローン]
- 継承
- 高階
- 高階カインド
- 高階型
- 高階関数
- [公開変数]
- [構造的部分型]
後方参照→ [前方参照]- [コピー]
- コメント
- コレクション
- コロン → [:]
- コンストラクタ
- コンテナ
- コンパイラ
- コンパイル時計算
- コンマ → [,]
さ行
- 再帰
- 再帰型
- 再帰関数
- サブスクリプト → [インデックス]
- サブタイピング多相
- サブルーチン
- 参照
- 参照オブジェクト
- 参照カウント(RC)
- 参照等価性 → 副作用
- 識別子
- シグネチャ
- 型シグネチャ
- 辞書
- [自然数] → [Nat]
- ジェネリクス → [全称型]
- ジェネレータ
- [射影型]
- 借用 → 参照
- シャドーイング
- 種 → カインド
- [集合] → [セット]
- 述語
- [述語関数]
- 条件分岐
- [所有権]
- 真偽型 → [Bool]
- シングルトン
- [シンボル] → 識別子
- [シンボル化]
- スクリプト
- スコープ
- スプレッド演算子 → [展開代入]
- スライス
- 制御文字
- [整数] → [Int]
- セット
- セミコロン → [;]
- 宣言
- 全称
- 全称型 → 多相型
- 閉じた全称型
- 開いた全称型
- 全称関数 → 多相関数
- 全称量化
- 全称型 → 多相型
- 前置演算子
- 相互再帰
- 添字 → [インデックス]
- [属性]
- [属性的部分型]
た行
- 代数
- 代数演算型
- 代数的データ型
- 代入
- 多重
- 多重継承
- 多重代入
- 多重定義 → [オーバーロードの禁止]
- 多相
- 多相型
- 多相関数
- 多態 → [ポリモーフィズム]
- ダックタイピング
- タプル
- 単相
- 単相化
- 単相型
- 単相関数
- [遅延初期化]
- 抽出代入
- 抽象構文木 → [AST]
- 中置演算子
- 定数
- [定義]
- 提供属性
- [適用]
- デコレータ
- デストラクタ
- 手続き → プロシージャ
- デフォルト引数
- 展開
- [展開演算子]
- [展開代入]
- 特殊形式
- 匿名関数 → 無名関数
- ドット演算子(
.
) → [属性参照] - トップ
- トップ型 → [Structural Object]
- トップクラス → [Object]
- トレイト
な行
- 内包表記
中置(なかおき)演算子→ [中置(ちゅうち)演算子]- [名前空間]
は行
- 配列
- 派生型
- パターン(マッチ)
- パッケージ
- ハッシュマップ → 辞書
- パッチ
- パブリック変数 → 公開変数
- パラメーター → 引数
- パラメトリック多相
- 反変
- 比較
- [比較演算子]
- [比較可能型]
- 非公開変数
- 標準
- 標準出力
- 標準入力
- 標準ライブラリ
- 副作用
- 複素数 → [Complex]
- [浮動小数点数] → [Float]
- プライベート変数 → [非公開変数]
- ブール代数 → [Bool]
- プロシージャ
- 引数
- 部分型付け → [サブタイピング]
- [不変]
- [不変オブジェクト]
- [不変型]
- [不変参照]
- 篩型
- [ブロック]
- 分解代入
- 変数
- ボトム
- ボトム型 → [{}]
- ボトムクラス → [Never]
- [ポリモーフィズム]
ま行
や行
- 幽霊型
- 要求属性
- [要素]
- [呼び出し]
ら行
わ行
- ワイルドカード
クイックツアー
syntax
以下のドキュメントは、概ねプログラミング初心者でも理解できることを目指して書かれています。
すでにPythonやRust, Haskellなどの言語を習得されている方にとっては、少し冗長であるかもしれません。
そこで以下では概説的にErgの文法を紹介します。 特に言及のない部分はPythonと同じと考えてください。
基本的な計算
ergプログラムは、コンパイル時にすべて厳密に型付けされています。しかし、型はクラスやトレイトによって部分型であると判断できる場合には自動的にキャスティングされます(詳細についてはNumを参照してください)。
よって、例えば数値型であれば異なる型同士でも計算をすることもできます。
a = 1 # 1: Nat(Int)
b = a - 10 # -9: Int
c = b / 2 # -4.5: Float
d = c * 0 # -0.0: Float
e = f // 2 # 0: Nat(Int)
意図しない型の拡大を許容したくない場合には、宣言時に型を指定することでコンパイル時にエラーとして検出できます。
a = 1
b: Int = a / 2
# error message
Error[#0047]: File <stdin>, line 1, in <module>
2│ b: Int = a / 2
^
TypeError: the type of b is mismatched:
expected: Int
but found: Float
真偽値型
True、Falseは真偽値型のシングルトンですが、整数型を汎化型として持ちます。
そのため、整数型や整数の部分型にあたる自然数型、それらの汎化型になる有理数型や浮動小数点数型で比較をすることができます。
ただし、浮動小数点数型はEq
を汎化型に持たないため、==
を使うとコンパイルエラーになります。
True == 1 # OK
False == -1 # OK
True == 1.0 # OK
True < 2.0f64 # OK
True == 0.0f64 # NG
True == "a" # NG
変数、定数
変数は=
で定義します。Haskellなどと同じように、一度定義した変数は書き換えられません。ただし別のスコープではシャドーイングできます。
i = 0
if True:
i = 1
assert i == 0
大文字で始まるものは定数です。コンパイル時計算できるものだけが定数にできます。 また、定数は定義以降すべてのスコープで同一です。この性質により、パターンマッチで定数を使うことができます。
random = import "random" # pythonの標準ライブラリをインポートできる
PI = 3.141592653589793
match random.random!(0..10):
PI ->
log "You get PI, it's a miracle!"
宣言
Pythonと違い、変数の型のみを先に宣言することが可能です。 当然、宣言の型と実際に代入されるオブジェクトの型は互換していなくてはなりません。
i: Int
i = 10 # OK
i = 1.0 # NG
関数
概ねHaskellと同じように定義できます。
fib 0 = 0
fib 1 = 1
fib n = fib(n - 1) + fib(n - 2)
無名関数は以下のように定義できます。
i -> i + 1
assert [1, 2, 3].map(i -> i + 1).to_arr() == [2, 3, 4]
演算子
Erg独自の演算子は以下の通りです。
可変化演算子(!)
Ocamlのref
のようなものです。
i = !0
i.update! x -> x + 1
assert i == 1
プロシージャ
副作用のあるサブルーチンはプロシージャと呼ばれ、!
がついています。
副作用のないサブルーチンは関数と呼びます。
プロシージャを関数中で呼び出すことはできません。
これにより、副作用は明示的に分離されます。
print! 1 # 1
ジェネリック関数(多相関数)
id|T|(x: T): T = x
id(1): Int
id("a"): Str
レコード
ML系言語にあるレコード(あるいはJSのオブジェクトリテラル)に相当するものを利用できます。
p = {x = 1; y = 2}
assert p.x == 1
所有権
Ergは可変オブジェクト(!
演算子で可変化したオブジェクト)に所有権がついており、複数の場所からは書き換えられません。
i = !0
j = i
assert j == 0
i # MoveError
対して不変オブジェクトは複数の場所から参照できます。
可視性
変数の頭に.
をつけると、その変数は公開変数となり、外部モジュールから参照できるようになります。
# foo.er
.x = 1
y = 1
foo = import "foo"
assert foo.x == 1
foo.y # VisibilityError
パターンマッチ
変数パターン
# 基本的な代入
i = 1
# 型注釈付き
i: Int = 1
# 関数の場合
fn x = x + 1
fn: Int -> Int = x -> x + 1
リテラルパターン
# もし`i`がコンパイル時に1であることが確定していないならば、TypeErrorが発生する。
# `_: {1} = i`の省略形
1 = i
# 簡単なパターンマッチング
match x:
1 -> "1"
2 -> "2"
_ -> "other"
# フィボナッチ関数
fib 0 = 0
fib 1 = 1
fib n: Nat = fib n-1 + fib n-2
定数パターン
PI = 3.141592653589793
E = 2.718281828459045
num = PI
name = match num:
PI -> "pi"
E -> "e"
_ -> "unnamed"
破棄(ワイルドカード)パターン
_ = 1
_: Int = 1
right(_, r) = r
可変長パターン
後述するタプル/配列/レコードパターンと組み合わせて使う。
[i, *j] = [1, 2, 3, 4]
assert j == [2, 3, 4]
first|T|(fst: T, *rest: T) = fst
assert first(1, 2, 3) == 1
タプルパターン
(i, j) = (1, 2)
((k, l), _) = ((1, 2), (3, 4))
# ネストしていないなら()を省略可能(1, 2は(1, 2)として扱われる)
m, n = 1, 2
配列パターン
length [] = 0
length [_, *rest] = 1 + length rest
レコードパターン
{sin; cos; tan; ...} = import "math"
{*} = import "math" # 全てインポートする
person = {name = "John Smith"; age = 20}
age = match person:
{name = "Alice"; _} -> 7
{_; age} -> age
データクラスパターン
Point = Inherit {x = Int; y = Int}
p = Point::{x = 1; y = 2}
Point::{x; y} = p
内包表記
odds = [i | i <- 1..100; i % 2 == 0]
クラス
Ergでは多重継承をサポートしていません。
クラスはデフォルトで継承不可能であり、継承可能クラスを定義する際はInheritable
デコレータをつけます。
@Inheritable
Point2D = Class {x = Int; y = Int}
Point3D = Inherit Point2D, Base := {x = Int; y = Int; z = Int}
トレイト
Rustのトレイトと似ていますが、より本来の意味に近いもので、合成や分離ができ、属性とメソッドは対等に扱われます。 また、実装を伴いません。
XY = Trait {x = Int; y = Int}
Z = Trait {z = Int}
XYZ = XY and Z
Show = Trait {show: Self.() -> Str}
@Impl XYZ, Show
Point = Class {x = Int; y = Int; z = Int}
Point.
...
パッチ
クラスやトレイトに後付けで実装を与えたりできます。
Invert = Patch Bool
Invert.
invert self = not self
assert False.invert()
篩型
述語式で型に制限をかけられます。
Nat = {I: Int | I >= 0}
値を含むパラメトリック型(依存型)
a: [Int; 3]
b: [Int; 4]
a + b: [Int; 7]