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.

Inko runs on 64 bits Linux, BSD, Mac OS, and Windows. 32 bits platforms may work, though they are not officially supported at this time.

Install Get started
import std::stdio::stdout

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

let pid = process.spawn {
  let message = process.receive as String

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

  process.send(pid: 0, message: 'pong')
}

process.send(pid: pid, message: 'ping')

let response = process.receive as String

# This will print "pong" to STDOUT.
stdout.print(response)
import std::process

let sender = process.channel lambda (receiver) {
  let number = receiver.receive

  # Here the compiler knows that
  # "number" is always an Integer:
  number + 5
}

# This is OK:
sender.send(2)

# This will produce a type error:
sender.send('oops')
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]

Concurrent

Writing concurrent programs with Inko is easy. Its lightweight processes allow you to run many processes concurrently, without having to worry about using too many resources.

Processes don't share their memory, and communicate by passing messages, which are deep copied. This removes the need for explicit synchronisation, and makes data race conditions impossible.

Object-oriented

Inko is an object-oriented programming language, drawing heavy inspiration from Smalltalk, Self, and Ruby. There are no statements used for conditionals and loops, instead Inko uses message passing for (almost) everything. This allows objects to control the behaviour of these kind of expressions.

Named objects can be defined, similar to classes. Traits can be used to define reusable behaviour and required methods. Inheritance is not supported, preventing objects from being coupled together too tight.

Safe

Inko's error handling model forces you to handle runtime exceptions, such as network timeouts, at the call site. This prevents exceptions from occurring in unexpected places.

Critical errors, such as division by zero errors will terminate the program immediately, known as a panic. By using panics for critical errors, instead of exceptions, the amount of exceptions that need to be handled is drastically reduced.

Gradually typed

Inko is gradually typed, with static typing being the default. This gives you the safety of a statically typed language, and the option to exchange this for the flexibility of a dynamically typed language.

The use of gradual typing allows you to scale from a simple prototype, all the way to a large scale project, without having to switch to a different programming language.

Garbage collected

Inko uses a high performance parallel garbage collector, based on Immix. Each process is garbage collected independently, removing the need for a global stop-the-world phase. For most processes, garbage collection should take no more than a few milliseconds.

Interpreted

Inko is an interpreted programming language, with a bytecode virtual machine written in Rust. Bytecode is portable between CPU architectures and operating systems, removing the need for compiling your program for different architectures.