A language for building concurrent software with confidence

Inko makes it easy to build concurrent software, without having to worry about unpredictable performance, unexpected runtime errors, data races, and type errors.

Inko features deterministic automatic memory management, move semantics, static typing, type-safe concurrency, efficient error handling, and more.

Get started Discord GitHub
class Stack[T] {
  let @values: Array[T]

  fn static new -> Stack[T] {
    Stack { @values = [] }
  }

  fn mut push(value: T) {
    @values.push(value)
  }

  fn mut pop -> Option[T] {
    @values.pop
  }
}

class async Main {
  fn async main {
    let stack = Stack.new

    # This infers `T` to `Int`.
    stack.push(42)

    # This is an error, as `T` is inferred to `Int`, and
    # `String` isn't compatible with `Int`.
    stack.push('Oh no!')

    # The compiler can also infer earlier types based
    # on how they are used later on:
    let stack = Stack.new
    let value = stack.pop # The exact type isn't known yet

    10 + value.unwrap # `value` inferred as `Option[Int]`
  }
}
trait ToString {
  fn to_string -> String
}

class Person {
  let @name: String
}

impl ToString for Person {
  fn to_string -> String {
    @name
  }
}

# Traits can also be implemented conditionally:
class List[T] {
  # ...
}

# Here `ToString` is only available for instances
# of `List` if `T` also implements `ToString`.
impl ToString for List if T: ToString {
  fn to_string -> String {
    '...'
  }
}
class async Main {
  fn async main {
    let mut num = 0

    while num < 10 {
      num += 1
    }

    loop {
      # This loops run forever. You can skip/break
      # iteration using `next` and `break`.
    }
  }
}
import std.stdio.STDOUT

class async Main {
  fn async main {
    STDOUT.new.print('Hello, world!')
  }
}
fn div(left: Int, right: Int) -> Result[Int, String] {
  # `throw x` is short for `return Result.Error(x)`,
  # saving us a bit of typing.
  if right == 0 { throw 'Attempt to divide by zero' }

  Result.Ok(left / right)
}

fn div2(left: Int, right: Int) -> Result[Int, String] {
  # `try` is short for matching against a value,
  # and throwing it again if it's an `Error`. It
  # also works for `Option` values, in which case
  # it throws a `None` if the value is also a
  # `None`.
  let res = try div(left, right)

  if res == 5 { Result.Ok(50) } else { Result.Ok(res) }
}

class async Main {
  fn async main {
    # `Result` is just an algebraic data type, so we can
    # pattern match against it to get the underlying
    # value.
    match div(10, 2) {
      case Ok(val) -> val
      case Error(err) -> panic(err)
    }

    # We can also just unwrap the Ok value if we're
    # certain we'll never get an `Error` case:
    div(10, 2).unwrap # => 5
  }
}
import std.test.Tests

class async Main {
  fn async main {
    let tests = Tests.new

    tests.test('Adding two integers') fn (t) {
      t.equal(10 + 5, 15)
      t.equal(1 + -1, 0)
    }

    tests.run
  }
}
class async Calculator {
  fn async fact(size: Int, output: Channel[Int]) {
    let result = 1
      .to(size)
      .iter
      .reduce(1) fn (product, val) { product * val }

    output.send(result)
  }
}

class async Main {
  fn async main {
    let calc = Calculator {}
    let out = Channel.new(size: 1)

    # This calculates the factorial of 15 in the
    # background, then we wait for the result to
    # be sent back to us via a channel.
    calc.fact(15, out)
    out.receive # => 1307674368000
  }
}
class async Main {
  fn async main {
    let numbers = [10, 20, 30]

    numbers
      .opt(2)
      .map fn (n) { n.to_string } # => Option.Some('30')
  }
}
import std.fs.file.ReadOnlyFile

class async Main {
  fn async main {
    # expect() unwraps the Result, panicking with
    # the given error message when encountering an
    # `Error`.
    let file = ReadOnlyFile
      .new('README.md')
      .expect("the file doesn't exist")

    let bytes = ByteArray.new

    file
      .read_all(bytes)
      .expect('failed to read the file')

    bytes.to_string # => "# README ..."
  }
}
import std.drop.Drop
import std.stdio.STDOUT

class Thing {}

impl Drop for Thing {
  fn mut drop {
    STDOUT.new.print('Thing is dropped')
  }
}

class async Main {
  fn async main {
    let thing = Thing {}

    # Thing goes out of scope here, running its destructor.
  }
}
fn moves {
  let numbers = [10, 20, 30]

  # This is OK, and returns a `ref 10`.
  numbers[0]

  let nums = numbers

  # This is no longer OK, as `numbers` gave up ownership to
  # `nums`.
  numbers[0]
}

fn refs {
  let owned = [10, 20, 30]

  # We can "borrow" a value using references:
  let borrowed = ref owned

  # Both are OK and produce the same value: `ref 10`.
  owned[0]
  borrowed[0]

  # This isn't OK, as `ref T` doesn't allow mutation:
  borrowed.push(42)

  # For that we need a mutable reference:
  let mutable = mut owned

  mutable.push(42)
}
class async Main {
  fn async main {
    match Option.Some(42) {
      case Some(number) -> number
      case None -> 0
    }

    # Of course we also support nested patterns:
    match Option.Some((10, 'foo')) {
      case Some((10, message)) -> message
      case _ -> 'oh no!'
    }

    # Guards are also supported:
    match Option.Some(42) {
      case Some(number) if number < 50 -> number
      case _ -> 0
    }
  }
}

Deterministic automatic memory management

Inko doesn't rely on garbage collection to manage memory. Instead, Inko relies on single ownership and move semantics. Values start out as owned and are dropped when they go out of scope:

let numbers = [10, 20, 30]

# "numbers" is no longer in use here, so it's dropped.
return

These values can be borrowed either mutably or immutably. Inko allows multiple borrows (both mutable and immutable borrows), and allows moving of the borrowed values while borrows exist:

let a = [10, 20, 30]

# All of this is perfectly fine:
let b = ref a # borrows "a" immutably
let c = mut a # borrows "a" mutably
let d = a     # moves "a" into "d"

This gives you the benefits of single ownership, but at a fraction of the cost compared to languages such as Rust. The use of single ownership also means more predictable behaviour and performance, and not having to spend a long time adjusting different garbage collection settings.

Inko is safe

With Inko you never again have to worry about NULL pointers, use-after-free errors, unexpected runtime errors, data races, and other types of errors commonly found in other languages. For optional data Inko provides an Option type, which is an algebraic data type that you can pattern match against. Inko supports both mutable and immutable references, allowing you to restrict mutation where necessary.

Concurrency made easy

Inko uses lightweight processes for concurrency, and its concurrency model is inspired by Erlang and Pony. Processes are isolated from each other and communicate by sending messages. Processes and messages are defined as classes and methods, and the compiler type-checks these to ensure correctness.

The compiler ensures that data sent between processes is unique, meaning there are no outside references to the data. This removes the need for (deep) copying data, and makes data races impossible. Inko also supports multi-producer multi-consumer channels, allowing processes to communicate with each other without needing explicit references to each other.

Here's how you'd implement a simple concurrent counter:

class async Counter {
  let @value: Int

  fn async mut increment {
    @value += 1
  }

  fn async send_to(channel: Channel[Int]) {
    channel.send(@value)
  }
}

class async Main {
  fn async main {
    let counter = Counter { @value = 0 }
    let output = Channel.new(size: 1)

    counter.increment
    counter.increment
    counter.send_to(output)
    output.receive # => 2
  }
}

Error handling done right

Inko uses a form of error handling inspired by Joe Duffy's excellent article "The Error Model". Errors are represented using the algebraic type "Result", and Inko provides syntax sugar in the form of try and throw to make error handling easy. Critical errors that can't/shouldn't be handled are supported in the form of "panics", which abort the program when they occur.

For example, here's how you'd handle errors when opening a file and calculating its size:

import std.fs.file.ReadOnlyFile
import std.stdio.STDOUT

class async Main {
  fn async main {
    let size =
      ReadOnlyFile
        .new('README.md')             # => Result[ReadOnlyFile, Error]
        .then fn (file) { file.size } # => Result[Int, Error]
        .unwrap_or(0)                 # => Int

    STDOUT.new.print(size.to_string) # => 1099
  }
}

Efficient

Inko aims to be an efficient language, though it doesn't aim to compete with low-level languages such as C and Rust. Instead, we aim to provide a compelling alternative to the likes of Ruby, Erlang, and Go.

Inko uses a native code compiler, using LLVM as its backend, and aims to provide a balance between fast compile times and good runtime performance. The native code is statically linked against a small runtime library written in Rust, which takes care of scheduling processes, non-blocking IO, and provides various low-level functions.

Pattern matching

Inko supports pattern matching on a variety of types, such as tuples and algebraic data types:

match [10, 20].opt(1) {
  case Some(number) -> number # => 20
  case None -> 0
}

match (10, 'hello') {
  case (10, 'hello') -> 'foo'
  case (20, _) -> 'bar'
  case _ -> 'baz'
}

You can also match against literals such as integers and strings, and against regular classes:

class Person {
  let @name: String
  let @age: Int
}

let alice = Person { @name = 'Alice', @age = 42 }

match alice {
  case { @name = name } -> name # => 'Alice'
}

Pattern matching is compiled down to decision trees, and the compiler tries to keep their sizes as small as possible. The compiler also ensures that all patterns are covered.