Spec chapter 05
Functions and Closures
Declaration
Function Add(a: Int, b: Int): Int
Return a + b
End Function
Function Log(msg: String) // no return type = returns nothing
Print(msg)
End Function
@Inline Function Square(x: Float): Float => x * x // expression-bodiedNamed functions are declared at the top level of a file, not inside other blocks. Lambdas (below) cover functions inside functions.
Parameters
Function Spawn(pos: Vec3, hp: Int = 100, name: String = "grunt"): Enemy // defaults
Spawn(Vec3(0, 1, 0), name: "boss") // named args
Function Swap(ByRef a: Int, ByRef b: Int) // pass by reference
Function Sum(nums: Int...): Int // variadic; nums is Int[]- Parameters are read-only inside the function. To change one, copy it into a
Local. - Default values must be known at compile time.
Variadic parameters
nums: Int...takes the rest of a call’s arguments as anInt[]:Sum(),Sum(1), andSum(1, 2, 3)pack theirs into a new array. One argument that’s an array already, aT[]or an array literal, is passed as it is:Sum(xs). Named, it takes an array:Sum(nums: xs).- Only the last parameter can take the rest of the arguments. It can’t be
ByRefor have a default, since without any it’s an empty array, and an Operator can’t have one. - A generic one infers its type argument from each argument:
Largest(1.5, 2, 3)withrest: T...givesFloat. values: Printable..., with std.core’s Printable, takes values of any types a string can show, mixed, and each becomes its text at the call, as"{x}"makes it, sovaluesis aString[]: that’s howPrint(score, name)takes them. An array argument is a value like any other, so it’s shown; named,values:takes aString[]of texts. A value a string can’t show, like a T?, is an error that names the function: “String? can’t be shown by ‘Print’, since it may be Null; give a value for Null with ’??’”.values: Formattable..., with std.core’s Formattable, takes values of any Formattable types, mixed, as Format’s does, sovaluesis aFormattable[]: an object goes in as it is, and a value that isn’t one, a number, Bool, String, Enum, or Struct, in an object of std’s that holds it, whose Format and ToString are its value’s. OneFormattable[]argument is passed whole. A value of a type that isn’t Formattable is an error that names the parameter: “P isn’t Formattable, which ‘values’ takes”.- Where two overloads fit a call as well as each other, one that takes the arguments as they are beats one that packs them into its variadic parameter, as C#‘s do.
ByRef
A ByRef parameter is the caller’s own variable: assigning to it changes the caller’s
variable. The call says ByRef too, so it’s visible that the call can change it:
Swap(ByRef x, ByRef y)
Swap(x, y) // ✗ error: Swap takes a ByRef; write ByRef x- Only something assignable can be passed
ByRef: aLocal, aGlobal, aByRefparameter, or a field of one. Not aConst, a literal, a loop variable, a read-only parameter, a computed expression, or an array’s element, which can move when the array grows. - The argument’s type must match the parameter’s exactly, and it must already be assigned.
- The same variable can’t be passed
ByReftwice in one call. - A
ByRefparameter can’t have a default value, can’t be captured by a closure, and can’t be passed to another thread.
Overloading
Functions may be overloaded by parameter types, or by how many type parameters of their own they have (see 07-generics.md).
Function Print(value: Int)
Function Print(value: String)When several overloads fit a call, one that matches every argument’s type exactly beats one that needs a lossless conversion. If that still leaves more than one, the call is ambiguous and an error.
A literal argument matches its default type (Int or Float) exactly, and any other type it
fits by conversion. So with Show(v: Long) and Show(v: Double), Show(5_000_000_000) is
ambiguous; write 5_000_000_000 As Long.
Returns
- A function with a return type must return a value on every path; reaching the end without one is a compile error.
- A function without a return type returns nothing. It can use a bare
Return, but notReturn value.
Tuples and multiple returns
Function MinMax(xs: Int[]): (Int, Int)
...
Return (lo, hi)
End Function
Local lo, hi = MinMax(data) // destructuring
Local pair = MinMax(data) // (Int, Int)
Print("{pair.0}") // elements are numbered from 0
a, b = (b, a) // swap- A tuple type is two or more types in parentheses:
(Int, String),(Int, Int)?,(String, Int)[].(T)is justT. - A tuple is a value, copied on assignment like a Struct (see 08). Its elements
can be of any type but
Weak<T>. (a, b)makes one. Where a tuple is expected, each value converts to its element’s type:Local p: (Long, String?) = (1, Null). Any other tuple converts only to its own type, or to itsT?.t.0,t.1, and so on are its elements, which can be assigned.t.0.1is an element of an element.Local lo, hi = valuedeclares a Local for each element, of the element’s type. There’s a name for every element, and_skips one. Only aLocaltakes a tuple apart.a, b = value, or(a, b) = value, assigns each element to a target, converting it to the target’s type. The whole value is made first, then each target is assigned, from the left, soa, b = (b, a)swaps.==and!=compare element by element, when every element’s type has them.- A tuple shows in a string as its elements:
"{(1, "a")}"is(1, a).
Calls
- Parentheses are always required, including for zero-argument calls and calls used
as statements. Blitz-style command calls (
Graphics3D 800, 600) do not exist.
Entry point
- Top-level statements in the main file are the program’s entry point (script style, as in Blitz3D).
- There is no special
Mainfunction:Function Main()is an ordinary function. - A bare
Returnin top-level code ends the program early.
Function types and lambdas
Local twice = Function(x: Int) => x * 2 // expression lambda
Local cb: Func(Int): Int = twice
button.OnClick = Function() // block lambda
Print("clicked")
End FunctionLambdas are always written with Function: Function(x) => expr, or a block ending in
End Function. There is no () => shorthand.
Func(A, B): Ris the type of a function value that takes anAand aBand returns anR;Func(A)returns nothing. A?or[]after the return type belongs to it, so a Func that can be Null is(Func(): Int)?, and an array of them is(Func(): Int)[].- A parameter without a type, like the
xinFunction(x) => x * 2, takes the type that the Func expected there has. With no Func expected, it needs one. Passed to overloads, the lambda fits only those whose parameter there is a Func it can be, asReplace(text, Function(m) => ...)picks Regex’s Replace that takes one over the one that takes a String. - A lambda returns what its
: Typesays, or else what the Func expected says. Without either,=> valuereturns the value’s type, and a block returns nothing. Returnin a lambda leaves the lambda.BreakandContinuecan’t leave it, and neither can an error: a Func doesn’t throw, so the lambda catches what it calls.- A block lambda’s body ends statements at line breaks, even inside the brackets of a call.
- A Function’s name alone is a function value:
Local f: Func(Int): Int = Twice. With overloads, the Func expected picks one. Methods, built-in functions, and Functions that throw or take a ByRef parameter aren’t values; a lambda can call them. - A function value is called like a function,
f(1), with its arguments in order. It has no==, but aFunc?compares withNull.
Sendable Func
Sendable Func(A): R is a function value that other threads can call, as Swift’s @Sendable
closures are (see 10):
Function ParallelMap<T: Sendable, R: Sendable>(items: T[], f: Sendable Func(T): R): R[]- A lambda or a Function’s name written where one is expected is one. What the lambda captures
can’t change and is Sendable, as for Spawn’s work: a Const, a parameter, or a loop variable
holding a Sendable value, or a Self that’s Sendable. Its code is checked as work on another
thread, so it can’t use a Global or call a
@MainThreadFunction, and an Async lambda, whose code runs on the main thread, isn’t one. - Its parameters and result can be of any type, since each call runs on the thread that makes it.
- It’s Sendable itself, so a Shared Class’s ReadOnly Field, a Channel, or a Mutex can hold one, and Spawn’s work and a Parallel For’s body can call one. Spawn and Thread.Start take one as their work too.
- It converts to the Func of its parameters and result, but a Func doesn’t convert to it, since a Func’s closure stays on the thread that made it: “expected Sendable Func(Int32): Int32, found Func(Int32): Int32, which other threads can’t call”.
- Its closure counts its owners atomically, as a Shared Class’s objects do, so whichever thread lets go last frees it.
Closures
A closure is a function that captures variables from the scope where it was created.
Function MakeCounter(): Func(): Int
Local count = 0
Return Function() // captures count
count++
Return count
End Function
End Function
Local next = MakeCounter()
Print(next()) // 1
Print(next()) // 2- Variables are captured by reference (the closure and the outer scope share them).
- A closure is a refcounted object. A captured
Locallives in a cell that the closure and its scope share. A parameter, a loop variable, aConst, andSelfcan’t change, so the closure copies them: each pass of aForgives a lambda its own loop variable. - A
ByRefparameter can’t be captured, since the caller’s variable may be gone when the lambda runs. Neither canSelfin a Struct’sNew, which isn’t made yet. - Using a field or Method by name in a lambda captures
Self. - A null check doesn’t narrow a captured
Localinside the lambda, and once a lambda that assigns aLocalexists, no check outside narrows it either: any call could change it.
Capture lists
Under refcounting, a closure stored in an object that captures that object creates a cycle that is never freed. Capture lists (C++-style) control how values are captured:
OnOpen = Function[Weak Self]() => Self?.PlaySound() // Self may be gone, hence ?.[Weak x]capturesxas a weak reference (its type becomesx?inside the closure). It refers to the objectxholds when the closure is made, and like anyWeak<T>, a check can’t narrow it.- With
[Weak Self], fields and Methods are reached throughSelf?., not by name. - The compiler warns when a closure stored in a field captures
Selfstrongly.
C callbacks
Closures cannot be passed to C. FFI callbacks use Callback(...), which accepts only
non-capturing functions. See 12-ffi.md.
Generic functions
Function Max<T: Comparable>(a: T, b: T): TSee 07-generics.md.
Report an issue with this page or with FutureBlitz on GitHub.