Types and methods
Inko provides two kinds of types: classes and traits. In this chapter we'll take a look at how to define and use such types.
Classes
Classes store state and provide methods, and are created using the class
keyword:
class Person {
let @name: String
let @age: Int
}
Instances of classes are created using the class literal syntax:
Person { @name = 'Alice', @age = 42 }
Within a class you can also create instances of the class itself using Self
:
Self { @name = 'Alice', @age = 42 }
When creating an instance, all fields must be assigned a value, and a field can't be assigned a value multiple times.
Classes don't support inheritance, and instead rely on traits to provide reusable behaviour.
Classes come in three forms: regular classes, enum classes, and async classes.
Enum classes are algebraic data types created using the class enum
syntax, and
its variants are specified using the case
keyword:
class enum Error {
case FileDoesntExit
case PermissionDenied
}
When pattern matching against enum classes, the compiler ensures the match is exhaustive.
Async classes are created using the class async
syntax and are used to define
and spawn processes. This is covered in greater detail in the
Concurrency chapter.
Methods
Methods may be defined when defining the class or when reopening it:
class Person {
let @name: String
let @age: Int
fn name -> String {
@name.clone
}
}
impl Person {
fn age -> Int {
@age.clone
}
}
The default return type of a method is Nil
. When the return type is Nil
, any
expression implicitly returned in the method is ignored:
fn example {
42
}
example # => nil
If a method is defined as returning Nil
, you can't explicitly return a value
that isn't Nil
:
fn example {
return 42 # => Compile-time error
}
For enum classes the compiler generates a static method for each variant, using the same name as the variant:
class enum Error {
case FileDoesntExit
case PermissionDenied
}
Error.FileDoesntExit # Same as Error.FileDoesntExit()
Fields
Fields default to being private to the module the class is defined in. You can
make them public using let pub
:
class Person {
let pub @name: String
let pub @age: Int
}
Fields are accessed using the same syntax as method calls, making it easier to introduce custom getter/setter methods without having to change every line that uses the fields:
let alice = Person { @name = 'Alice', @age = 42 }
alice.name # => 'Alice'
alice.age # => 42
Enum classes can't define custom fields.
Traits
Traits are a sort of contract for classes to adhere to: a trait can specify one or more required methods as well as default methods. A class implementing a trait must implement the required methods, and is automatically given a copy of the default methods; unless the class overrides the implementation. Traits may also list other traits a class must implement.
A simple example of a trait is std::string::ToString
, defined as follows:
trait pub ToString {
fn pub to_string -> String
}
A class implementing this trait must provide a to_string
implementation
compatible with the one of the trait. Here's what such an implementation might
look like:
import std::string::ToString
class Person {
let @name: String
let @age: Int
}
impl ToString for Person {
fn pub to_string -> String {
@name.clone
}
}
A class can only implement a trait once, even if the trait is generic.
Type and method visibility
Types and methods default to being private to the module they are defined in,
and can be made public using the pub
keyword. For example, a public class is
defined as follows:
fn pub foo {
# ...
}
A private type can't be used in the signature of a public method or field. Private types can define public methods, which in practise means they're the same as private methods. This is allowed so private types can implement traits that expose public methods, without requiring the type to also be public.
Core types
Inko provides various core types, such as String
, Int
, and Array
.
Some of these types are value types, which means that when they are moved a copy is created and then moved. This allows you to continue using the original value after it would be moved.
Int
The Int
class is used for integers. Integers are 64 bits signed integers.
Int
is a value type.
Float
The Float
class is used for IEEE 754 double-precision floating point numbers.
Float
is a value type.
String
The String
class is used for strings. Strings are UTF-8 encoded immutable
strings. Internally strings are represented such that they can be efficiently
passed to C code, at the cost of one extra byte of overhead per string.
String
uses atomic reference counting when copying. This means that ten copies
of a 1 GiB String
only require 1 GiB of memory.
String
is a value type.
Boolean
Inko's boolean type is Boolean
. Instances of Boolean
are created using
true
and false
.
Boolean
is a value type.
Array
Array
is a contiguous growable array type and can store any value, as long as
all values in the array are of the same type.
ByteArray
ByteArray
is similar to Array
, except its optimised for storing bytes. A
ByteArray
needs less memory compared to an Array
, but can only store Int
values in the range of 0 up to (and including) 255.
Option
Option
is an algebraic data type/enum class used to represent an optional
value. It has two variants: Some(T)
and None
, with None
signalling the
lack of a value.
Map
Map
is a hash map and can store key-value pairs of any type, as long as the
keys implement the traits std::hash::Hash
and std::cmp::Equal
.
Nil
Nil
is Inko's unit type, and used to signal the complete lack of a value. The
difference with Option
is that a value of type Nil
can only ever be Nil
,
not something else. Nil
is used as the default return type of methods, and in
some cases can be used to explicitly ignore the result of an expression (e.g. in
pattern matching bodies).
Nil
is a value type.
Special types
Inko has three special types: Self
, Any
and Never
. These types are special
in that they don't exist at runtime as some sort of structure, instead they only
exist in the compiler.
Self
is a type that refers to either the surrounding class, or when used in a
trait, refers to the class that implements the trait.
Any
is a type used for a value that could be anything, including data not
managed by the Inko runtime such as a pointer to a C structure. Types can't be
cast to an Any
(nor are they compatible with Any
), but you can cast
Any
to any other type. This is useful when working with Inko's FFI, but you
should avoid Any
anywhere else. ref Any
is a variation of this type that
doesn't take ownership of a value. This type is used in the FFI for function
arguments.
Never
is a type that indicates something never happens. When used in a return
or throw type it means a method never returns or throws. If a method doesn't
define a throw type, it defaults to Never
.
Generic types
Types can be made generic, allowing them to operate on a wide range of types. For example, here's how you'd might define a generic linked list:
class Node[T] {
let @next: Option[Node[T]]
let @value: T
}
class List[T] {
let @head: Option[Node[T]]
let @tail: Option[mut Node[T]]
}
Classes, traits, methods and variants can all be made generic. Here's how you'd
define a generic Result
type commonly found in functional languages:
class enum Result[T, E] {
case Ok(T)
case Error(E)
}
Type inference
Inko supports type inference, removing the need for type annotations in most
cases. For example, the type signature of an Array
can be inferred based on
its usage:
# Here the compiler infers `a` as `Array[Int]`, because of the `push` below.
let a = []
a.push(42)
This works for any type, including generic types such as the Option
type:
# `a` is inferred as `Option[Int]`.
let mut a = Option.None
a = Option.Some(42)
If a generic type can't be inferred, the compiler produces an error. In this case explicit type signatures are necessary:
let mut a: Option[Int] = Option.None
The prelude
Inko automatically imports certain symbols into your modules. These symbols are part of what is called "the prelude".
The prelude includes the following types and methods:
Symbol | Source module |
---|---|
Int |
std::int |
Float |
std::float |
String |
std::string |
Array |
std::array |
Boolean |
std::bool |
Nil |
std::nil |
ByteArray |
std::byte_array |
Option |
std::option |
Map |
std::map |
panic |
std::process |