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 GitHubimport std.fs.file (ReadOnlyFile)
class async Main {
fn async main {
# or_panic() gets the Result value, panicking
# with the given error message when
# encountering a `Result.Error`.
let file = ReadOnlyFile
.new('README.md'.to_path)
.or_panic("the file doesn't exist")
let bytes = ByteArray.new
file
.read_all(bytes)
.or_panic('failed to read the file')
bytes.to_string # => "# README ..."
}
}
class async Main {
fn async main {
let numbers = [10, 20, 30]
numbers
.opt(2)
.map(fn (n) { n.to_string }) # => Option.Some('30')
}
}
fn moves {
let numbers = [10, 20, 30]
# This is OK, and returns a `ref 10`.
numbers.get(0)
let nums = numbers
# This is no longer OK, as `numbers` gave up ownership to
# `nums`.
numbers.get(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.get(0)
borrowed.get(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)
}
import std.stdio (Stdout)
class async Main {
fn async main {
Stdout.new.print('Hello, world!')
}
}
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
}
}
}
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
}
}
import std.sync (Future, Promise)
class async Calculator {
fn async fact(size: Int, promise: uni Promise[Int]) {
let result = 1
.to(size)
.iter
.reduce(1, fn (product, val) { product * val })
promise.set(result)
}
}
class async Main {
fn async main {
let calc = Calculator()
# This calculates the factorial of 15 in the
# background, then we wait for the result to
# be sent back to us via a channel.
match Future.new {
case (future, promise) -> {
calc.fact(15, promise)
future.get # => 1307674368000
}
}
}
}
class Stack[T] {
let @values: Array[T]
fn static new -> Stack[T] {
Stack([])
}
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.get # `value` inferred as `Option[Int]`
}
}
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`.
}
}
}
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 {
'...'
}
}
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 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 get the Ok value if we're
# certain we'll never get an `Error` case:
div(10, 2).get # => 5
}
}
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.
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.
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:
import std.sync (Future, Promise)
class async Counter {
let @value: Int
fn async mut increment {
@value += 1
}
fn async get(promise: uni Promise[Int]) {
promise.set(@value)
}
}
class async Main {
fn async main {
let counter = Counter(value: 0)
counter.increment
counter.increment
match Future.new {
case (future, promise) -> {
counter.get(promise)
future.get # => 2
}
}
}
}
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'.to_path) # => Result[ReadOnlyFile, Error]
.then fn (file) { file.size } # => Result[Int, Error]
.or(0) # => Int
Stdout.new.print(size.to_string) # => 1099
}
}
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.
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.