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 procedure…end, 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 withinitialto 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:
| |
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 -
procedure…enddefines both value-returning functions and side-effecting subroutines; the difference is in the body, not the declaration. return,fail, andsuspend- a procedure can return one value, fail and produce nothing, or suspend to yield a sequence; running off theendis an implicit failure.- Generators are just procedures -
suspendinside a loop makes a procedure produce multiple values, consumed naturally byeveryor by goal-directed contexts like10 < 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), andstaticwithinitial(private state retained across calls). - Failure replaces exceptions - a procedure that can’t produce a result uses
fail, and callers check withif, 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
Comments
Loading comments...
Leave a Comment