Skip to content

Classes

We've briefly covered classes in the Basic types chapter. In this chapter we'll take a closer look at them.

In Inko, classes don't support inheritance. Instead, behaviour is reused through composition by using traits.

Defining classes

You can define a class by using the class keyword, followed by its name and a pair of curly braces:

class Person {

}

Instances of classes are created as follows:

Person {}

We refer to this pattern as a "constructor", because it's used to construct a new instance of a class.

Attributes

Of course our empty class is not useful, so let's give it some fields to store data in. We call such fields "attributes", and we define them in the body of an class. All attributes must be defined in the class before they can be used. Let's say we want our Person type to have two attributes: a name and an age attribute. Our name will be a String, and the age will be an Integer. We can do this as follows:

class Person {
  @name: String
  @age: Integer
}

We refer to attributes using the syntax @NAME where NAME is the name of the attribute. Attributes are private to the class, meaning they can't be accessed directly. Instead, you must define a method that returns the attribute, which we'll cover below.

When defining an attribute as done above, we can't specify a default value for the attribute; instead it's up to the user of our type to assign a value to all attributes. For our Person example above, this is done as follows:

Person { @name = 'Alice', @age = 32 }

When creating a class instance, all attributes must be assigned. If an attribute is not assigned, a compile-time error is produced.

Methods

Classes can have two types of methods: instance methods, and static methods. Instance methods are only available to instances of the class, while static methods are available to the class itself.

To define an instance method, use the def keyword:

class Person {
  @name: String
  @age: Integer

  def name -> String {
    @name
  }
}

Here we define the instance method name, which returns the @name attribute. We can use this method like so:

Person { @name = 'Alice', @age = 42 }.name # => 'Alice'

To define a static method, use static def:

class Person {
  @name: String
  @age: Integer

  static def anonymous(age: Integer) -> Person {
    Person { @name = 'Anonymous', @age = age }
  }

  def name -> String {
    @name
  }
}

We can then use it like so:

Person.anonymous(42).name # => 'Anonymous'

Static methods can't refer to attributes, meaning that this is an error:

class Person {
  @name: String
  @age: Integer

  static def oops -> String {
    @name
  }
}

Both static and instance methods can use self to refer to their receiver. In case of an instance method, that will be the instance the method is called on (a Person instance for our name instance method). For static methods, this will be the class itself (Person in this case). You don't need to use self if you want to send a message to the current receiver. This means that this:

class Person {
  @name: String
  @age: Integer

  def nickname -> String {
    self.name
  }

  def name -> String {
    @name
  }
}

Is the same as this:

class Person {
  @name: String
  @age: Integer

  def nickname -> String {
    name
  }

  def name -> String {
    @name
  }
}

Sometimes you do need to use self. For example, if a method takes an argument with the same name as another method.

Constructor methods

Having to specify all attributes when creating class instances can be tedious. To make it easier to create instances, you can use what's known as a constructor method. A constructor method is a static method used to create a new instance of the class it's defined on. We saw such an example earlier: the static anonymous method used to create a new Person.

Various types come with at least one constructor method: new. For example, instances of Array are created using such a method:

Array.new(10, 20, 30)

For our Person example shown earlier, we can define a new method like so:

class Person {
  @name: String
  @age: Integer

  static def new(name: String, age: Integer) -> Self {
    Person { @name = name, @age = age }
  }
}

We can then create an instance as follows:

Person.new(name: 'Alice', age: 32)

Having to repeat the type name in our constructor method is a bit tedious. Instead of doing this, we can use the Self type when constructing a class instance:

class Person {
  @name: String
  @age: Integer

  static def new(name: String, age: Integer) -> Self {
    Self { @name = name, @age = age }
  }
}

The use of Self for a constructor is only valid in a static method.

When defining classes, we recommend defining at least a static new method for the class. Various built-in types even require the use of new to create an instance, and don't support the use of the constructor syntax. These types are as follows:

  • Array
  • Block
  • Boolean
  • ByteArray
  • Float
  • Integer
  • Module
  • NilType
  • String
  • std::ffi::Function
  • std::ffi::Library
  • std::ffi::Pointer
  • std::fs::file::ReadOnlyFile
  • std::fs::file::ReadWriteFile
  • std::fs::file::WriteOnlyFile
  • std::map::DefaultHasher
  • std::net::Socket
  • std::process::Process
  • std::unix::Socket

Reopening classes

A class can be reopened in any module, allowing you to add new methods after its initial definition. This is done as follows:

class Person {
  @name: String
}

impl Person {
  def name -> String {
    @name
  }
}

Here we reopen Person, and add the name instance method to it.