Traits
Unlike many other languages with classes, Inko doesn't support class inheritance. Instead, code is shared between classes using "traits". Traits are essentially blueprints for classes, specifying what methods must be implemented and/or providing default implementations of methods a class may wish to override.
Let's say we want to convert different class instances into strings. We can do so using traits:
import std.stdio (Stdout)
trait ToString {
fn to_string -> String
}
class Cat {
let @name: String
}
impl ToString for Cat {
fn to_string -> String {
@name
}
}
class 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 class 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 {
'...'
}
}
class Cat {
let @name: String
}
impl ToString for Cat {
fn to_string -> String {
@name
}
}
class 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 {
'...'
}
}
class Cat {
let @name: String
}
impl ToString for Cat {
}
class 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]
.