Traits
Inko types doesn't support type inheritance. Instead, code is shared between types using "traits". Traits are essentially blueprints for types, specifying what methods must be implemented and/or providing default implementations of methods a type may wish to override.
Let's say we want to convert different type instances into strings. We can do so using traits:
import std.stdio (Stdout)
trait ToString {
fn to_string -> String
}
type Cat {
let @name: String
}
impl ToString for Cat {
fn to_string -> String {
@name
}
}
type async Main {
fn async main {
let garfield = Cat(name: 'Garfield')
Stdout.new.print(garfield.to_string)
}
}
Running this program produces the output "Garfield".
Default methods
In this example, the to_string
method in the ToString
trait is a required
method. This means that any type that implements ToString
must provide an
implementation of the to_string
method.
Default trait methods are defined as follows:
import std.stdio (Stdout)
trait ToString {
fn to_string -> String {
'...'
}
}
type Cat {
let @name: String
}
impl ToString for Cat {
fn to_string -> String {
@name
}
}
type async Main {
fn async main {
let garfield = Cat(name: 'Garfield')
Stdout.new.print(garfield.to_string)
}
}
Here the default implementation of to_string
is to return the string '...'
.
Our implementation of ToString
for Cat
still overrides it, so the output is
still "Garfield". Because the method is a default method, we can use it as-is as
follows:
import std.stdio (Stdout)
trait ToString {
fn to_string -> String {
'...'
}
}
type Cat {
let @name: String
}
impl ToString for Cat {
}
type async Main {
fn async main {
let garfield = Cat(name: 'Garfield')
Stdout.new.print(garfield.to_string)
}
}
If we now run the program, the output is "...".
Required traits
Traits can specify other traits that must be implemented before the trait itself can be implemented:
trait ToString {
fn to_string -> String
}
trait ToUpperString: ToString {
fn to_upper_string -> String {
to_string.to_upper
}
}
Here the ToUpperString
trait states that for a type to be able to implement
ToUpperString
, it must also implement ToString
.
You can also specify multiple required traits:
trait A {}
trait B {}
trait C: A + B {}
Here the trait C
requires both A
and B
to be implemented.
Conflicting trait methods
It's possible for different traits to define methods with the same name. If a type tries to implement such traits, a compile-time error is produced. Inko doesn't support renaming of trait methods as part of the implementation, so you'll need to find a way to resolve such conflicts yourself.
Conditional trait implementations
Sometimes we want to implement a trait, but only if additional requirements are
met. For example, we want to implement std.cmp.Equal
for Array
but only if
its sub values also implement std.cmp.Equal
. This is done as follows:
import std.cmp (Equal)
impl Equal[ref Array[T]] for Array if T: Equal[ref T] {
fn pub ==(other: ref Array[T]) -> Bool {
...
}
}
What happens here is that we implement Equal
over ref Array[T]
, for any
Array[T]
provided that whatever is assigned to T
also implements
Equal[ref T]
. For example, given an Array[User]
, the Array.==
method is
only available if User
implements Equal[ref User]
.