Intermediate

Functions in Icon

Learn how to define and use procedures in Icon - parameters, return values, recursion, scope, and the generators that make Icon unique

In Icon, the unit of reusable code is the procedure. There is no separate function keyword - a procedure is defined with procedureend, and whether it behaves like a “function” (returns a value) or a “subroutine” (acts for its side effects) depends entirely on how you write its body.

What makes procedures in Icon genuinely different from those in most languages is that they participate in goal-directed evaluation. A procedure call doesn’t just return a value - it can succeed with a value, fail and produce nothing, or suspend and act as a generator that produces a whole sequence of values on demand. This single mechanism replaces a lot of machinery that other languages handle with exceptions, optionals, and iterator protocols.

This tutorial covers the everyday parts of writing procedures - parameters, return values, recursion, and scope - and then shows how fail and suspend turn a plain procedure into something Icon-specific. Procedures are also first-class values: you can store one in a variable and call it later.

Defining and Calling Procedures

A procedure lists its parameters in parentheses and hands a value back with return. Arguments that the caller omits are simply &null, and the /name := value idiom is the conventional way to supply a default - the unary / operator succeeds only when its operand is null, so the assignment happens only for missing arguments.

Create a file named functions.icn:

procedure main()
    write(greet("World"))
    write(greet())            # name is omitted -> uses the default
    write("Sum: ", add(3, 4))
end

procedure greet(name)
    /name := "World"          # assign only if name is null
    return "Hello, " || name || "!"
end

procedure add(a, b)
    return a + b
end

greet and add are ordinary value-returning procedures. Notice that write itself takes multiple arguments and concatenates them, so write("Sum: ", add(3, 4)) prints the label and the result together.

Recursion

Because a procedure can call itself, recursion works just as you’d expect. The classic factorial and Fibonacci definitions read naturally in Icon, and the every/to combination from earlier tutorials makes it easy to drive them over a range.

Create a file named recursion.icn:

procedure main()
    every n := 1 to 6 do
        write(n, "! = ", factorial(n))
    write("fib(10) = ", fib(10))
end

procedure factorial(n)
    if n <= 1 then return 1
    return n * factorial(n - 1)
end

procedure fib(n)
    if n < 2 then return n
    return fib(n - 1) + fib(n - 2)
end

Variable Scope

Identifiers inside a procedure are local by default. Three declarations let you change that:

  • global - declared outside any procedure; visible everywhere.
  • local - the default; a fresh value each time the procedure is entered.
  • static - retained between calls, paired with initial to run setup code exactly once.

Create a file named scope.icn:

global counter

procedure main()
    counter := 0
    every 1 to 3 do
        write("Count: ", increment())

    every 1 to 3 do
        write("Tick: ", tick())
end

procedure increment()
    counter +:= 1            # modifies the global
    return counter
end

procedure tick()
    static n
    initial n := 0           # runs only on the first call
    n +:= 1
    return n
end

The tick procedure keeps its own private running total in n. Even though n is initialized to 0, the initial clause guarantees that only happens once, so the value survives across every call.

Failure and Generators

This is where Icon procedures stop resembling those in other languages. Instead of return, a procedure body can use fail to produce no value at all, or suspend to yield a value while remaining ready to produce more. A procedure that runs off its end without a return also fails.

Create a file named generators.icn:

procedure main()
    # suspend turns squares() into a generator
    every write(squares(5))

    # Goal-directed evaluation: keep generating until one value exceeds 10
    write("First square > 10: ", 10 < squares(5))
end

procedure squares(n)
    local i
    every i := 1 to n do
        suspend i * i        # yield each square, then resume
end

In every write(squares(5)), the generator produces 1, 4, 9, 16, 25 and each is written in turn. In 10 < squares(5), Icon tries successive squares until the comparison succeeds - 1, 4, and 9 fail the test, 16 passes. A comparison operator yields the value of its right operand on success, so writing it as 10 < squares(5) (generator on the right) makes the expression yield 16 - the square that passed.

First-Class Procedures and Safe Failure

A procedure name is just a value, so you can assign it to a variable and call through that variable. And because failure is a normal outcome, a procedure can signal “no result” with fail instead of raising an error - the caller tests success with if.

Create a file named procedures_fc.icn:

procedure main()
    op := add                # store a procedure in a variable
    write("Via variable: ", op(10, 5))

    if result := safe_divide(10, 2) then
        write("10 / 2 = ", result)

    if not safe_divide(10, 0) then
        write("Division by zero failed safely")
end

procedure add(a, b)
    return a + b
end

procedure safe_divide(a, b)
    if b = 0 then fail       # no value, no error
    return a / b
end

safe_divide never crashes on a zero denominator; it simply fails, and if not detects that cleanly. This pattern - a procedure that fails rather than throws - is everywhere in idiomatic Icon.

Running with Docker

Each example compiles to a stand-alone executable named after its source file. Build and run them with the official Icon image:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Pull the official image
docker pull codearchaeology/icon:latest

# Basic procedures
docker run --rm -v $(pwd):/app -w /app codearchaeology/icon:latest sh -c "icont functions.icn && ./functions"

# Recursion
docker run --rm -v $(pwd):/app -w /app codearchaeology/icon:latest sh -c "icont recursion.icn && ./recursion"

# Scope
docker run --rm -v $(pwd):/app -w /app codearchaeology/icon:latest sh -c "icont scope.icn && ./scope"

# Generators
docker run --rm -v $(pwd):/app -w /app codearchaeology/icon:latest sh -c "icont generators.icn && ./generators"

# First-class procedures and failure
docker run --rm -v $(pwd):/app -w /app codearchaeology/icon:latest sh -c "icont procedures_fc.icn && ./procedures_fc"

Expected Output

functions.icn:

Hello, World!
Hello, World!
Sum: 7

recursion.icn:

1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
fib(10) = 55

scope.icn:

Count: 1
Count: 2
Count: 3
Tick: 1
Tick: 2
Tick: 3

generators.icn:

1
4
9
16
25
First square > 10: 16

procedures_fc.icn:

Via variable: 15
10 / 2 = 5
Division by zero failed safely

Key Concepts

  • One keyword for everything - procedureend defines both value-returning functions and side-effecting subroutines; the difference is in the body, not the declaration.
  • return, fail, and suspend - a procedure can return one value, fail and produce nothing, or suspend to yield a sequence; running off the end is an implicit failure.
  • Generators are just procedures - suspend inside a loop makes a procedure produce multiple values, consumed naturally by every or by goal-directed contexts like 10 < squares(5).
  • Default parameters via /name := value - omitted arguments arrive as &null, and the / operator lets you fill them in only when they’re missing.
  • Three scopes - local (default, per-call), global (program-wide), and static with initial (private state retained across calls).
  • Failure replaces exceptions - a procedure that can’t produce a result uses fail, and callers check with if, avoiding error-handling boilerplate.
  • Procedures are first-class values - assign a procedure to a variable and call through it, enabling higher-order patterns.

Running Today

All examples can be run using Docker:

docker pull codearchaeology/icon:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining