Reading Type Signatures
Throughout this book and in the standard library documentation, you’ll encounter function type signatures. This guide will help you understand what they mean. Don’t try to memorize everything here - just use this as a reference when you encounter unfamiliar notation.
Basic Function Signatures
The simplest function signature looks like this:
val double: fn(x: i64) -> i64
This breaks down into:
fn(...)- this is a functionx: i64- takes one parameter namedxof type i64 (64-bit integer)-> i64- returns a value of type i64
Every positional parameter in a function type carries a name as well as a type. The name is documentation for the reader and is what your editor shows in hover and completion popups; it does not change the call (you still pass arguments positionally, in order).
Another example:
val concat: fn(a: string, b: string) -> string
Takes two strings, returns a string.
Type Parameters (Generics)
Type parameters (like generics in other languages) are written with a single quote followed by an identifier: 'a, 'b, 'e, etc.
Simple Type Parameters
val identity: fn(x: 'a) -> 'a
This means: “takes a value of any type 'a and returns a value of the same type 'a”. The identity function could work with integers, strings, or any other type.
val first: fn(a: Array<'a>) -> 'a
This means: “takes an array of any type 'a and returns a single element of type 'a”. If you pass an Array<string>, you get back a string. If you pass an Array<i64>, you get back an i64.
Multiple Type Parameters
val map: fn(a: Array<'a>, f: fn(x: 'a) -> 'b) -> Array<'b>
This function takes:
- An array of type
'a - A function that transforms
'ainto'b - Returns an array of type
'b
The types 'a and 'b can be the same or different.
Optional Labeled Arguments
Arguments prefixed with ?# are optional and labeled:
val text: fn(?#style: Style, content: string) -> Widget
This function can be called in two ways:
// style uses default value
text("Hello");
// style is specified
text(#style: my_style, "Hello")
Order Flexibility
Labeled arguments can be provided in any order, but must come before positional arguments:
val widget: fn(?#width: i64, ?#height: i64, content: string) -> Widget
// All of these are valid:
widget("text")
widget(#width: 100, "text")
widget(#height: 50, #width: 100, "text")
widget(#height: 50, "text")
Required Labeled Arguments
Arguments with # but no ? are required but labeled:
val input_handler: fn(
?#enabled: &[bool, null],
#handle: &fn(e: Event) -> [`Stop, `Continue],
child: &Tui
) -> Tui
You must provide #handle, but it doesn’t have to be in the first position. However, it must come before the unlabeled &Tui argument:
input_handler(#handle: &my_handler, &my_widget)
Variadic Arguments
The @args notation means a function accepts any number of arguments:
val sum: fn(@args: i64) -> i64
You can call this with any number of integers:
sum(1, 2, 3);
sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
Variadic with Required Arguments
Sometimes a function requires at least one argument:
val max: fn(first: 'a, @args: 'a) -> 'a
The first 'a is required, then any number of additional arguments of the same type.
Only a builtin takes a variadic argument: a lambda written in Graphix
with an @args parameter is refused.
Reference Types
An ampersand & before a type means “reference to” rather than the value itself:
val text: fn(content: &string) -> Widget
This takes a reference to a string, not the string value directly. References are important for:
- Efficiency - avoid copying large data structures
- Reactivity - updating a referenced value triggers updates without rebuilding entire structures
Create a reference with & and dereference (get the value) with *:
let s = "Hello";
// r is a reference to s
let r = &s;
// v is the value "Hello"
let v = *r;
&T is read-only. A function that writes through a reference (*r <- v) takes &mut T, and you pass it &mut x:
let set = |r: &mut i64, v: i64| *r <- v;
let x = 0;
// x becomes 1
set(&mut x, 1)
A &T parameter accepts a &mut too, and covers any narrower type: &[i64, null] accepts &x with x: i64. A &mut T must refer to exactly a T.
In function signatures, &T in a parameter position means the function expects a reference. In widget examples, you’ll often see:
block(#title: &line("My Title"), &my_widget)
The &line(...) creates a reference to the line, and &my_widget is a reference to the widget.
For a deeper dive, see References.
Error Types (throws)
When a function can throw errors, the signature includes throws:
val divide: fn(a: i64, b: i64) -> i64 throws `DivideByZero
This function returns i64 if successful, but might throw a DivideByZero error.
Multiple Error Types
A function can throw multiple error types:
val parse_and_divide: fn(num: string, denom: string) -> i64 throws [`ParseError, `DivideByZero]
Generic Error Types
Often error types are parameterized:
val filter: fn(v: 'a, f: fn(x: 'a) -> bool throws 'e) -> 'a throws 'e
This means: the filter function itself doesn’t throw errors, but if the function you pass to it throws errors of type 'e, then filter will also throw those same errors.
A function type without throws throws nothing. A parameter written
f: fn(x: 'a) -> bool refuses a callback that can throw; write throws 'e to accept one and pass its errors on.
Result Type
The Result type is a convenient way to represent success or error:
type Result<'r: NoError, 'e> = ['r, Error<'e>]
So a function signature like:
val parse: fn(s: string) -> Result<i64, `ParseError>
Returns either an i64 (success) or an Error<ParseError>` (failure).
The NoError bound says the success type holds no error itself, so the
error member is the only error in a result and error as e always picks
it out. Result<[i64, Error<A>], B> is refused; write the union of
the errors instead, Result<i64, [A, B]>.
See Error Handling for complete details on working with errors.
Set Types
Square brackets [...] denote a set type - the value can be any one of the types in the set:
val process: fn(x: [i64, string]) -> string
This function accepts either an i64 or a string, and returns a string.
Optional Types (Nullable)
The pattern [T, null] means “T or nothing”:
val find: fn(haystack: Array<string>, needle: string) -> [string, null]
Returns a string if found, null if not found. This is aliased as Option<T>:
type Option<'a> = ['a, null];
val find: fn(haystack: Array<string>, needle: string) -> Option<string>
Nested Sets
Types can nest arbitrarily:
val sum: fn(@args: [Number, Array<[Number, Array<Number>]>]) -> Number
This accepts numbers, arrays of numbers, or even arrays of (numbers or arrays of numbers). The flexibility allows you to call:
sum(1, 2, 3);
sum([1, 2], [3, 4]);
sum(1, [2, 3], 4)
Function Constraints
Type variables in functions can have constraints:
val sum: fn<'a: Number>(@args: 'a) -> 'a
This is subtly different from the sum examples earlier. This says,
sum is a function that takes any number of arguments of the same
type and returns the same type, and that type must be some kind of
number.
Auto Parameters
The compiler often infers type variables (and constrains them) by itself during the type inference process.
if we compile a function with no type constraints, such as:
let f = |x, y| x + y
Its type will be something like:
val f: fn<'_2067: Number + Singleton>(x: '_2067, y: '_2067) -> '_2067
The compiler has inferred two properties here,
- both arguments are one type of number: arithmetic is
fn<'a: Number + Singleton>(x: 'a, y: 'a) -> 'a, sof(1, 2.5)is refused (cast one side). - the result is that same type.
Singleton says the type is one type, not a union of several. The
operands must be exactly one type: x + 1 over x: [i64, null] is
refused (take the null off first, x$ + 1), and so is any arithmetic
over a type holding two numeric types, [i64, f64] or Number, even
x * x: its values would promote at runtime, so the result’s type
would not say what it is. A call of f with two [i64, f64] values is
refused the same way, where it is called.
Comparison is fn('a, 'a) -> bool over exactly one type: |x, y| x < y
takes two arguments of one type, and neither operand’s type may merely
contain the other’s. x == 3 over x: [i64, null] and v == `Red
over v: [`Green, `Red] are refused; select on the value instead
(select v { `Red => true, _ => false }, opt::is_some). Any one
type compares, unions included: two [Green, Red] values, two
[string, i64] values, and two [i64, f64] values, whose different
types order by type before value (i64:3 == f64:3.0 is false).
Because unchecked arithmetic operators like + wrap on overflow, and
/ and % return bottom on a zero divisor, rather than throwing, there
is no throws clause in the type signature. (A bottomed division is
logged by the interpreter; JIT compiled code, the default, returns
bottom without logging. Run with --no-fusion to see the diagnostics
when debugging.)
In the shell this type signature is made a bit more complex by the
shell also telling you what type variables are currently bound to, or
unbound if they aren’t bound. So in the shell this pops out as,
〉f
-: fn<'_2067: unbound: Number + Singleton>(x: '_2067: unbound, y: '_2067: unbound) -> '_2067: unbound
The constraint '_2067: unbound: Number + Singleton is read as: _2067
is not currently bound to a type but is constrained to one type of
Number. This is
all useful information, even though it’s intimidating at first it’s
worth putting in the work to learn to decipher it.
Putting It All Together
Let’s decode some complex real-world signatures:
Example 1: TUI Table Widget
val table: fn(
?#header: &[Row, null],
?#selected: &[i64, null],
?#row_highlight_style: &[Style, null],
?#highlight_symbol: &[string, null],
rows: &Array<&Row>
) -> Tui
(a few of table’s labeled arguments; the rest have the same shape)
Breaking it down:
?#header: &[Row, null]- optional labeled argument, reference to a Row or null?#selected: &[i64, null]- optional labeled argument, reference to the selected index?#row_highlight_style: &[Style, null]- optional labeled argument, reference to a Style?#highlight_symbol: &[string, null]- optional labeled argument, reference to the symbol stringrows: &Array<&Row>- required positional argument, reference to array of row references-> Tui- returns a TUI widget
All parameters are references because the table needs to react to changes without rebuilding.
Example 2: Filter Function
val filter: fn(v: 'a, pred: fn(x: 'a) -> bool throws 'e) -> 'a throws 'e
Breaking it down:
v: 'a- a value of any typepred: fn(x: 'a) -> bool throws 'e- a predicate function that:- Takes the same type
'a - Returns bool
- Might throw errors of type
'e
- Takes the same type
-> 'a- returns the same type as inputthrows 'e- propagates any errors from the predicate
Example 3: Queue Function
val queue: fn(#clock: Any, ?#flush: Any, ?#flush_before: [i64, null], v: 'a) -> 'a
Breaking it down:
#clock: Any- required labeled argument of any type, just used as an event source?#flush: Any- optional labeled argument, an event that empties the queue?#flush_before: [i64, null]- optional labeled argument: when it isn, the values that arrived before then-th are discardedv: 'a- a value of any type-> 'a- returns values of the same type
Call it like: queue(#clock: my_timer, my_value)
Example 4: Array Map
val map: fn(a: Array<'a>, f: fn(x: 'a) -> 'b throws 'e) -> Array<'b> throws 'e
Breaking it down:
a: Array<'a>- array of any type'af: fn(x: 'a) -> 'b throws 'e- transformation function that:- Takes type
'a(array element type) - Returns type
'b(result element type) - Might throw errors of type
'e
- Takes type
-> Array<'b>- returns array of transformed typethrows 'e- propagates errors from the transform function
Quick Reference Table
| Notation | Meaning | Example |
|---|---|---|
'a, 'b, 'e | Type parameter (generic) | fn(x: 'a) -> 'a |
'_23, '_24, '_25 | Inferred type parameter (generic) | fn(x: '_23) -> '_23 |
name: T | Named positional argument | fn(x: i64) -> i64 |
?#param | Optional labeled argument (its default is the lambda’s, |#x: i64 = 0| ..) | fn(?#x: i64) |
#param | Required labeled argument | fn(#x: i64) |
@args | Variadic (any number of args) | fn(@args: i64) |
&T | Reference to type T | fn(s: &string) |
throws 'e | Can throw errors of type ’e | fn() -> i64 throws 'e |
[T, U] | T or U (set/union type) | [i64, null] |
-> | Returns | fn(x: i64) -> string |
Array<T> | Array of T | Array<string> |
Map<K, V> | Map with keys K, values V | Map<string, i64> |
Error<'e> | Error containing type ’e | Error<\ParseError>` |
Result<'r, 'e> | Success ’r or Error ’e | Result<i64, \Err>` |
Option<'a> | Value ’a or null | Option<string> |
Tips for Reading Signatures
- Start with the basics - identify parameters and return type
- Look for type parameters - they tell you about genericity
- Check for optional/labeled args - they indicate flexibility in calling
- Note reference types - important for reactivity
- Watch for throws - you’ll need error handling
- Don’t panic at complexity - break it down piece by piece
Remember: you don’t need to memorize these patterns. As you use Graphix, you’ll naturally become familiar with common signatures. This guide is here whenever you need a reminder!
See Also
- Fundamental Types - Built-in types and type sets
- Functions - Creating and using functions
- Error Handling - Working with errors and the throws system
- References - Deep dive into reference types
- User Defined Types - Structural typing and custom types