Class
A class in Erg is roughly a type that can create its own elements (instances). Here is an example of a simple class.
Person = Class {.name = Str; .age = Nat}
# If `.new` is not defined, then Erg will create `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
The type given to Class
(normally a record type) is called the requirement type (in this case {.name = Str; .age = Nat}
).
Instances can be created with <Class name>::__new__ {<attribute name> = <value>; ...}
can be created with.
{.name = "John Smith"; .age = 25}
is just a record, but it is converted to a Person
instance by passing Person.new
.
The subroutine that creates such an instance is called a constructor.
In the class above, the .new
method is defined so that field names, etc. can be omitted.
Note that the following definition without line breaks will result in a syntax error.
Person.new name, age = ... # SyntaxError: cannot define attributes directly on an object
Newtype notation
You can define a class C
by C = Class T
for a non-record type T
. This is a short-hand notation, which is equivalent to C = Class {base = T}
.
This is to simplify the definition of the so-called "new type pattern".
Also, the constructor __new__
/new
can be passed directly to a T
type object without wrapping it in a record.
Id = Class {base = Int}
i = Id.new {base = 1}
# ↓
Id = Class Int
i = Id.new 1
Instance and class attributes
In Python and other languages, instance attributes are often defined on the block side as follows, but note that such writing has a different meaning in Erg.
# Python
class Person:
name: str
age: int
# In Erg, this notation implies the declaration of a class attribute (not an instance attribute)
Person = Class()
Person.
name: Str
age: Int
# Erg code for the Python code above
Person = Class {
.name = Str
.age = Nat
}
Element attributes (attributes defined in a record) and type attributes (also called instance/class attributes, especially in the case of classes) are completely different things. Type attributes are attributes of the type itself. An element of a type refers to a type attribute when it does not have the desired attribute in itself. An element attribute is a unique attribute directly possessed by the element. Why is this distinction made? If all attributes were element attributes, it would be inefficient to duplicate and initialize all attributes when the object is created. In addition, dividing the attributes in this way clarifies roles such as "this attribute is shared" and "this attribute is held separately".
The example below illustrates this. The attribute species
is common to all instances, so it is more natural to use it as a class attribute. However, the attribute name
should be an instance attribute because each instance should have it individually.
Person = Class {name = Str}
Person::
species = "human"
Person.
describe() =
log "species: \{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.
Incidentally, if an instance attribute and a type attribute have the same name and the same type, a compile error occurs. This is to avoid confusion.
C = Class {.i = Int}
C.i = 1 # AttributeError: `.i` is already defined in instance fields
Class, Type
The question "What is the type of 1
?" requires a slightly longer answer.
There is only one class Nat
from which 1
belongs. The class to which an object belongs can be obtained by classof(obj)
or obj.__class__
.
In contrast, there are countless types to which 1
belongs. Examples are {1}, {0, 1}, 0..12, Nat, Int, Num
.
The reason an object can belong to more than one type is that Erg has a subtype system. Nat
is a subtype of Int
and Int
is a subtype of Num
.
Difference from structural types
We said that a class is a type that can generate its own elements, but that is not a strict description. In fact, a record type + patch can do the same thing.
Person = {.name = Str; .age = Nat}
PersonImpl = Patch Person
PersonImpl.
new name, age = {.name; .age}
john = Person.new("John Smith", 25)
There are four advantages to using classes. The first is that the constructor is validity checked, the second is that it is more performant, the third is that you can use notational subtypes (NSTs), and the fourth is that you can inherit and override.
We saw earlier that record type + patch can also define a constructor (of sorts), but this is of course not a legitimate constructor. This is of course not a legitimate constructor, because it can return a completely unrelated object even if it calls itself .new
. In the case of a class, .new
is statically checked to see if it produces an object that satisfies the requirements.
~
Type checking for classes is simply a matter of checking the object's . __class__
attribute of the object. So it is fast to check if an object belongs to a type.
~
Erg enables NSTs in classes; the advantages of NSTs include robustness. When writing large programs, it is often the case that the structure of an object is coincidentally matched.
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!"
The structure of Dog
and Person
is exactly the same, but it is obviously nonsense to allow animals to greet and humans to bark.
The former is impossible, so it is safer to make it inapplicable. In such cases, it is better to use classes.
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`.
Another feature is that the type attributes added by the patch are virtual and are not held as entities by the implementing class.
That is, T.x
, T.bar
are objects that can be accessed (compile-time bound) by types compatible with {i = Int}
, and are not defined in {i = Int}
or C
.
In contrast, class attributes are held by the class itself. Therefore, they cannot be accessed by classes that are not in an inheritance relationship, even if they have the same structure.
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!
print! {i = 1}.bar # <method bar>
C.new({i = 1}).bar # <method bar>
Difference from Data Class
There are two types of classes: regular classes, which are generated with Class(record)
, and data classes, which are generated with Inherit(record)
.
The data class inherits the functionality of the record class and has features such as decomposition assignment, ==
and hash
implemented by default, etc. On the other hand, the data class has its own equivalence relation and format display.
On the other hand, if you want to define your own equivalence relations or formatting displays, you should use the normal class.
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} # same as `e = D.new {i = 1}`
f = D::{i = 2}
print! e # D(i=1)
assert e ! = f
Enum Class
To facilitate defining classes of type Or
, an Enum
is provided.
X = Class()
Y = Class()
XorY = Enum X, Y
Each type can be accessed as XorY.X
, XorY.Y
and the constructor can be obtained as 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
Class Relationships
A class is a subtype of a requirement type. methods (including patch methods) of the requirement type can be used in the class.
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