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.
Memory safety
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:
import std.sync (Promise)
type async Counter {
let mut @value: Int
fn async mut increment {
@value += 1
}
fn async get(promise: uni Promise[Int]) {
promise.set(@value)
}
}
type async Main {
fn async main {
let counter = Counter(value: 0)
counter.increment
counter.increment
await counter.get # => 2
}
}
Batteries included
Inko provides a "batteries included" standard library, reducing the amount of third-party dependencies necessary. For example, here's a simple HTTP server that only depends on the standard library:
import std.net.http.server (Handle, Request, Response, Server)
type async Main {
fn async main {
Server.new(fn { recover App() }).start(8_000).or_panic
}
}
type App {}
impl Handle for App {
fn pub mut handle(request: mut Request) -> Response {
Response.new.string('Hello, world!')
}
}
HTTP clients are also provided, with transparent support for HTTP and HTTPS:
import std.net.http.client (Client)
import std.stdio (Stdout)
import std.uri (Uri)
type async Main {
fn async main {
let client = Client.new
let uri = Uri.parse('https://inko-lang.org').or_panic
let res = client.get(uri).send.or_panic
let buf = ByteArray.new
let _ = res.body.read_all(buf).or_panic
Stdout.new.print(buf)
}
}
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)
type async Main {
fn async main {
let size = ReadOnlyFile
.new('README.md'.to_path) # => Result[ReadOnlyFile, Error]
.then(fn (file) { file.metadata }) # => Result[Metadata, Error]
.map(fn (meta) { meta.size }) # => Result[Int, Error]
.or(0)
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:
type async Main {
fn async main {
match [10, 20].get(1) {
case Ok(number) -> number # => 20
case _ -> 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:
type Person {
let @name: String
let @age: Int
}
type async Main {
fn async main {
let alice = Person(name: 'Alice', age: 42)
match alice {
case { @name = name } -> name # => 'Alice'
}
}
}
Or using let:
type Person {
let @name: String
let @age: Int
}
type async Main {
fn async main {
let { @name = name } = Person(name: 'Alice', age: 42)
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.