Skip to main content

Functions

Jade functions are defined with fn, accept zero or more parameters, and return a value either with an explicit return statement or via implicit return (the last bare expression in the body). Functions are first-class values and can be passed to other functions or assigned to variables.

Overview

A function is a named, reusable block of statements. It is introduced with the fn keyword, followed by a name, a parenthesized parameter list, and a brace-delimited body. Calling a function evaluates its body in a new scope and returns a value.

There are two ways to return a value from a function:

  • Explicit return: return <expr> exits immediately and produces that value.
  • Implicit return: if the last statement in the body is a bare expression (no return keyword), that expression's value is returned automatically.

If execution reaches the end of the body without hitting a return statement and without a final bare expression, the function returns nil. A bare return with no expression also produces nil.

Functions are first-class values in Jade. A function definition binds the name to a fn value in the current environment, just like let binds a name to an integer or float. That value can be stored in a variable, passed as an argument, and called through any expression that evaluates to a function.

Syntax

Function Definition

fn <name>(<param>, <param>, ...) {
<body statements>
return <expr>
}
  • <name> — an identifier naming the function; binds it in the enclosing scope.
  • <param> — zero or more parameter names separated by commas; each becomes a local variable inside the body.
  • <body statements> — any sequence of statements. If the last statement is a bare expression, its value is returned implicitly.
  • return <expr> — exits immediately and produces the given value. A bare return produces nil.

Function Call

<expr>(<arg>, <arg>, ...)
  • <expr> — any expression that evaluates to a function value.
  • <arg> — zero or more argument expressions evaluated left-to-right in the caller's scope.

Basic Examples

A function with two parameters

fn add(a, b) {
return a + b
}

let sum = add(3, 4)

Implicit return (last expression)

fn double(x) {
x * 2
}

print(double(5)) // 10

The last statement is the bare expression x * 2. Because it is not a let, if, or other statement, its value is automatically returned. This is equivalent to writing return x * 2.

A function with no parameters

fn get_answer() {
return 42
}

let answer = get_answer()

The empty parameter list () is required even when there are no parameters.

Chaining calls

fn square(x) {
return x * x
}

let chained = add(square(2), square(3))

Call expressions can be nested. square(2) evaluates to 4, square(3) evaluates to 9, and add(4, 9) returns 13.

Advanced Examples

Recursion — factorial

fn factorial(n) {
if n == 0 {
return 1
}
return n * factorial(n - 1)
}

let f5 = factorial(5)

Functions can call themselves. factorial(5) computes 5 * 4 * 3 * 2 * 1 = 120. Mutual recursion also works because function definitions are bound before either is called.

First-class functions — higher-order functions

fn double(x) {
return x * 2
}

fn apply(f, x) {
return f(x)
}

fn compose(f, g, x) {
return f(g(x))
}

let f = double
let a = f(5)
let b = apply(double, 6)
let d = compose(double, double, 3)

double is assigned to f, which can then be called as f(5). apply receives a function as its first argument and calls it with the second. compose(double, double, 3) returns 12.

Fibonacci — two recursive calls

fn fib(n) {
if n <= 1 {
return n
}
return fib(n - 1) + fib(n - 2)
}

let fib10 = fib(10)

fib(10) returns 55.

Error Conditions

ErrorTriggerExample
NotCallableCalling a non-function valuelet x = 5 then let y = x(1)
ArityMismatchWrong number of argumentsfn add(a, b) { return a + b } then add(1)
NestedFunctionDefining a function inside another function bodyfn outer() { fn inner() { return 1 } return 2 }
ReturnOutsideFunctionUsing return at the top levelreturn 1
UndefinedVariableReferencing a name not in scopefn f() { return z } where z is not defined

Built-in Functions

Jade provides a small set of global built-in functions that are always in scope.

FunctionSignatureDescription
printprint(value)Writes value to stdout followed by a newline. Accepts any type.
writewrite(str)Writes a string to stdout without a trailing newline and flushes immediately.
lenlen(value)Returns the number of elements in a str, array, or dict.
inputinput() / input(prompt)Reads one line from stdin and returns it as a str. If prompt is given, prints it to stdout (no newline) before reading. Returns "" on EOF.
// print vs write
print("hello") // hello\n
write("hello") // hello (no newline, flushed immediately)

// len on different types
let s = "jade"
print(len(s)) // 4

let arr = [1, 2, 3]
print(len(arr)) // 3

// input — read from stdin
let name = input("Enter your name: ")
print("Hello, " + name)

// input with no prompt
let line = input()

Closures

A closure is an anonymous function written inline using the |params| body syntax. It captures a snapshot of the variables visible at the point it is created, so it can reference those variables even when called later from a different scope.

Syntax

|<param>, <param>, ...| <expr> // single-expression body
|<param>, <param>, ...| { <stmts> } // block body
|| <expr> // zero-parameter closure

Basic examples

// Single-expression body — implicitly returns the expression
let double = |x| x * 2
print(double(5)) // 10

// Multiple parameters
let add = |x, y| x + y
print(add(3, 4)) // 7

// Zero parameters
let greet = || "hello"
print(greet()) // hello

Capturing outer variables

All variables visible at closure creation are captured by copy. The closure carries its own snapshot — mutations inside the closure do not affect the outer scope.

let multiplier = 3
let triple = |x| x * multiplier
print(triple(5)) // 15

Block body

let abs_val = |x| {
if x < 0 {
return -x
}
return x
}
print(abs_val(-7)) // 7
print(abs_val(4)) // 4

Closures as arguments

Closures are first-class values and can be passed to higher-order functions:

fn apply(f, x) {
return f(x)
}

let result = apply(|x| x * x, 6) // 36
note

The | symbol that opens a closure is only treated as a closure delimiter when it appears at the start of an expression (primary position). In all other positions it remains the bitwise OR operator. The empty-param form uses ||, which is distinct from the logical OR operator because || only appears as logical OR in an infix position, never at the start of an expression.