Generics, 3 of 4
Constraints
Say what a type parameter must be able to do, with inline constraints, Where lines, and Interfaces of your own.
What you’ll build
A constraint says what a type argument must be able to do, and in return lets the generic code
do it. In this tutorial you write a Sum over any kind of number, a TotalArea over any shape,
a Tally that needs two constraints at once, and a Bag<T> whose Total Method exists only
for numbers.
A built-in constraint
Numeric is one of std.core’s built-in Interfaces. A type that implements it has +, -, *,
and /, and the Static Methods Zero() and One(). With <T: Numeric>, the function can add
Ts and start its total at T.Zero().
Import std.core
Function Sum<T: Numeric>(values: T[]): T
Local total = T.Zero()
For v In values
total += v
End For
Return total
End Function
Print(Sum([1, 2, 3]))
Print(Sum([0.5, 0.25]))6 0.75
Each call is checked against the constraint where it’s made, so a call with Strings doesn’t compile:
Import std.core
Function Sum<T: Numeric>(values: T[]): T
Local total = T.Zero()
For v In values
total += v
End For
Return total
End Function
Print(Sum(["a", "b"]))Compiling main.fb main.fb:11:7: error: String doesn't implement Numeric, which 'Sum' needs for T | 11 | Print(Sum(["a", "b"])) | ^^^ error: couldn't build main.fb because of 1 error
The built-in Interfaces are the usual constraints:
| Interface | What it gives |
|---|---|
Equatable | == and != |
Comparable | <, >, <=, and >=, and == from Equatable |
Hashable | Hash(), which Map keys and Set elements need |
Printable | ToString(), so a value can go in a string |
Numeric | +, -, *, /, Zero(), and One() |
Cloneable | Clone(), a copy of the value |
Defaultable | DefaultValue(), a value to start from |
Numbers implement all of them but Cloneable, and Strings all but Numeric and Cloneable.
Bools and Enums are Equatable, Hashable, Printable, and Defaultable. Every Class is
Equatable and Hashable without listing them, comparing and hashing its objects themselves,
unless it has an Operator == of its own: then it needs a Hash() of its own to be Hashable.
An Interface of your own
Any Interface can be a constraint. TotalArea takes an array of any type that implements
Shape, and calls its Area Method.
Import std.core
Interface Shape
Method Area(): Float
End Interface
Struct Square Implements Shape
Field Side: Float
Method Area(): Float => Side * Side
End Struct
Struct Circle Implements Shape
Field Radius: Float
Method Area(): Float => 3.14159 * Radius * Radius
End Struct
Function TotalArea<T: Shape>(shapes: T[]): Float
Local total = 0.0
For s In shapes
total += s.Area()
End For
Return total
End Function
Print(TotalArea([Square(2), Square(3)]))
Print(TotalArea([Circle(1)]))13 3.14159
Square and Circle are Structs. A variable of type Shape holds only Classes’ objects, so
Local shapes: Shape[] = [Square(2)] is an error, but a type parameter constrained by Shape
takes a Struct. Each instance calls the Struct’s own Area directly, as code written for
Square[] would.
Several constraints, on Where lines
A constraint goes inline, <T: Comparable>, when it’s the only one on the declaration. Two or
more, or one that relates two type parameters, go on Where lines at the top of the body, and
& joins two on one type parameter. A declaration on one line, written with =>, has no body to
hold Where lines, so it writes them all inline.
Tally counts each item in a Map, so T must be Hashable, and shows them in a string, so it
must be Printable too. A Map’s Keys() gives them in the order they were first added.
Import std.core
Import std.collections
Import std.text
Function Tally<T>(items: T[]): String
Where T: Hashable & Printable
Local counts: Map<T, Int> = {}
For item In items
counts[item] = (counts.Get(item) ?? 0) + 1
End For
Local parts: String[] = []
For item In counts.Keys()
parts.Add("{item} x{counts[item]}")
End For
Return ", ".Join(parts)
End Function
Print(Tally(["red", "blue", "red", "green", "red"]))
Print(Tally([3, 1, 3]))red x3, blue x1, green x1 3 x2, 1 x1
There is one right form for each signature, and the compiler warns about the other. A single
constraint written on a Where line gets:
warning: a declaration's one constraint goes inline, as <T: Comparable>, not on a Where lineA Method for some instances only
A Where line on a Method of a generic type makes the Method conditional: only the instances
whose type arguments meet it have the Method. A Bag<Int> has Total, and a Bag<String>
doesn’t.
Import std.core
Class Bag<T>
Field items: T[] = []
Method Add(item: T)
items.Add(item)
End Method
Method Total(): T
Where T: Numeric
Local total = T.Zero()
For item In items
total += item
End For
Return total
End Method
End Class
Local coins = Bag<Int>()
coins.Add(5)
coins.Add(20)
Print(coins.Total())
Local names = Bag<String>()
names.Add("Ada")
Print(names.items)25 [Ada]
Calling names.Total() is a compile error, at the call:
Import std.core
Class Bag<T>
Field items: T[] = []
Method Add(item: T)
items.Add(item)
End Method
Method Total(): T
Where T: Numeric
Local total = T.Zero()
For item In items
total += item
End For
Return total
End Method
End Class
Local names = Bag<String>()
names.Add("Ada")
Print(names.Total())Compiling main.fb main.fb:22:7: error: String doesn't implement Numeric, which 'Total' needs for T | 22 | Print(names.Total()) | ^^^^^^^^^^^ error: couldn't build main.fb because of 1 error
What you learned
- A constraint, like
<T: Numeric>, lists whatTmust implement, and gives the generic code its Methods, Static Methods, and operators. - Any Interface can be a constraint, and a constrained type parameter takes Structs as well as Classes.
- One constraint goes inline; several, joined with
&or on several type parameters, go onWherelines. - A
Whereline on a Method makes it exist only for the instances that meet it.
The rules are in the language reference.
Report an issue with this page or with FutureBlitz on GitHub.