Data & Control

Functions & Generics

Choose named functions, lambdas, and generics, then read their inputs, results, and protocol bounds.

Choose a named function, a lambda, or a generic function for the work you need to reuse. The example shows how input and result types connect these forms.

Install Topaz using First Program. If function declarations are new to you, begin with Values and Functions.

Run three function forms

Save this code as functions-generics.tpz in your working folder.

TOPAZ
record Label derives Show {
    text: string,
}

function applyTwice(value: int, step: (int) -> int) -> int {
    step(step(value))
}

function firstOr<T>(values: Array<T>, fallback: T) -> T {
    match values {
        case [] => fallback
        case [first, ..] => first
    }
}

function showValue<T: Show>(value: T) -> string {
    Show.show(value)
}

let increment: (int) -> int = value => value + 1
let first: string = firstOr(["Topaz", "Classic"], "none")
let shown = showValue(Label { text: "docs" })

print("{applyTwice(3, increment)}")
print(first)
print(shown)

In that folder, check the file and run it.

BASH
topaz check functions-generics.tpz
topaz run functions-generics.tpz

The check succeeds with exit code 0. The program prints these three lines.

Output
5
Topaz
Label { text: docs }

Pass a function as an argument

applyTwice takes an integer and a function, then returns an integer. Its step parameter has type (int) -> int, so the supplied function must accept and return an int.

increment is a lambda, an unnamed function written with =>. It adds one to its input. The call applyTwice(3, increment) computes increment(increment(3)), which gives 5.

The final expression of a function body supplies its result. Use return to leave earlier. A lambda can be stored or passed as a value and can capture surrounding bindings. See Bindings, Scope & Closures for a counter that keeps its value between calls.

Reuse one function with different types

firstOr<T> returns the first array element, or fallback when the array is empty. T is a type parameter. It says that the array elements, fallback, and result have the same type.

The example passes a string array, so Topaz infers T = string and returns "Topaz". Changing that array to [] returns "none". The fallback still supplies the type string.

This is rank-1 polymorphism. Type parameters belong to the named function, and each call determines their types. A lambda receives concrete parameter types from context. Use a named generic function when the same algorithm works with different types, as firstOr does.

Require an operation with a bound

showValue calls Show.show to turn its argument into a string. Show is a static protocol, a named set of operations whose implementation the compiler checks. The bound T: Show requires the argument type to implement it.

Label derives Show supplies that implementation for Label. This is nominal conformance, established for the declared type. A primitive, structural record, function, or collection does not acquire it just by having a similar operation.

Keep T: Show while showValue calls Show.show. Removing the bound makes that call a static error. For a body that uses both protocols, write T: Show + JSON. Both bounds are required, and their order does not matter.

Choose a function form

What you needWhat to use
Reusable behavior with known input and result typesA concrete named function
Behavior passed or stored as a valueA lambda
One algorithm relating inputs and results across typesA named generic function
Protocol operations inside a generic bodyA named generic function with the required bounds

Call functions with defaults or extra arguments

Parameters are immutable. A default value is evaluated when its argument is omitted. It must be a literal or const expression and cannot refer to another parameter.

For a variable number of arguments, write the final parameter as ...args: T. Inside the function, args has type Array<T>.

Call ruleWhat happens
Function and argument evaluationThe callee is evaluated once, then positional and spread arguments from left to right
Named argumentsThey follow positional and spread arguments
Array spreadIt supplies the variadic tail and cannot fill fixed parameters
Explicit type argumentsSupply the complete list in declaration order, matching the values and expected result

Generic calls usually infer their type arguments. A partial explicit list is a static error wherever explicit type arguments are supported.

Supported generic declarations

Declare a function's own type parameters and protocol bounds on a named function. Lambdas receive concrete types from their context and cannot declare their own type parameters.

  • Lambdas, function types, aliases, nominal declarations, receiver methods, and protocol methods cannot declare bounds.
  • A generic body can use the operations permitted by its declared bounds. An operation requiring a stronger bound is a static error.
  • Higher-rank polymorphism, generic lambdas, default type parameters, variance annotations, dynamic protocol dispatch, and arbitrary trait constraints are not supported.

Use Types to choose input and result types. Continue to Bindings, Scope & Closures to follow values captured by a function.