Control Flow

Control flow in Nim is ordinary and expression-oriented at the same time: the same keywords both steer the program and produce values. This lesson covers branching, the two loop forms, and the idioms that keep loops short and auditable.

Branching

Two constructs cover all decisions. Use if when the condition is a boolean expression, and case when you are selecting between known alternatives — especially enumerations, where the compiler can prove you handled every value.

if / elif / else

let temperature = 21

if temperature > 30:
  echo "hot"
elif temperature > 15:      # 'elif' chains without extra nesting
  echo "mild"              # this branch runs: 21 > 15
else:
  echo "cold"

# Conditions are plain booleans; there is no truthiness for integers.
let enabled = true
if enabled and temperature < 25:
  echo "comfortable"

case, Ranges and Sets

case is exhaustive for enumerations and supports ranges and sets of values, which turns a chain of comparisons into a table of openings.

type Grade = enum gA, gB, gC, gF

proc points(g: Grade): int =
  case g                    # no 'else' needed: every enum value is covered
  of gA: 4                  # each branch is an expression returning an int
  of gB: 3
  of gC: 2
  of gF: 0

echo points(gB)             # 3

proc classify(ch: char): string =
  case ch
  of 'a' .. 'z': "lower"    # ranges are allowed in case labels
  of 'A' .. 'Z': "upper"
  of '0' .. '9': "digit"
  of '_', '-': "separator"  # a list of alternatives
  else: "other"             # exhaustive checks end here, so 'else' is required
echo classify('7'), " ", classify('_')

Loops

while repeats until a condition fails; for walks a collection or an iterator. In practice for is used far more often, because it cannot run away with an index.

while Loops

var fuel = 3
while fuel > 0:             # condition checked before every iteration
  echo "burning, fuel=", fuel
  dec fuel                  # decrement by one — clearer than fuel = fuel - 1

# A 'while true' loop needs an explicit exit; name the loop when you break it.
var attempts = 0
block retry:
  while true:
    inc attempts
    if attempts >= 3:
      break retry            # leaves the labeled block, not just the loop
echo attempts                # 3

for Loops and Iterators

The for loop drives iterators, so the same syntax walks ranges, sequences, tables and files. Use countup and countdown when you need a step other than one.

for i in 0 .. 4:            # inclusive range: 0 1 2 3 4
  echo i                    # echo ends the line; use stdout.write to stay inline

for i in countup(0, 10, 2): # explicit step: 0 2 4 6 8 10
  echo i

for i in countdown(3, 1):   # descending
  echo i

let names = @["Ada", "Grace", ""]
for name in names:          # walking a sequence yields values, not indices
  if name.len == 0:
    continue                # skip this item, keep iterating
  echo "name: ", name

# When the index matters, iterate with an index operator over a range.
for i in 0 ..< names.len:   # '..<' is the half-open operator: no off-by-one
  echo i, " -> ", names[i]

Control Flow as Expressions

Because the branches of if, case and block are expressions, they can be assigned, returned or passed as arguments. Prefer this over declaring a mutable variable that is then filled in.

Assigning a Decision

import std/strformat
proc describe(count: int): string =
  # No temporary variable: the function body IS the expression.
  if count == 0: "empty"
  elif count == 1: "single"
  else: &"{count} items"     # interpolation needs the import above
echo describe(0), "/", describe(1), "/", describe(5)

Early Exit with a Value

A named block combined with break value gives you a structured early return inside an expression, which keeps the success path first and the fallback last.

proc firstDigit(words: seq[string]): int =
  block search:
    for word in words:
      for ch in word:               # nested loop, still one exit point
        if ch in '0' .. '9':
          break search (ord(ch) - ord('0'))   # yields a value from the block
    -1                              # fallback: nothing numeric was found

echo firstDigit(@["age:", "42"])    # 4
echo firstDigit(@["nope"])          # -1

# Cases that must not fall through use 'return' explicitly, which is fine too:
proc clamp(value, low, high: int): int =
  if value < low: return low
  if value > high: return high
  value
echo clamp(150, 0, 100)             # 100

Practice

Write a fizzbuzz for 1 to 20 using for and one if/elif chain, then rewrite it so the body is a single case expression over (n mod 3, n mod 5). Comparing both versions shows why expression-oriented control flow compresses code without hiding logic. Next: Procedures & Scope.