A function defines a reusable transformation and specifies the types crossing its boundary. Start with a concrete named function. Use a lambda for concise behavior passed as a value, and introduce a type parameter only when a single relationship applies across multiple types.
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.tpzThe result is:
5
Topaz
Label { text: docs }Begin with a named function
applyTwice specifies two parameters and an int result. Its final expression provides that result, making an explicit return statement unnecessary. Use return when an early exit clarifies control flow.
The step parameter has type (int) -> int. Callers must provide a callable that accepts and returns an int. increment is a lambda matching that shape. A lambda uses =>, can be stored like any other value, and captures visible bindings under closure rules.
Add a type parameter when the relationship repeats
firstOr<T> operates regardless of whether the array contains strings, integers, or another type. It only guarantees that the elements, fallback, and result share the single type T. The call supplies strings, so type inference selects T = string. The caller does not need to specify a type argument.
This demonstrates rank-1 polymorphism. The named function is generic, and each invocation selects a concrete instantiation. Prefer generic functions when the algorithm preserves a true 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 requires proof that T conforms to the static Show protocol. Label derives Show provides that nominal conformance. The bound does not apply to arbitrary printable values. Structural values and primitives do not gain protocol conformance automatically.
Bounds represent an escalation beyond standard generics rather than 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 reference 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.
Type inference is the standard generic call path. Where explicit type arguments are permitted, the list must be complete and 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 bindings and protocol bounds belong to named function declarations. If behavior requires 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 initial function and lambda lesson, revisit Values and Functions. Use Types for callable and generic type shapes, and Bindings, Scope & Closures for capture and lifetime.