Memory management

Inko uses automatic memory management, using "single ownership" instead of a garbage collector. Inko has four primary types of references: owned references, immutable borrows, mutable borrows, and unique references. In this tutorial we'll take a look at the basics of working with these different references.

Owned references

We'll start with a simple list of cats:

class Cat {}

class async Main {
  fn async main {
    let cats = [Cat {}, Cat {}]
  }
}

Save this in a file called cats.inko and run it as follows:

inko run cats.inko

If all went well, no output is produced.

Let's adjust the program to print the number of cats to the terminal:

import std.stdio.STDOUT

class Cat {}

class async Main {
  fn async main {
    let cats = [Cat {}, Cat {}]

    STDOUT.new.print("{cats.size} cats")
  }
}

If we run this again, the output is "2 cats".

Now we'll change the program to the following:

import std.stdio.STDOUT

class Cat {}

class async Main {
  fn async main {
    let cats = [Cat {}, Cat {}]
    let more_cats = cats

    STDOUT.new.print("{cats.size} cats")
  }
}

If we try to run this program, we're greeted with the following compile-time error:

cats.inko:10:24 error(moved): 'cats' can't be used as it has been moved

What happened is as follows: cats is a variable containing an owned reference to the array of cats. When this reference is assigned to the more_cats variable, ownership of the reference is moved to the more_cats variable. Once ownership is moved, we can no longer use the old reference (cats in this case).

When the owned reference is no longer in use, its memory is discarded, which we refer to as a value that's "dropped". The details of this are discussed separately.

Borrowing

If we only had owned references, writing meaningful programs in Inko would be difficult. Inko has two types of references that don't transfer ownership, known as "borrows": immutable borrows, and mutable borrows.

Immutable borrows are created using the ref keyword:

import std.stdio.STDOUT

class Cat {}

class async Main {
  fn async main {
    let cats = [Cat {}, Cat {}]
    let more_cats = ref cats

    STDOUT.new.print("{cats.size} cats")
  }
}

Mutable borrows are created using the mut keyword:

import std.stdio.STDOUT

class Cat {}

class async Main {
  fn async main {
    let cats = [Cat {}, Cat {}]
    let more_cats = mut cats

    STDOUT.new.print("{cats.size} cats")
  }
}

Running both these programs produces the output "2 cats", without any compile-time errors.

Mutable vs immutable

The difference between immutable and mutable borrows is simple: mutable borrows allow mutating of the borrowed data, while immutable borrows don't. For example:

import std.stdio.STDOUT

class Cat {}

class async Main {
  fn async main {
    let cats = [Cat {}, Cat {}]
    let cats_ref = ref cats

    cats_ref.pop
  }
}

If you try to run this program, you'll be greeted with the following compile-time error:

cats.inko:10:5 error(invalid-call): the method 'pop' requires a mutable receiver, but 'ref Array[Cat]' isn't mutable

To fix this, we need to use a mutable borrow:

import std.stdio.STDOUT

class Cat {}

class async Main {
  fn async main {
    let cats = [Cat {}, Cat {}]
    let cats_mut = mut cats

    cats_mut.pop
  }
}

Automatic borrows

When passing a value to something that expects a borrow, Inko automatically borrows the value according to the expected borrow:

class Person {
  let @name: String
}

fn example(person: ref Person) {}

class async Main {
  fn async main {
    let person = Person { @name = 'Alice' }

    example(person)
  }
}

Here example(person) results in the compiler passing a ref Person as the argument, allowing you to continue using person after returning from the example call. The behavior of automatically borrowing values is as follows:

InputExpectedPassed
Tref Tref T
Tmut Tmut T
ref Tref Tref T
mut Tmut Tmut T
mut Tref Tref T

Moving while borrowing

Inko allows you to move owned references while borrows to the owned reference exist. If an owned value is dropped while borrows to it still exist, a runtime error known as a "panic" is produced, terminating the program:

import std.stdio.STDOUT

class Cat {}

class async Main {
  fn async main {
    let cats = [Cat {}, Cat {}]
    let borrow = ref cats
    let more_cats = cats

    STDOUT.new.print("{borrow.size} cats")
    STDOUT.new.print("{more_cats.size} cats")
  }
}

If we run this program, the output is as follows:

2 cats
2 cats
Stack trace (the most recent call comes last):
  [...]/cats.inko:12 in main.Main.main
  [...]/std/src/std/array.inko:104 in std.array.Array.$dropper
Process 'Main' (0x55fd6eb37170) panicked: can't drop a value of type 'Array' as it still has 1 reference(s)

The reason this happens is because more_cats is dropped before borrow is dropped, while the borrow still exists (because borrow is defined before more_cats), resulting in this error.

Unique references

Inko also has a type of reference known as a "unique reference". Such references impose heavy restriction on borrowing, which ensures that these borrows don't exist when the unique reference is moved around. To illustrate this, change the cats.inko program to the following:

import std.stdio.STDOUT

class Cat {}

class async Main {
  fn async main {
    let cats = recover [Cat {}, Cat {}]
    let borrow = ref cats

    STDOUT.new.print("{cats.size} cats")
  }
}

Now run this using inko run cats.inko, and you should be presented with the following compile-time error:

cats.inko:8:18 error(invalid-type): values of type 'uni ref Array[Cat]' can't be assigned to variables or fields

What happened is the following: using the recover keyword we turned the array of cats into a unique array of cats. When using ref cats, a special borrow known as a "unique immutable borrow" (quite the mouthful) is created. The compiler imposes restrictions on such borrows, such as not allowing them to be assigned to variables.

Unique references are used when sending data between processes, as a value being unique ensures no borrows to the data exist, and thus no race conditions can occur when using the reference.

Value types

OK I lied when I said Inko has four types of primary references, as I left out one important one: value types. Value types are owned references that are copied when they are moved, instead of transferring ownership. This allows you to use both the old and new version. To illustrate, create values.inko with these contents:

import std.stdio.STDOUT

class async Main {
  fn async main {
    let out = STDOUT.new
    let a = 42
    let b = a

    out.print(a.to_string)
    out.print(b.to_string)
  }
}

Now run it using inko run values.inko, and the output is as follows:

42
42

The reason this program works is because 42 is an instance of the Int type, which is a 64-bits signed integer, and Int is a value type.

Other value types are floats (Float), strings (String), processes, channels (Channel), nil (Nil), booleans (Bool), and C structures used as part of the FFI.