SCUA

Manual

Functions and closures

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() where f returns one value leaves b as nil.
  • 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 assignmentlet 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.