A function names a reusable transformation and states the types that cross its boundary. Start with a concrete named function. Use a lambda for small behavior passed as a value, and introduce a type parameter only when one relationship should work for more than one type.
Run the progression in one program
Save this as functions-generics.tpz:
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)Check it before running:
topaz check functions-generics.tpz
topaz run functions-generics.tpz
The result is:
5
Topaz
Label { text: docs }Begin with a named function
applyTwice names two parameters and an int result. Its last expression supplies that result, so a written return is unnecessary. Use return when an early exit makes the control flow clearer.
The step parameter has type (int) -> int: callers must provide a callable that accepts and returns an int. increment is a lambda with that shape. A lambda uses =>, can be stored like another value, and captures visible bindings under the closure rules.
Add a type parameter when the relationship repeats
firstOr<T> does not care whether the array contains strings, integers, or another type. It only promises that the elements, fallback, and result share one type T. The call supplies strings, so inference chooses T = string; the caller does not need to write a type argument.
This is rank-1 polymorphism: the named function is generic, and each call chooses one concrete instantiation. Prefer it when the same algorithm preserves a real type relationship. Do not make a function generic merely to avoid choosing a type.
Add a bound only when the body needs an operation
showValue<T: Show> calls Show.show, so its body needs proof that T conforms to the static Show protocol. Label derives Show provides that nominal conformance. The bound does not mean “anything that happens to look printable”; structural values and primitives do not gain protocol conformance automatically.
Bounds are an escalation from an ordinary generic, not the default. A named generic function may require an order-independent conjunction such as T: Show + JSON when its body genuinely uses both protocols.
Choose the smallest function form
| Need | Choose | Signal to the reader |
|---|---|---|
| Reusable behavior with a stable name | A concrete named function | Parameters and result form a public contract. |
| Short behavior passed or stored as a value | A lambda | The surrounding context supplies concrete parameter types. |
| One algorithm preserving a relationship across types | An unbounded named generic | The same T connects inputs and result. |
| A generic body that calls a static protocol operation | A bounded named generic | The bound lists exactly the operations the body may require. |
Calls, defaults, and variadics
Parameters are immutable. A default value is evaluated when its argument is omitted and must be a literal or const expression; it cannot refer to another parameter. A variadic tail is written ...args: T, must be final, and appears inside the function as Array<T>.
A call evaluates its callee once and then evaluates positional and spread arguments from left to right. Named arguments follow positional and spread arguments. Array spread contributes only to a variadic tail; it does not fill an arbitrary fixed parameter.
Inference is the ordinary generic call path. Where exact explicit type arguments are admitted, the list must be complete and must agree with the value arguments and expected result. Partial decoration is a static error.
Common mistake: reaching for a generic lambda
Anonymous behavior is a lambda whose parameter types become concrete from context. Type-parameter binders and protocol bounds belong to named function declarations. If behavior needs its own reusable generic contract, give it a name and declare that contract once.
Exact limits
- Generic functions are rank-1; higher-rank function values remain deferred.
- Lambdas, callable types, aliases, nominal declarations, receiver methods, and protocol methods do not declare bounds.
- Generic lambdas, default type parameters, variance annotations, dynamic protocol dispatch, and arbitrary trait constraints remain deferred.
- A generic body has exactly its declared bounds and cannot silently use a stronger operation.
For the first function and lambda lesson, revisit Values and Functions. Use Types for callable and generic type shapes, and Bindings, Scope & Closures for capture and lifetime.