Documentation / Functions

Functions

Moth functions use named declarations rather than anonymous closures or general function values. A signature records each parameter's type and access mode, then lists every success and error return.

Calls stay local and explicit. The source shows named arguments, mutable access and failure propagation where they occur.

Choose the explanation level: Function declarations

Function declarations

A function gives a reusable block of code a name.

greet |name String|:
    io.line([: Hello, [name]])
;

The function name comes first. Parameters live inside |...|.

Use || when there are no parameters:

say_hello ||:
    io.line("Hello")
;

Add an arrow when the function returns a value:

double |value Int| -> Int:
    return value * 2
;

The colon opens the body. The semicolon closes it.

A source function forms a named top-level declaration.

greet |name String|:
    io.line([: Hello, [name]])
;

double |value Int| -> Int:
    return value * 2
;

Declaration shape

name |parameters|:
    body
;

name |parameters| -> ReturnType:
    body
;
  • The function name comes first.
  • Parameters are enclosed by |...|.
  • || declares a function with no parameters.
  • -> introduces one or more return slots.
  • Omit the arrow only when the function has no return slots at all.
  • A function with only an error slot still writes -> Error!.
  • Void is not written as a return type.
  • : opens the body and ; closes it.
  • Function names use regular_snake_case.

Functions with required success values must return or otherwise terminate on every reachable path. Terminality is validated before HIR lowering.

Generic functions

Declaration-site generic parameters appear after the function name.

identity type A |value A| -> A:
    return value
;

Generic inference and bounds are documented by Generics.

Function values

Source functions are named declarations and cannot currently be used as ordinary values. General closures, anonymous function values, generic function values and higher-order polymorphism are outside scope. A narrower future design for references to named, monomorphic, non-capturing functions remains an open design question and has no current syntax or implementation commitment.

Choose the explanation level: Parameters and defaults

Parameters and defaults

Parameters describe the values a function needs.

greet |name String, excited Bool|:
    if excited:
        io.line([: Hello, [name]!])
    ;
;

Each parameter has a name and a type.

A default makes a parameter optional at the call site:

describe |prefix String = "item", subject String| -> String:
    return [prefix, ": ", subject]
;

label = describe(subject = "apple")

The call supplies subject by name and keeps the default prefix.

Every function parameter has a name and an explicit type.

describe |prefix String, subject String| -> String:
    return [prefix, ": ", subject]
;

Parameter forms

name Type
name ~Type
name Type = default
  • name Type declares a shared parameter.
  • name ~Type declares a mutable or exclusive parameter.
  • = default provides a value when the caller omits that slot.
  • Parameter names follow the no-shadowing rule.
  • Each parameter name must be unique in the signature.

Reactive parameters use $Type and are documented by Reactivity.

Defaults

A default belongs to the declaration.

describe |prefix String = "item", subject String| -> String:
    return [prefix, ": ", subject]
;

A caller can omit a defaulted parameter.

Named arguments can skip an earlier default:

label = describe(subject = "apple")

A positional call cannot skip a slot in the middle. Use a named argument when the call needs to reach a later parameter.

Default value constraints

A parameter default must be a compile-time value.

Accepted:

DEFAULT_PREFIX #= "item"

describe |prefix String = DEFAULT_PREFIX, subject String| -> String:
    return [prefix, ": ", subject]
;

Rejected:

  • runtime bindings
  • runtime function calls
  • references to other parameters

Struct fields share the same = default syntax, but struct defaults have their own compile-time rule. A const record uses the same |...| parameters list with a value required for every parameter, and it does not declare a constructable type. See Const records. Choice payload fields do not support defaults.

Choose the explanation level: Calls and access

Calls and access

Call a function by writing its name followed by arguments.

result = double(21)

Arguments are positional by default. They can also name their parameter:

label = describe(subject = "apple")

Positional arguments come first. Named arguments come afterward.

Mutable parameters need explicit access to an existing mutable value:

count ~= 1
increment(~count)

Fresh values do not use ~:

increment(1)

Function calls use positional arguments, named arguments or both.

render_card(title, body)
render_card(title = title, body = body)
render_card(title, body = body)

Argument routing

  • Positional arguments must come before the first named argument.
  • A positional argument is invalid after a named argument.
  • A named argument must match a real parameter name.
  • Each parameter may be supplied once.
  • Omitted defaulted parameters use their declared default.
  • Every non-defaulted parameter must receive an argument.
  • Extra positional arguments are invalid.
  • Argument values are checked and contextually coerced against their parameter types.

Named arguments can skip earlier defaults.

User functions, struct constructors, choice payload constructors and source-authored receiver methods share this routing policy.

Host functions and compiler-owned builtin member calls are positional-only.

Shared parameters

A shared parameter receives an ordinary argument without ~.

show |value String|:
    io.line(value)
;

show("hello")

Passing ~value to a shared parameter is invalid.

Mutable parameters

An existing mutable place needs explicit ~ at the call site.

increment |value ~Int|:
    value += 1
;

count ~= 1
increment(~count)

Passing an existing place without ~ to a mutable parameter is invalid.

A fresh rvalue can satisfy a mutable parameter without source ~:

increment(1)

Literals, templates, constructor calls and computed values are fresh. Writing ~ on them is invalid because ~ is place-only access syntax.

Binding mutability and call-site access are separate. See Mutable bindings.

Choose the explanation level: Returns and multiple values

Returns and multiple values

Use return to send a value back to the caller.

double |value Int| -> Int:
    return value * 2
;

A function can return more than one success value:

pair || -> String, Int:
    return "Priya", 2
;

name, count = pair()

The number of receiving names must match the number of returned success values.

Functions that can fail add one final error return marked with !. The Errors page explains how to propagate or recover from that path.

return produces the success values declared by a function.

double |value Int| -> Int:
    return value * 2
;

Success returns

  • Return values must match the declared success-slot count.
  • Each return value must be compatible with its corresponding slot type.
  • A function with required success values must return or otherwise terminate on every reachable path.
  • A function with no success slots may reach the end of its body.
  • return with no values can leave a no-success function early.
  • Void is not an explicit return-slot type.

Multiple success values

A function may declare and return more than one success value.

pair || -> String, Int:
    return "Priya", 2
;

name, count = pair()

The receiving multi-bind must match the success arity.

Multi-bind targets may continue on a following line only after a comma:

name,
    count = pair()

A comma promises another target, so a trailing comma before = is invalid. This is the same continuation rule used by delimiter-free dependency selections. Delimited collections and call arguments keep their own list grammar.

Multiple return values are not a user-visible tuple value. A normal single-target declaration cannot receive a multi-return call.

Value-producing if, full matches and catch blocks can also satisfy multi-value receiving sites when every producing path has the required arity.

Error slot shape

A function signature may contain at most one error-channel slot marked with !.

That slot must be last.

load |id String| -> String, Error!:
    if id is "":
        return! Error("Missing id")
    ;

    return "value"
;

return!, postfix propagation and catch are documented by Errors, options and assertions.

Return contract

Return slots contain types and channels only. Source code has no borrowed, owned, move or parameter-alias return annotation.

Returning an existing value follows ordinary shared-reference semantics. The compiler infers freshness and alias effects from validated bodies and calls. Public interfaces preserve those inferred relationships. External bindings may declare equivalent alias behaviour because their foreign bodies are unavailable.

Binding freshness and allocation identity

Every function result enters a fresh caller binding. This does not imply a fresh allocation. Returning an existing record, collection, map, string allocation, opaque handle or projection preserves its allocation identity and lifetime owner. The result binding never points at the callee's parameter or local binding slot.

Reassigning one binding does not rebind another binding that received the same allocation through a return. The allocation remains alive while any legal alias retains it.

Ordinary return does not copy. Use copy when the caller needs an independent result graph.

Inferred alias relationships govern borrow and lifetime validation without changing the source type.

External alias metadata describes whether a foreign result aliases an argument allocation. It does not expose binding references to source code and does not allow host code to return a Moth binding cell.

Fallible and multiple returns

A fallible carrier is fresh while its success payload may alias a parameter allocation. Multiple return values may alias parameters or each other. Each returned value enters its own caller binding. Runtime lowering preserves those allocation relationships. Current analysis may conservatively treat projected results as sharing any parameter root returned by the function. This can produce a stricter alias diagnostic than more precise per-result analysis would require.