Procedures & Scope

A Nim program is a tree of procedures, and the language gives them two unusual powers: they can be overloaded by their argument types, and they can be called as if they belonged to their first argument. This lesson covers definitions, parameters, overloading and the scope rules that decide which name you are using.

Defining Procedures

A procedure is declared with proc, a name, a parameter list in parentheses, and a return type. Everything after the colon is the body.

Parameters and Defaults

proc greet(name: string, greeting = "Hello"): string =
  ## Parameters with a default may be omitted at the call site.
  greeting & ", " & name & "!"

echo greet("Ada")                    # Hello, Ada!
echo greet("Ada", "Hi")              # Hi, Ada!
echo greet(greeting = "Yo", name = "Bob")   # named arguments: order becomes free

# Parameters are immutable by default, so the caller's data is safe to pass.
proc length_(text: string): int = text.len
echo length_("four")                 # 4

Return Values

Every procedure has an implicit result variable of its return type. You can assign to it, return early with return, or write the body as a single expression.

proc sumTo(limit: int): int =
  result = 0                        # 'result' starts as the zero value of int
  for i in 1 .. limit:
    result += i                     # build the answer incrementally
echo sumTo(4)                       # 10

proc square(x: int): int = x * x    # expression body: no 'result', no 'return'
echo square(9)                      # 81

proc absolute(x: int): int =
  if x < 0: return -x               # early return is idiomatic for guards
  x
echo absolute(-3), " ", absolute(3)

Overloading and Operators

Several procedures may share a name if their parameter lists differ. The compiler picks the best match from the argument types, so you can give one natural name to several representations.

One Name, Several Signatures

proc area(side: int): int = side * side                       # square
proc area(width, height: int): int = width * height           # rectangle
proc area(radius: float): float = 3.14159 * radius * radius   # circle

echo area(3)          # 9      — square overload
echo area(3, 4)       # 12     — two-int overload
echo area(2.0)        # 12.566 — float overload

# Generic overloads keep one implementation for many element types.
proc twice[T](value: T): seq[T] = @[value, value]
echo twice(7), " ", twice("go")    # @[7, 7] @["go", "go"]

Operators Are Procedures

Operator symbols are just procedure names, so you can define one for your own type and it will participate in the same overload resolution as built-in operators.

import std/strformat

type Vector = object
  x, y: float

proc `+`(a, b: Vector): Vector = Vector(x: a.x + b.x, y: a.y + b.y)
proc `$`(v: Vector): string = &"({v.x}, {v.y})"   # '$' is the stringify operator

let total = Vector(x: 1.0, y: 2.0) + Vector(x: 3.0, y: 4.0)
echo total                 # (4.0, 6.0) — echo calls the '$' we just defined

Scope, Mutability and First-Class Procedures

Parameters are read-only inside the procedure, so a callee cannot mutate the caller's data by accident. Scopes are lexical: a name defined inside a block disappears at its end. Two features extend that model — var parameters for output, and procedure values for behaviour passed as data.

Output Parameters with var

A var parameter is passed by reference, so the procedure can modify the caller's variable. Nim also has out parameters, which additionally initialize the value to its default first — making the intent explicit for readers.

proc increment(counter: var int, amount = 1) =
  counter += amount          # writes through to the caller's variable

var hits = 0
increment(hits)              # no '&' or '*' at the call site
increment(hits, 5)
echo hits                    # 6

# 'out' parameters promise the callee writes a complete value, so the compiler
# can treat them as initialized and warn if you forget to assign.
proc divide(a, b: int, quotient, remainder: out int) =
  quotient = a div b
  remainder = a mod b
var q, r: int
divide(17, 5, q, r)
echo q, " remainder ", r     # 3 remainder 2

Local Scopes and Shadowing

let name = "outer"

block:
  let name = "inner"         # a new binding inside this block only
  echo name                  # inner

echo name                    # outer — the block binding is gone

# 'var' parameters and mutable locals both respect these scopes, so a helper
# can never accidentally capture a loop index from its caller.
proc double(value: int): int = value * 2
for i in 1 .. 2:
  echo double(i)             # 2 then 4

Procedures as Values

A procedure can be stored, passed and returned. proc types describe the signature; a captured local environment turns a procedure into a closure, allocated on the heap when needed.

proc applyTwice(f: proc (x: int): int, start: int): int =
  f(f(start))                # the callback is ordinary data to this procedure

proc double(x: int): int = x * 2
echo applyTwice(double, 3)   # 12

# A closure captures 'factor' from its surrounding scope.
proc makeScaler(factor: int): proc (x: int): int =
  result = proc (x: int): int = x * factor   # closure: {.closure.} by default

let times10 = makeScaler(10)
echo times10(4)              # 40

# Forward declaration: Nim allows using a name before its body when a
# declaration appears first — common for mutually recursive procedures.
proc isEven(n: int): bool     # forward declaration
proc isOdd(n: int): bool = n != 0 and isEven(n - 1)
proc isEven(n: int): bool = n == 0 or isOdd(n - 1)
echo isEven(4), " ", isOdd(7)  # true true

Practice

Write a procedure stats(values: seq[int], total, average: out float) that computes both results in one pass, then pass a callback to sort the sequence descending without writing a loop. When both compile, continue with Collections.