Skip to main content

Functions

Functions in Mochi are values. They can be assigned to variables, passed as arguments, returned from other functions, and stored inside data structures. Parameter and return types are explicit, and every call is type-checked at compile time.

Declaring functions

Mochi uses fun to declare a function with a parameter list, a return type, and a brace body:

fun add(a: int, b: int): int {
return a + b
}

print(add(2, 3)) // 5

The return type follows the parameter list after a colon. The body is a block of statements, and a return supplies the value.

A function that returns nothing uses void:

fun greet(name: string): void {
print("hello, " + name)
}

Calling a void function in an expression position is a type error.

Arrow functions

When the body is a single expression, the arrow form is shorter:

let square = fun(x: int): int => x * x
let double = fun(x: int): int => x * 2

print(square(5)) // 25
print(double(7)) // 14

The right-hand side of => is the return value. An arrow function works anywhere a function value is expected and does not need a name.

[1, 2, 3] | map(fun(n: int): int => n * 10)

Functions as values

A function value can be stored in a let or var, passed as an argument, or returned from another function.

fun apply(f: fun(int): int, x: int): int {
return f(x)
}

let triple = fun(x: int): int => x * 3
print(apply(triple, 7)) // 21
print(apply(square, 9)) // 81

The type of a function is fun(<param types>): <return type>. The parameter f above has type fun(int): int.

Multiple parameters and named call sites

Mochi calls use positional arguments by default. For functions with several parameters of the same type, named arguments at the call site improve readability.

fun build_user(name: string, age: int, admin: bool): User {
return User { name: name, age: age, admin: admin }
}

let u = build_user(name: "Ada", age: 36, admin: false)

Named arguments must match parameter names exactly. A call may mix positional and named arguments as long as positional ones come first.

Default values

Parameters can have default values. A parameter with a default is optional at the call site.

fun http_get(url: string, timeout: int = 30): string {
// …
}

http_get("https://example.com")
http_get("https://example.com", timeout: 5)

Defaults are evaluated lazily at each call, so they can reference earlier parameters or globals. Avoid side effects you would not want repeated.

Varargs

A ... in front of a parameter type collects the remaining arguments into a list:

fun sum_all(values: ...int): int {
return values | reduce(0, fun(acc: int, n: int): int => acc + n)
}

print(sum_all(1, 2, 3, 4)) // 10

A function may have at most one varargs parameter, and it must be the last parameter in the signature.

To pass an existing list as varargs, prefix it with ... at the call site:

let xs = [1, 2, 3, 4]
print(sum_all(...xs)) // 10

Closures

Functions capture variables from the enclosing scope by reference. If the outer var changes, the closure sees the new value.

fun counter(): fun(): int {
var n = 0
return fun(): int => {
n = n + 1
return n
}
}

let next = counter()
print(next()) // 1
print(next()) // 2
print(next()) // 3

Closures cover stateful generators and event-handler patterns without a separate language feature.

Recursion

Functions can call themselves. Mochi does not perform tail-call optimization, so deeply recursive code may exhaust the stack.

fun fact(n: int): int {
if n <= 1 { return 1 }
return n * fact(n - 1)
}

print(fact(8)) // 40320

For unbounded depth, write the same algorithm with a loop and a var accumulator.

fun fact_iter(n: int): int {
var acc = 1
for i in 1..(n + 1) {
acc = acc * i
}
return acc
}

Mutual recursion works between top-level functions because top-level names are resolved together. Nested functions can refer to themselves but not to sibling locals declared later.

Higher-order helpers in the prelude

Several common higher-order functions ship in the prelude:

FunctionSignature
map(xs, f)(list<T>, fun(T): U): list<U>
filter(xs, p)(list<T>, fun(T): bool): list<T>
reduce(xs, init, f)(list<T>, U, fun(U, T): U): U
for_each(xs, f)(list<T>, fun(T): void): void
sort_by(xs, key)(list<T>, fun(T): K): list<T>

The pipeline operator | keeps chains readable:

let total =
[1, 2, 3, 4, 5]
| filter(fun(n: int): bool => n % 2 == 1)
| map(fun(n: int): int => n * n)
| reduce(0, fun(acc: int, n: int): int => acc + n)

print(total) // 35 (1 + 9 + 25)

Function types in signatures

A function can take or return other functions. The function type syntax is fun(<params>): <return>.

fun compose(f: fun(int): int, g: fun(int): int): fun(int): int {
return fun(x: int): int => f(g(x))
}

let inc = fun(x: int): int => x + 1
let double = fun(x: int): int => x * 2

let inc_then_double = compose(double, inc)
print(inc_then_double(3)) // 8 = (3 + 1) * 2

Type aliases shorten complex signatures. See types:

type IntFn = fun(int): int

fun compose(f: IntFn, g: IntFn): IntFn {
return fun(x: int): int => f(g(x))
}

Methods on types

A function declared inside a type body becomes a method. Methods reference the surrounding fields directly.

type Circle {
radius: float

fun area(): float {
return 3.14159 * radius * radius
}

fun scale(factor: float): Circle {
return Circle { radius: radius * factor }
}
}

let c = Circle { radius: 2.0 }
print(c.area()) // 12.56636
print(c.scale(2.0).area()) // 50.26544

There is no explicit self parameter. Read more in types.

Visibility

Functions declared at the top level of a file are visible to other files in the same package. To export a function so other packages can import it, prefix the declaration with export:

package mathutils

export fun add(a: int, b: int): int {
return a + b
}

fun internal_helper(): int {
return 0 // visible only inside the mathutils package
}

Read more in packages.

Common patterns

Early return for guards

fun find(xs: list<User>, id: int): User | nil {
for u in xs {
if u.id == id { return u }
}
return nil
}

Builder via partial application

fun bind_left(f: fun(int, int): int, a: int): fun(int): int {
return fun(b: int): int => f(a, b)
}

let add5 = bind_left(fun(a: int, b: int): int => a + b, 5)
print(add5(10)) // 15

Pipeline over a small DSL

fun normalize(s: string): string {
return s | trim | lower
}

print(normalize(" Hello WORLD ")) // "hello world"

trim and lower are in the prelude with signature fun(string): string, so they fit a pipeline directly.

Common errors

MessageCauseFix
missing return statementA non-void function has a path with no returnAdd a return on every path.
argument of type T does not match parameter UWrong call-site typeConvert the argument or change the parameter type.
function takes N arguments but was called with MArity mismatchUpdate the call. Defaults make trailing parameters optional.
cannot infer type of arrow functionBare fun(x) => … without annotationsAnnotate the parameters and the return type.

Next