Functions
Functions are values, definitions are equations, and give
hands back the result.
Defining functions
A function is written as an equation. When the body is a single expression, the definition fits on a line:
func double(x) = x * 2
func add(x, y) = x + y
print(double(21)) # 42
print(add(40, 2)) # 42
A block body groups statements in braces. The block's value is its last expression, so short bodies often need no explicit return at all:
func factorial(n) = {
result := 1
for i in 1..n { result *= i }
result
}
print(factorial(5)) # 120
give
give expr returns a value and exits the function immediately;
give alone returns null. It is J2's return, named
for what a function does with an answer. Early gives keep conditions
shallow:
func classify(n) = {
if n < 0 { give "negative" }
if n == 0 { give "zero" }
give "positive"
}
print(classify(-3)) # negative
print(classify(8)) # positive
Two properties make give more than a spelling change. It unwinds the
entire function at once, straight out of nested loops:
func find_pair(target) = {
for a in 1..10 {
for b in 1..10 {
if a * b == target { give fmt("{} x {}", a, b) }
}
}
give "none"
}
print(find_pair(12)) # 2 x 6
And it is not an error, so no try can accidentally swallow it; a
give inside a try block returns from the function as usual
(see Error Handling).
Recursion
Functions call themselves freely, and may call functions defined later in the file, so mutual recursion needs no forward declarations:
func is_even(n) = {
if n == 0 { give true }
give is_odd(n - 1)
}
func is_odd(n) = {
if n == 0 { give false }
give is_even(n - 1)
}
print(is_even(10)) # true
Pattern clauses
A function may be defined in several clauses, where a parameter position holds a literal instead of a name. Calls dispatch to the first clause whose literals match, which lets base cases read like the mathematics they came from:
func fact(0) = 1
func fact(n) = n * fact(n - 1)
print(fact(5)) # 120
Clauses merge by name and are tried in definition order, so put the specific cases first and the general case last.
Arity
Calls must supply exactly the declared parameters; too few or too many is an error. There are no default parameter values and no variadic user functions. When a family of calls wants optional configuration, pass a map.
Lambdas
An anonymous function is func without a name, in expression position.
Lambdas are the everyday companions of the higher-order builtins:
nums = [1, 2, 3, 4, 5]
squares = map(nums, func(x) = x * x)
evens = filter(nums, func(x) = x % 2 == 0)
total = reduce(nums, 0, func(a, b) = a + b)
desc = sort_by(nums, func(a, b) = b - a)
print(squares) # [1,4,9,16,25]
print(evens) # [2,4]
print(total) # 15
print(desc) # [5,4,3,2,1]
Closures
A lambda captures the bindings it references from the surrounding scope. The capture is a snapshot taken when the lambda is created: later changes to the original binding do not reach into the closure. Returning lambdas from functions is the standard way to build configured behavior:
func make_adder(base) = func(x) = x + base
add5 = make_adder(5)
add10 = make_adder(10)
print(add5(3)) # 8
print(add10(3)) # 13
Functions are values
Named functions can be passed, stored, and applied like anything else. The pipe and filter operators from Operators exist precisely because functions travel well:
func shout(w) = upper(w) + "!"
words = ["hey", "ho"]
print(words >> shout) # [HEY!,HO!]
apply_twice = func(f, x) = f(f(x))
func inc(n) = n + 1
print(apply_twice(inc, 40)) # 42
Typed signatures
Parameters and return values may carry type annotations. Annotated code behaves identically; the point is performance. A fully annotated function gives the native compiler enough certainty to produce specialized machine code and to prove loops and calls safe for automatic parallelization.
func axpy(y: seq<float>, x: seq<float>, a: float) -> nil = {
for i in 0..(len(y) - 1) {
y[i] = y[i] + a * x[i]
}
}
ys := make_seq(5, 1.0)
xs := make_seq(5, 2.0)
axpy(ys, xs, 10.0)
print(ys) # [21,21,21,21,21]
The type language: int, float, bool,
text, nil (no useful return value),
seq<T>, map<K, V>, pair<A, B>,
and function types like func(float) -> float. Function-typed parameters
make higher-order code eligible for native lowering too:
func twice(f: func(float) -> float, x: float) -> float = f(f(x))
func halve(v: float) -> float = v / 2.0
print(twice(halve, 12.0)) # 3
Generics
A function may take type parameters in angle brackets. The native compiler monomorphizes each use, so a generic kernel is as fast as a hand-specialized one:
func dot<T>(a: seq<T>, b: seq<T>) -> T = {
s := a[0] * b[0]
for i in 1..(len(a) - 1) { s += a[i] * b[i] }
give s
}
print(dot([1, 2, 3], [4, 5, 6])) # 32, integers in, integer out
print(dot([0.5, 1.5], [2.0, 4.0])) # 7, floats in, float out