Ergの型システム

badge

以下では、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(代数演算型)があります。

RecordEnumIntervalUnionIntersectionDiff
kindAttributiveRefinementRefinementAlgebraicAlgebraicAlgebraic
generatorrecordsetrange operatoror operatorand operatornot operator

記名的部分型(Nominal subtyping, NST)を使用することもでき、SST型のNST型への変換を型の記名化(Nominalization)と呼びます。こうしてできた型を記名型(Nominal type)と呼びます。 Ergでは、記名型はクラスとトレイトがそれに該当します。単にクラス/トレイトといった場合、それはレコードクラス/レコードトレイトを指す場合が多いです。

TypeAbstractionSubtyping procedure
NSTNominalTypeTraitInheritance
SSTStructuralTypeStructural Trait(Implicit)

記名型全体を表す型(NominalType)と構造型全体の型(StructuralType)は型全体の型(Type)のサブタイプです。

Ergは型定義に引数(型引数)を渡すことができます。型引数を持つOption, Arrayなどを多項カインドと呼びます。これら自体は型ではありませんが、引数を適用することで型となります。また、引数を持たないInt, Str型などを単純型(スカラー型)と呼びます。

型は集合とみなすことができ、包含関係も存在します。例えばNumAddSubなどを含んでおり、IntNatを含んでいます。 全てのクラスの上位クラスは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はいわゆる型引数で、IntStrなど実装のある型(クラス)が入れられます。他の言語で型引数には特別な記法(ジェネリクス、テンプレートなど)が与えられていますが、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であるために1Ratioにダウンキャストされるからです。 しかしこれはキャストされません。

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}IntNatなどの部分型となる列挙型です。 列挙型などには名前を付けて要求/実装メソッドを付けられます。その型にアクセスできる名前空間では、要求を満たすオブジェクトは実装メソッドを使用できます。

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が要素の型です。01両方を持っているInt型の部分型とも言えます。 {}のようなオブジェクトはそれ自体が型であり、上のように変数に代入して使ってもよいし、代入せずに使うこともできます。 このような型を構造型といいます。クラス(記名型)と対比して後者としての使い方を強調したいときは無名型ともいいます。{0, 1}のような種類の構造型は列挙型と呼ばれ、他に区間型、レコード型などがあります。

型同一性

下のような指定はできません。Addはそれぞれ別のものを指すと解釈されるからです。 例えば、IntStrはともにAddだが、IntStrの加算はできません。

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)|