クラス
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