Skip to content
FutureBlitz

    Install

    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]))
    Output
    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"]))
    $ fb run main.fb
       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:

    InterfaceWhat it gives
    Equatable== and !=
    Comparable<, >, <=, and >=, and == from Equatable
    HashableHash(), which Map keys and Set elements need
    PrintableToString(), so a value can go in a string
    Numeric+, -, *, /, Zero(), and One()
    CloneableClone(), a copy of the value
    DefaultableDefaultValue(), 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)]))
    Output
    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]))
    Output
    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 line

    A 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)
    Output
    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())
    $ fb run main.fb
       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 what T must 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 on Where lines.
    • A Where line on a Method makes it exist only for the instances that meet it.

    Report an issue with this page or with FutureBlitz on GitHub.