Safe and concurrent object-oriented programming, without the headaches.

Inko is a gradually-typed, safe, object-oriented programming language for writing concurrent programs. By using lightweight isolated processes, data race conditions can not occur. The syntax is easy to learn and remember, and thanks to its error handling model you will never have to worry about unexpected runtime errors.

Install Get started Git repository
import std::stdio::stdout

# This will print "Hello, world!" to STDOUT.
stdout.print('Hello, world!')
import std::process::(self, Process)
import std::stdio::stdout

let child = process.spawn {
  let parent = process.receive as Process
  let message = process.receive as String

  # This will print "ping" to STDOUT.
  stdout.print(message)

  parent.send('pong')
}

child.send(process.current)
child.send('ping')

let response = process.receive as String

# This will print "pong" to STDOUT.
stdout.print(response)
object CustomArray!(T) {
  def init {
    let @values: Array!(T) = []
  }

  def push(value: T) -> T {
    @values.push(value)
  }
}

let array = CustomArray.new

# This is OK:
array.push(10)

# This will produce a type error:
array.push('oops')
import std::error::StandardError
import std::stdio::stderr

def withdraw(euros: Integer) !! StandardError {
  euros.negative?.if_true {
    throw StandardError
      .new('Invalid number of Euros!')
  }

  # ...
}

# "withdraw" might throw a "StandardError",
# so we *must* use "try" when using "withdraw".
try withdraw(euros: 5)

# We can also handle the error, if needed:
try withdraw(euros: 5) else (error) {
  stderr.print(error.message)
}

# "try!" terminates the program upon
# encountering an error:
try! withdraw(euros: 5)
import std::fs::file
import std::stdio::stdout

# This will open a file in read-only
# mode. The use of "try!" causes the
# program to abort (= a panic) if the
# file could not be opened.
let readme =
  try! file.read_only('README.md')

let content =
  try! readme.read_string

stdout.print(content)
import std::fs::file

let readme =
  try! file.read_only('README.md')

# This will produce a type error, since
# the file is opened in read-only mode.
readme.write_string('oops')

# This also won't work, because we can
# not remove a read-only file.
readme.remove
import std::test
import std::test::assert

test.group 'Integer.+', do (group) {
  group.test 'Summing two Integers', {
    try assert.equal(1 + 2, 3)
  }
}

test.run
import std::conversion::ToString
import std::stdio::stdout

object Person impl ToString {
  def init(name: String) {
    let @name = name
  }

  def to_string -> String {
    @name
  }
}

let person = Person.new('Alice')

# This will print "Alice" to STDOUT:
stdout.print(person)
def static(a: Integer, b: Integer) -> Integer {
  a + b
}

def dynamic(a, b) {
  a + b
}

# This is OK, and will produce "7":
static(2, 5)

# This will produce a type error:
static(2, 'oops')

# This is OK, because the arguments
# of "dynamic" are dynamically typed:
dynamic(2, 5)

# This is also OK:
dynamic('hello', ' world')

# This will produce a runtime error,
# because `Integer + String` is not valid:
dynamic(2, 'oops')
# ?User means we may return a User, or Nil.
def find(email: String) -> ?User { }

def update(user: User) { }

let user = find('alice@example.com')

# This will return the username if a User
# was returned, or Nil if "user" is Nil:
user.username

# This will produce a type error, because
# User doesn't respond to "oops":
user.oops

# This won't work, because "update" takes a User, and not a ?User.
update(user)

user.if_true {
  # `*user` tells the compiler to treat "user" as a User, not a ?User.
  update(*user)
}
# Inko supports tail call elimination, so this
# method will not overflow the call stack.
def fact(number: Integer, acc = 1) -> Integer {
  number.zero?.if_true {
    # This will return from the method, also
    # known as a "block return".
    return acc
  }

  fact(number - 1, acc * number)
}

fact(15) # => 1307674368000
import std::stdio::stdout

let mut number = 0

{ number < 10 }.while_true {
  stdout.print('This loop will run 10 times')

  number += 1
}

{
  stdout.print('This is an infinite loop.')
}.loop
import std::stdio::stdout

let numbers = [10, 20, 30]

# "each" allows us to easily iterate over
# a collection:
numbers.each do (number) {
  stdout.print(number)
}

# Using "iter" we can obtain an external,
# composable iterator:
let new_numbers = numbers
  .iter
  .map do (num) { num * 2 }
  .to_array

new_numbers # => [20, 40, 60]

Features

  • Lightweight processes that don't share memory.
  • Object-oriented.
  • Composition instead of inheritance.
  • Explicit handling of exceptions at the call site.
  • Gradually typed.
  • Parallel garbage collection per process.
  • Bytecode that is OS and architecture independent.
  • Requires only 5MB of disk space for the runtime and VM.
  • Free and Open Source, forever.
  • Built-in unit testing framework.

Supported platforms

  • Linux.
  • Windows 10 or newer.
  • Mac OS Sierra or newer.
  • FreeBSD, OpenBSD, and others should work, but are not officially supported at this time.
  • Only x86-64 platforms are officially supported.

Sponsors