Procedures & Scope
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.