You're looking at the documentation for the "main" branch, click here to view the documentation for the latest stable release.

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