Functions are values in SCUA. You can declare them with a name, write one inline, return one from another function, and pass one as an argument. A function written inside another captures the variables around it, which is what makes closures work.
fn greet(name)
return `hello, {name}`
end
print(greet("Ed"))
Run it:
$ scua greet.scua
hello, Ed
#Declaring functions
A named function is fn name(params) ... end. Use
return to hand back a value. A function with no
return, or one that falls off the end, gives back
nil.
The body is a block, so any let you make inside is local
to the call.
#Functions are values
You can write a function without a name and bind it to a variable.
fn(params) ... end is an expression that evaluates to a
function value:
let square = fn(x) return x * x end
print(square(9))
Run it:
$ scua square.scua
81
A named declaration and a binding to a function value are close to interchangeable. Reach for the inline form when you're passing a function somewhere or building one on the fly.
#Closures
A function written inside another captures the surrounding variables by reference, not by copy. The inner function keeps working even after the outer call that created it has returned, and it sees the latest value of what it captured.
A counter makes this concrete. Each call to make_counter
creates a fresh n, and the returned function closes over
it:
fn make_counter()
let n = 0
return fn()
n = n + 1
return n
end
end
let next = make_counter()
print(next())
print(next())
print(next())
Run it:
$ scua counter.scua
1
2
3
The n outlives the call to make_counter.
Each call to next updates the same captured
n.
Two closures can capture the same variable, and then they share it. Here one closure writes and the other reads, and the reader sees the writer's updates:
let total = 0
let add = fn(amount) total = total + amount return total end
let peek = fn() return total end
add(10)
add(5)
print(peek())
Run it:
$ scua shared.scua
15
Both add and peek close over the same
total, so peek reports what add
accumulated. A top-level let is shared with named functions
in the same file the same way; see Variables.
#Recursion
A named function can call itself:
fn fact(n)
if n <= 1 then return 1 end
return n * fact(n - 1)
end
print(fact(5))
Run it:
$ scua fact.scua
120
#Passing functions as arguments
Because functions are values, a function can take another function as a parameter and call it. This is how higher-order helpers like map and filter work.
fn apply_twice(f, x)
return f(f(x))
end
print(apply_twice(fn(n) return n + 3 end, 10))
Run it:
$ scua higher-order.scua
16
The built-in list module leans on this for
map, filter, reduce, and friends.
See Collections.
#Returning several values
A function can return more than one value. List them after
return, separated by commas, and bind them on the other
side with a matching let list:
fn div_mod(a, b)
return a // b, a % b
end
let q, r = div_mod(17, 5)
print(`17 = {q} * 5 + {r}`)
Run it:
$ scua div-mod.scua
17 = 3 * 5 + 2
This is the natural shape for "a result and a flag" — a lookup that returns both what it found and whether it found anything:
fn find(items, want)
for i, x in items do
if x == want then return i, true end
end
return -1, false
end
let idx, ok = find([10, 20, 30], 20)
print(`found at {idx}? {ok}`)
$ scua find.scua
found at 1? true
#How the two sides line up
The left side has a fixed number of names; the right side produces some number of values. SCUA matches them up the same way Lua does:
- Too few values — the leftover names get
nil.let a, b = f()wherefreturns one value leavesbasnil. - Too many values — the extras are dropped.
let only = div_mod(9, 4)keeps just the first value.
#Parallel assignment
The same comma form assigns to variables that already exist. The whole right side is evaluated before anything is written, so you can swap without a temporary:
let a = "left"
let b = "right"
a, b = b, a
print(`{a} {b}`)
$ scua swap.scua
right left
The targets aren't limited to plain variables — a field or an array
element works too: pos.col, pos.row = 3, 4.
#Giving the returns types
Like every type in SCUA, the return is optional — but you can declare
it. A multi-value return is written -> (T, U):
fn div_mod(a: int, b: int) -> (int, int)
return a // b, a % b
end
let q, r = div_mod(17, 5)
let label: string = q
The checker then verifies the function actually returns two
ints, and — because the signature flows to the call site —
types q and r as int, so the
misuse on the last line is caught:
$ scua div-mod-typed.scua
scua: div-mod-typed.scua:5: type error: value is int, but the binding is declared string
A wrong slot type (return 1, "two" against
-> (int, int)) or the wrong number of values is reported
the same way. As always the annotation is erased before the program runs
— it changes what the checker accepts, not the behaviour. Leave it off
and the values are simply untyped (any), exactly like an
unannotated single return.
#The one rule to remember
A call only expands into multiple values at the tail of a
binding or assignment — let x, y = f() or
x, y = f(). Anywhere else, a call gives just its first
value. So print(div_mod(20, 6)) prints only the quotient,
and return f() forwards only f()'s first
value. When you need every value elsewhere, bind them first, then use
the names.
(If you'd rather hand back a single structured thing, returning an
array or a table still works — return [lo, hi] — and reads
back with indexing or field access.)
#Typed functions
Everything above works with no annotations at all. When you want a
compile-time check, you can give a function's parameters and return
value types, and you can type a binding or field as a function with a
signature like fn(int) -> int. The checker then verifies
that calls pass the right argument types and that a function value
matches the signature it's assigned to. Annotations are erased before
the program runs, so they change what the checker accepts, not how the
code behaves. See Records and gradual
types for the details.