Skip to content
FutureBlitz

    Install

    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)
    Output
    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")
    Output
    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}"))
    Output
    [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")
    Output
    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")
    $ fb run main.fb
       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]))
    $ fb run main.fb
       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]))
    Output
    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 T with no constraint can only be stored and passed around; a constraint, like <T: Comparable>, gives it operators and Methods.

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