04 — Control flow

Previous: Values and functions

日本語版: 04_control_flow.vibe.md

Everything here is an expression: if produces a value, and so does a loop. That is worth getting used to early, because it removes most of the reasons other languages need a mutable variable.

if

fn main with Console {
  let v = if 1 < 2 {
    "yes"
  } else {
    "no"
  }
  println("v = \{v}")
}
v = yes

Both branches must produce the same type, since the whole if is one value.

while and return

while is the loop you already know. return leaves the function — not just the loop — which is what you want for a search:

fn find_first_neg(arr: Array[Int]) -> Int {
  let mut i = 0
  while i < Array::length(arr) {
    if Array::get(arr, i) < 0 {
      return i
    }
    i = i + 1
  }
  return -1
}

fn main with Console {
  println("find_first_neg([3, 1, -2, 5]) = \{find_first_neg([3, 1, -2, 5])}")
  println("find_first_neg([1, 2]) = \{find_first_neg([1, 2])}")
}
find_first_neg([3, 1, -2, 5]) = 2
find_first_neg([1, 2]) = -1

loop — a loop that carries values

while needs a mutable counter. loop does not: it declares parameters, continue supplies the next round's values, and break ends it with a result.

fn main with Console {
  let sum = loop (i = 0, acc = 0) {
    if i >= 10 {
      break acc
    }
    continue (i + 1, acc + i)
  }
  println("sum = \{sum}")
}
sum = 45

The rule to remember: `continue` takes one value per loop parameter, `break` takes one result. A bare continue repeats with everything unchanged. If you give continue the wrong number of values the compiler says so and names both counts.

Since break takes a single result, returning two things means returning a tuple:

fn main with Console {
  let r = loop (i = 0, acc = 0) {
    if i >= 3 {
      break (acc, i)
    }
    continue (i + 1, acc + i)
  }
  println("r = (\{r.0}, \{r.1})")
}
r = (3, 3)

for ... in collects

A for-in is an expression too, and it evaluates to the Array of its body's results. Add a name before the element to get the index:

fn main with Console {
  let doubled = for x in [1, 2, 3] {
    x * 2
  }
  let with_index = for i, x in [10, 20] {
    i + x
  }
  println("doubled = [\{Array::get(doubled, 0)}, \{Array::get(doubled, 1)}, \{Array::get(doubled, 2)}]")
  println("with_index = [\{Array::get(with_index, 0)}, \{Array::get(with_index, 1)}]")
}
doubled = [2, 4, 6]
with_index = [10, 21]

|>

x |> f is f(x), which lets a transformation read left to right instead of inside out:

import @vibe/builtin {
  trait Iterator
}

fn pair(a: Int, b: Int) -> Int {
  a * 10 + b
}

fn main with Console {
  let trimmed_len = "  hi  " |> String::trim |> String::length
  let arr_len = [1, 2, 3] |> Array::length
  let mapped = [1, 2, 3] |> Iterator::map(_, _ * 2)
  let repeated = 7 |> pair(_, _)
  println("trimmed_len = \{trimmed_len}")
  println("arr_len = \{arr_len}")
  println("mapped = [\{Array::get(mapped, 0)}, \{Array::get(mapped, 1)}, \{Array::get(mapped, 2)}]")
  println("repeated = \{repeated}")
}
trimmed_len = 2
arr_len = 3
mapped = [2, 4, 6]
repeated = 77

By default the piped value becomes the first argument. To put it somewhere else, mark the position with a bare _: x |> f(a, _) is f(a, x), and a slot may repeat, so 7 |> pair(_, _) is pair(7, 7).

A _ inside a larger expression is a different thing — the lambda shorthand from the last chapter. That is why Iterator::map(_, _ * 2) has two of them doing unrelated jobs: the first is the pipe slot, the second is (v) -> v * 2.

Next: Types and strings.