Control flow

Inko has the following control flow constructs: if, and, or, while, loop, try, and throw.

Conditionals

For conditionals we use if:

import std.stdio (STDOUT)

class async Main {
  fn async main {
    let out = STDOUT.new
    let num = 42

    if num == 42 { out.print('yes') } else { out.print('no') }
  }
}

When you run this program, the output is "yes". If you change the value of num to e.g. 50, the output is instead "no".

Inko also supports else if like so:

import std.stdio (STDOUT)

class async Main {
  fn async main {
    let out = STDOUT.new
    let num = 50

    if num == 42 {
      out.print('A')
    } else if num == 50 {
      out.print('B')
    } else {
      out.print('C')
    }
  }
}

The output of this program is "B".

To perform boolean AND and OR operations, you can use the and and or keywords:

import std.stdio (STDOUT)

class async Main {
  fn async main {
    let out = STDOUT.new
    let num = 50

    if num == 42 or num == 50 {
      out.print('A')
    } else if num >= 10 and num <= 20 {
      out.print('B')
    } else {
      out.print('C')
    }
  }
}

This prints "A" if you run the program as-is, and "B" if you change num to 20.

Loops

Inko has two types of built-in loops: unconditional loops which use the loop keyword, and conditional loops that use the while keyword.

Here we use a conditional loop to print a number 10 times:

import std.stdio (STDOUT)

class async Main {
  fn async main {
    let out = STDOUT.new
    let mut num = 0

    while num < 10 {
      out.print(num.to_string)
      num += 1
    }
  }
}

The output of this program is as follows:

0
1
2
3
4
5
6
7
8
9

The following program loops indefinitely, printing an ever increasing number every 500 milliseconds:

import std.process (sleep)
import std.stdio (STDOUT)
import std.time (Duration)

class async Main {
  fn async main {
    let out = STDOUT.new
    let mut num = 0

    loop {
      out.print(num.to_string)
      num += 1
      sleep(Duration.from_millis(500))
    }
  }
}

You can control the iteration of a loop using the next and break keywords: next jumps to the start of the next loop iteration, while break jumps out of the inner-most loop:

import std.stdio (STDOUT)

class async Main {
  fn async main {
    let out = STDOUT.new

    loop {
      out.print('hello')
      break
    }
  }
}

This program prints "hello", then stops the loop.

Various types in the standard library also provide ways of looping. For example, Int.times takes a closure that it calls N times:

10.times(fn (i) {
  i # => 0, 1, 2, ...
})

Similarly, we can use Int.until to create an exclusive range (or Int.to for an inclusive range) that we can then iterate over:

3.until(5).iter.each(fn (i) {
  i # => 3, 4
})

throw

throw takes an expression and wraps it in the Error constructor of the std.result.Result enum, then returns it:

fn example -> Result[Int, String] {
  throw 'oh no!'
}

This is the equivalent of the following:

fn example -> Result[Int, String] {
  return Result.Error('oh no!')
}

The throw keyword is only available in methods of which the return type is a Result.

try

try takes an expression of which the type is either std.result.Result or std.option.Option, and gets it. If the value is a Result.Error or an Option.None, the value is returned as-is.

Consider this example of using try with an Option value:

let value = Option.Some(42)

try value

This is the equivalent of:

let value = Option.Some(42)

match value {
  case Some(v) -> v
  case None -> return Option.None
}

And when using a Result:

let value = Result.Ok(42)

try value

This is the equivalent of:

let value = Result.Ok(42)

match value {
  case Ok(v) -> v
  case Error(e) -> return Result.Error(e)
}

Conditional moves

If a variable is dropped conditionally, it's not available afterwards:

let a = [10]

if something {
  let b = a
}

# `a` _might_ be moved at this point, so we can't use it anymore.

The same applies to loops: if a variable is moved in a loop, it can't be used outside the loop:

let a = [10]

loop {
  let b = a
}

Any variable defined outside of a loop but moved inside the loop must be assigned a new value before the end of the loop. This means the above code is incorrect, and we have to fix it like so:

let mut a = [10]

loop {
  let b = a

  a = []
}

We can do the same for conditions:

let mut a = [10]

if condition {
  let b = a

  a = []
}

# `a` can be used here, because we guaranteed it always has a value at this
# point

If a value is moved in one branch of a condition, it remains available in the other branches:

let a = [10]

# This is fine, because only one branch ever runs.
if foo {
  let b = a
} else if bar {
  let b = a
}