Trait

Traits are nominal types that add a type attribute requirement to record types. It is similar to the Abstract Base Class (ABC) in Python, but it has the feature of being able to perform algebraic operations.

Traits are used when you want to identify different classes. Examples of builtin traits are Eq and Add. Eq requires == to be implemented. Add requires the implementation of + (in-place).

So any class that implements these can be (partially) identified as a subtype of trait.

As an example, let's define a Norm trait that computes the norm (length) of a vector.

Norm = Trait {.norm = (self: Self) -> Int}

Note that traits can only be declared, not implemented. Traits can be "implemented" for a class as follows:

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

Since Point2D and Point3D implement Norm, they can be identified as types with the .norm method.

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

Error if the required attributes are not implemented.

Point3D = Class {.x = Int; .y = Int; .z = Int}

Point3D|<: Norm|.
    foo self = 1

One of the nice things about traits is that you can define methods on them in Patch (described later).

@Attach NotEqual
Eq = Trait {. `==` = (self: Self, other: Self) -> Bool}

NotEq = Patch Eq
NotEq.
    `! =` self, other = not self.`==` other

With the NotEq patch, all classes that implement Eq will automatically implement !=.

Trait operations

Traits, like structural types, can apply operations such as composition, substitution, and elimination (e.g. T and U). The resulting trait is called an instant trait.

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

Trait is also a type, so it can be used for normal type specification.

points: [Norm; 2] = [Point2D::new(1, 2), Point2D::new(3, 4)]
assert points.iter().map(x -> x.norm()).collect(Array) == [5, 25].

Trait inclusion

Subsume allows you to define a trait that contains a certain trait as a supertype. This is called the subsumption of a trait. In the example below, BinAddSub subsumes BinAdd and BinSub. This corresponds to Inheritance in a class, but unlike Inheritance, multiple base types can be combined using and. Traits that are partially excluded by not are also allowed.

Add R = Trait {
    .Output = Type
    . `_+_` = (self: Self, R) -> Self.Output
}

Sub R = Trait {
    .Output = Type
    . `_-_` = (self: Self, R) -> Self.Output
}

BinAddSub = Subsume Add(Self) and Sub(Self)

Structural Traits

Traits can be structured. This way, there is no need to explicitly declare the implementation, this is a feature that is called duck typing in Python.

SAdd = Structural Trait {
    . `_+_` = Self.(Self) -> Self
}
# |A <: SAdd| cannot be omitted
add|A <: SAdd| x, y: A = x.`_+_` y

C = Class {i = Int}
C.
    new i = Self.__new__ {i;}
    # this is an __implicit__ implementation of SAdd
    `_+_` self, other: Self = Self.new {i = self::i + other::i}

assert add(C.new(1), C.new(2)) == C.new(3)

Regular trait, i.e. nominal traits cannot be used simply by implementing a request method, but must be explicitly declared to have been implemented. In the following example, add cannot be used with an argument of type C because there is no explicit declaration of implementation. It must be C = Class {i = Int}, Impl := Add.

Add = Trait {
    .`_+_` = (self: Self, Self) -> Self
}
# |A <: Add| can be omitted
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 a subclass of Add
# hint: inherit or patch 'Add'

Structural traits do not need to be declared for this implementation, but instead type inference does not work. Type specification is required for use.

Polymorphic Traits

Traits can take parameters. This is the same as for polymorphic types.

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"].

Override in Trait

Derived traits can override the type definitions of the base trait. In this case, the type of the overriding method must be a subtype of the base method type.

# `Self.(R) -> O` is a subtype of ``Self.(R) -> O or Panic
Div R, O: Type = Trait {
    . `/` = (self: Self, R) -> O or Panic
}
SafeDiv R, O = Subsume Div, {
    @Override
    . `/` = (self: Self, R) -> O
}

Implementing and resolving duplicate traits in the API

The actual definitions of Add, Sub, and Mul look like this.

Add R = Trait {
    .Output = Type
    . `_+_` = (self: Self, R) -> .Output
}
Sub R = Trait {
    .Output = Type
    . `_-_` = (self: Self, R) -> .Output
}
Mul R = Trait {
    .Output = Type
    . `*` = (self: Self, R) -> .Output
}

.Output is duplicated. If you want to implement these multiple traits at the same time, specify the following.

P = Class {.x = Int; .y = Int}
# P|Self <: Add(P)| can be abbreviated to 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}

Duplicate APIs implemented in this way are almost always type inferred when used, but can also be resolved by explicitly specifying the type with ||.

print! P.Output # TypeError: ambiguous type
print! P|<: Mul(Int)|.Output # <class 'P'>

Appendix: Differences from Rust traits

Erg's trait is faithful to the one proposed by Schärli et al.. In order to allow algebraic operations, traits are designed to be unable to have method implementations directory, but can be patched if necessary.