Generics, 1 of 4
Generic functions
Write a function once, with type parameters, and call it with arrays of any type.
What you’ll build
In this tutorial you write three functions that work on arrays of any element type: First, which
gives an array’s first element, Transform, which changes each element into something else, and
Largest, which finds the biggest element. Each is written once and works for Ints, Strings, and
your own types.
A function for one type
Start with a function that works only on Ints. It gives Null for an empty array, so its result
is an Int?.
Import std.core
Function FirstInt(items: Int[]): Int?
If items.Count == 0 Then Return Null
Return items[0]
End Function
Local scores = [40, 75, 12]
Local none: Int[] = []
Print(FirstInt(scores) ?? -1)
Print(FirstInt(none) ?? -1)40 -1
To do the same for Strings, you would copy the function and change Int to String. A generic
function avoids the copy.
A type parameter
A type parameter is a name for a type, written in angle brackets after the function’s name. Inside
the function, T is used wherever the element type goes.
Import std.core
Function First<T>(items: T[]): T?
If items.Count == 0 Then Return Null
Return items[0]
End Function
Local scores = [40, 75, 12]
Local names = ["Ada", "Grace", "Linus"]
Print(First(scores) ?? -1)
Print(First(names) ?? "nobody")40 Ada
Each call gives T a type argument, the type it stands for in that call. The compiler works it
out from the argument: scores is an Int[], so T is Int, and First(scores) gives an
Int?.
For each type argument, the compiler builds a separate copy of First, called an instance. So
First<Int> and First<String> run as fast as functions written for those types.
Two type parameters
A function can have several type parameters, separated by commas. Transform takes an array of
T and a function value that turns a T into a U, and gives an array of U.
Import std.core
Import std.text
Function Transform<T, U>(items: T[], change: Func(T): U): U[]
Local results: U[] = []
For item In items
results.Add(change(item))
End For
Return results
End Function
Local names = ["Ada", "Grace", "Linus"]
Print(Transform(names, Function(name) => name.Length))
Print(Transform([1, 2, 3], Function(n) => "#{n}"))[3, 5, 5] [#1, #2, #3]
T comes from the array. Once T is known, the lambda’s parameter has a type, and U comes
from what the lambda gives: Int for name.Length, and String for "#{n}".
Writing the type arguments
When nothing in a call tells the compiler what T is, you write the type arguments yourself, in
angle brackets after the name. An empty array literal has no elements to tell T from:
Import std.core
Function First<T>(items: T[]): T?
If items.Count == 0 Then Return Null
Return items[0]
End Function
Print(First<String>([]) ?? "no first item")no first item
Without <String>, the call is a compile error that says what to write, and the empty array is
another, since nothing says what it holds:
Import std.core
Function First<T>(items: T[]): T?
If items.Count == 0 Then Return Null
Return items[0]
End Function
Print(First([]) ?? "no first item")Compiling main.fb main.fb:8:7: error: can't tell what T is in this call; write the type arguments, like First<Int>(...) | 8 | Print(First([]) ?? "no first item") | ^^^^^ main.fb:8:13: error: an empty array needs a type from where it's used, like 'Local a: Int[] = []' | 8 | Print(First([]) ?? "no first item") | ^^ error: couldn't build main.fb because of 2 errors
What a T can do
Inside a generic function, T can be stored, passed, returned, and put in arrays and T?s, but
that’s all, since T could be any type. A Largest that compares two Ts with > doesn’t
compile:
Import std.core
Function Largest<T>(items: T[]): T
Local best = items[0]
For item In items
If item > best Then best = item
End For
Return best
End Function
Print(Largest([3, 9, 4]))Compiling main.fb main.fb:6:12: error: '>' can't be used on T | 6 | If item > best Then best = item | ^^^^^^^^^^^ error: couldn't build main.fb because of 1 error
A constraint says what T must be able to do. <T: Comparable> means T must be a type whose
values can be ordered, which gives the function <, >, <=, and >= on it. Ints, Floats, and
Strings are all Comparable.
Import std.core
Function Largest<T: Comparable>(items: T[]): T
Local best = items[0]
For item In items
If item > best Then best = item
End For
Return best
End Function
Print(Largest([3, 9, 4]))
Print(Largest(["pear", "apple", "plum"]))
Print(Largest([2.5, 1.25]))9 plum 2.5
A call with a type that isn’t Comparable doesn’t compile. An empty array has no largest element,
so items[0] panics for one, as any index out of range does; First handles that case with a
T?. Constraints covers constraints in full.
What you learned
- A type parameter, like
<T>, names a type that each call supplies. - The compiler infers the type arguments from the call’s arguments, and you write them, as
First<String>(...), when it can’t. - Each set of type arguments makes an instance, compiled as if it were written for those types.
- A
Twith no constraint can only be stored and passed around; a constraint, like<T: Comparable>, gives it operators and Methods.
The rules are in the language reference.
Report an issue with this page or with FutureBlitz on GitHub.