Method and Trait#
Method system#
MoonBit supports methods in a different way from traditional object-oriented languages. A method in MoonBit is just a toplevel function associated with a type constructor. Methods can be defined using the syntax fn TypeName::method_name(...) -> ...
:
enum List[X] {
Nil
Cons(X, List[X])
}
fn List::concat[X](xs : List[List[X]]) -> List[X] {
...
}
As a convenient shorthand, when the first parameter of a function is named self
, MoonBit automatically defines the function as a method of the type of self
:
fn List::map[X, Y](xs : List[X], f : (X) -> Y) -> List[Y] {
...
}
is equivalent to:
fn map[X, Y](self : List[X], f : (X) -> Y) -> List[Y] {
...
}
Methods are just regular functions owned by a type constructor. So when there is no ambiguity, methods can be called using regular function call syntax directly:
let xs : List[List[_]] = { ... }
let ys = concat(xs)
Unlike regular functions, methods support overloading: different types can define methods of the same name. If there are multiple methods of the same name (but for different types) in scope, one can still call them by explicitly adding a TypeName::
prefix:
struct T1 {
x1 : Int
}
fn T1::default() -> T1 {
{ x1: 0 }
}
struct T2 {
x2 : Int
}
fn T2::default() -> T2 {
{ x2: 0 }
}
test {
// default() : T1::default() ? T2::default()?
let t1 = T1::default()
let t2 = T2::default()
}
When the first parameter of a method is also the type it belongs to, methods can be called using dot syntax x.method(...)
. MoonBit automatically finds the correct method based on the type of x
, there is no need to write the type name and even the package name of the method:
pub(all) enum List[X] {
Nil
Cons(X, List[X])
}
pub fn List::concat[X](xs : List[List[X]]) -> List[X] {
...
}
fn f() -> Unit {
let xs : @list.List[@list.List[Unit]] = Nil
let _ = xs.concat()
let _ = @list.List::concat(xs)
let _ = @list.concat(xs)
}
The highlighted line is only possible when there is no ambiguity in @list
.
Operator Overloading#
MoonBit supports operator overloading of builtin operators via methods. The method name corresponding to a operator <op>
is op_<op>
. For example:
struct T {
x : Int
}
fn op_add(self : T, other : T) -> T {
{ x: self.x + other.x }
}
test {
let a = { x: 0 }
let b = { x: 2 }
assert_eq!((a + b).x, 2)
}
Another example about op_get
and op_set
:
struct Coord {
mut x : Int
mut y : Int
} derive(Show)
fn op_get(self : Coord, key : String) -> Int {
match key {
"x" => self.x
"y" => self.y
}
}
fn op_set(self : Coord, key : String, val : Int) -> Unit {
match key {
"x" => self.x = val
"y" => self.y = val
}
}
fn main {
let c = { x: 1, y: 2 }
println(c)
println(c["y"])
c["x"] = 23
println(c)
println(c["x"])
}
{x: 1, y: 2}
2
{x: 23, y: 2}
23
Currently, the following operators can be overloaded:
Operator Name |
Method Name |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
By implementing op_as_view
method, you can create a view for a user-defined type. Here is an example:
type DataView String
struct Data {}
fn Data::op_as_view(_self : Data, start~ : Int = 0, end? : Int) -> DataView {
"[\{start}, \{end.or(100)})"
}
test {
let data = Data::{ }
inspect!(data[:]._, content="[0, 100)")
inspect!(data[2:]._, content="[2, 100)")
inspect!(data[:5]._, content="[0, 5)")
inspect!(data[2:5]._, content="[2, 5)")
}
Trait system#
MoonBit features a structural trait system for overloading/ad-hoc polymorphism. Traits declare a list of operations, which must be supplied when a type wants to implement the trait. Traits can be declared as follows:
trait I {
method_(Int) -> Int
method_with_label(Int, label~: Int) -> Int
//! method_with_label(Int, label?: Int) -> Int
}
In the body of a trait definition, a special type Self
is used to refer to the type that implements the trait.
Extending traits#
A trait can depend on other traits, for example:
trait Position {
pos(Self) -> (Int, Int)
}
trait Draw {
draw(Self) -> Unit
}
trait Object : Position + Draw {}
To implement the sub trait, one will have to implement the super traits, and the methods defined in the sub trait.
Implementing traits#
To implement a trait, a type must provide all the methods required by the trait.
This allows types to implement a trait implicitly, hence allowing different packages to work together without seeing or depending on each other.
For example, the following trait is automatically implemented for builtin number types such as Int
and Double
:
trait Number {
op_add(Self, Self) -> Self
op_mul(Self, Self) -> Self
}
Explicit implementation for trait methods can be provided via the syntax impl Trait for Type with method_name(...) { ... }
, for example:
trait MyShow {
to_string(Self) -> String
}
struct MyType {}
impl MyShow for MyType with to_string(self) { ... }
struct MyContainer[T] {}
// trait implementation with type parameters.
// `[X : Show]` means the type parameter `X` must implement `Show`,
// this will be covered later.
impl[X : MyShow] MyShow for MyContainer[X] with to_string(self) { ... }
Type annotation can be omitted for trait impl
: MoonBit will automatically infer the type based on the signature of Trait::method
and the self type.
The author of the trait can also define default implementations for some methods in the trait, for example:
trait J {
f(Self) -> Unit
f_twice(Self) -> Unit
}
impl J with f_twice(self) {
self.f()
self.f()
}
Implementers of trait I
don’t have to provide an implementation for f_twice
: to implement I
, only f
is necessary.
They can always override the default implementation with an explicit impl I for Type with f_twice
, if desired, though.
If an explicit impl
or default implementation is not found, trait method resolution falls back to regular methods.
Using traits#
When declaring a generic function, the type parameters can be annotated with the traits they should implement, allowing the definition of constrained generic functions. For example:
fn square[N : Number](x : N) -> N {
x * x // <=> x.op_mul(x)
}
Without the Number
requirement, the expression x * x
in square
will result in a method/operator not found error. Now, the function square
can be called with any type that implements Number
, for example:
struct Point {
x : Int
y : Int
} derive(Eq, Show)
impl Number for Point with op_add(self, other) {
{ x: self.x + other.x, y: self.y + other.y }
}
impl Number for Point with op_mul(self, other) {
{ x: self.x * other.x, y: self.y * other.y }
}
test {
assert_eq!(square(2), 4)
assert_eq!(square(1.5), 2.25)
assert_eq!(square(Point::{ x: 2, y: 3 }), { x: 4, y: 9 })
}
Invoke trait methods directly#
Methods of a trait can be called directly via Trait::method
. MoonBit will infer the type of Self
and check if Self
indeed implements Trait
, for example:
test {
assert_eq!(Show::to_string(42), "42")
assert_eq!(Compare::compare(1.0, 2.5), -1)
}
Trait implementations can also be invoked via dot syntax, with the following restrictions:
if a regular method is present, the regular method is always favored when using dot syntax
only trait implementations that are located in the package of the self type can be invoked via dot syntax
if there are multiple trait methods (from different traits) with the same name available, an ambiguity error is reported
if neither of the above two rules apply, trait
impl
s in current package will also be searched for dot syntax. This allows extending a foreign type locally.these
impl
s can only be called via dot syntax locally, even if they are public.
The above rules ensures that MoonBit’s dot syntax enjoys good property while being flexible. For example, adding a new dependency never break existing code with dot syntax due to ambiguity. These rules also make name resolution of MoonBit extremely simple: the method called via dot syntax must always come from current package or the package of the type!
Here’s an example of calling trait impl
with dot syntax:
struct MyCustomType {}
impl Show for MyCustomType with output(self, logger) { ... }
fn f() -> Unit {
let x = MyCustomType::{ }
let _ = x.to_string()
}
Trait objects#
MoonBit supports runtime polymorphism via trait objects.
If t
is of type T
, which implements trait I
,
one can pack the methods of T
that implements I
, together with t
,
into a runtime object via t as &I
.
Trait object erases the concrete type of a value,
so objects created from different concrete types can be put in the same data structure and handled uniformly:
trait Animal {
speak(Self) -> String
}
type Duck String
fn Duck::make(name : String) -> Duck {
Duck(name)
}
fn speak(self : Duck) -> String {
"\{self._}: quack!"
}
type Fox String
fn Fox::make(name : String) -> Fox {
Fox(name)
}
fn Fox::speak(_self : Fox) -> String {
"What does the fox say?"
}
test {
let duck1 = Duck::make("duck1")
let duck2 = Duck::make("duck2")
let fox1 = Fox::make("fox1")
let animals : Array[&Animal] = [
duck1 as &Animal,
duck2 as &Animal,
fox1 as &Animal,
]
inspect!(
animals.map(fn(animal) { animal.speak() }),
content=
#|["duck1: quack!", "duck2: quack!", "What does the fox say?"]
,
)
}
Not all traits can be used to create objects. “object-safe” traits’ methods must satisfy the following conditions:
Self
must be the first parameter of a methodThere must be only one occurrence of
Self
in the type of the method (i.e. the first parameter)
Users can define new methods for trait objects, just like defining new methods for structs and enums:
trait Logger {
write_string(Self, String) -> Unit
}
trait CanLog {
log(Self, &Logger) -> Unit
}
fn &Logger::write_object[Obj : CanLog](self : &Logger, obj : Obj) -> Unit {
obj.log(self)
}
// use the new method to simplify code
impl[A : CanLog, B : CanLog] CanLog for (A, B) with log(self, logger) {
let (a, b) = self
logger
..write_string("(")
..write_object(a)
..write_string(", ")
..write_object(b)
.write_string(")")
}
Builtin traits#
MoonBit provides the following useful builtin traits:
trait Eq {
op_equal(Self, Self) -> Bool
}
trait Compare : Eq {
// `0` for equal, `-1` for smaller, `1` for greater
compare(Self, Self) -> Int
}
trait Hash {
hash_combine(Self, Hasher) -> Unit // to be implemented
hash(Self) -> Int // has default implementation
}
trait Show {
output(Self, Logger) -> Unit // to be implemented
to_string(Self) -> String // has default implementation
}
trait Default {
default() -> Self
}
Deriving builtin traits#
MoonBit can automatically derive implementations for some builtin traits:
struct T {
x : Int
y : Int
} derive(Eq, Compare, Show, Default)
test {
let t1 = T::default()
let t2 = T::{ x: 1, y: 1 }
inspect!(t1, content="{x: 0, y: 0}")
inspect!(t2, content="{x: 1, y: 1}")
assert_not_eq!(t1, t2)
assert_true!(t1 < t2)
}
See Deriving for more information about deriving traits.