Skip to content
FutureBlitz

    Install

    Generics, 2 of 4

    Generic types

    Classes, Structs, and Interfaces with type parameters, and Methods with type parameters of their own.

    What you’ll build

    In this tutorial you build a Stack<T>, a Class that holds items of any one type, give it a generic Method, and then write a generic Struct and a generic Interface. These are the same building blocks std.collections uses for Map<K, V>, Set<T>, and the rest.

    A generic Class

    A Class takes type parameters after its name, as a function does. T is then a type throughout the Class: in its fields, its Methods’ parameters, and their results.

    Import std.core
    
    Class Stack<T>
        Field items: T[] = []
    
        Method Push(item: T)
            items.Add(item)
        End Method
    
        Method Pop(): T?
            If items.Count == 0 Then Return Null
            Local top = items[items.Count - 1]
            items.RemoveAt(items.Count - 1)
            Return top
        End Method
    
        Property Count: Int
            Get => items.Count
        End Property
    End Class
    
    Local undo = Stack<String>()
    undo.Push("draw line")
    undo.Push("fill")
    Print(undo.Count)
    Print(undo.Pop() ?? "nothing")
    Print(undo.Pop() ?? "nothing")
    Print(undo.Pop() ?? "nothing")
    Output
    2
    fill
    draw line
    nothing

    Stack<String>() makes a stack of Strings. Stack<String> and Stack<Int> are two different types, each its own instance of the Class, so pushing an Int onto undo is a compile error.

    A generic type is always written with its type arguments where a type goes. A parameter declared as s: Stack is an error:

    Import std.core
    
    Class Stack<T>
        Field items: T[] = []
    End Class
    
    Function Size(s: Stack): Int => s.items.Count
    
    Print(Size(Stack<Int>()))
    $ fb run main.fb
       Compiling main.fb
    main.fb:7:18: error: Stack<T> is generic, so it needs type arguments, like Stack<Int>
      |
    7 | Function Size(s: Stack): Int => s.items.Count
      |                  ^^^^^
    error: couldn't build main.fb because of 1 error

    When you make an object, the compiler can infer the type arguments from what you pass to New, as it does for a function’s call. With nothing passed, as for Stack<String>(), you write them, unless the variable it’s assigned to has a type that gives them, as Inference and defaults shows.

    A generic Method

    A Method can have type parameters of its own, after its Class’s. Map makes a new stack by changing each item, so it needs a second type, U, for what the items become.

    Import std.core
    
    Class Stack<T>
        Field items: T[] = []
    
        Method Push(item: T)
            items.Add(item)
        End Method
    
        Method Pop(): T?
            If items.Count == 0 Then Return Null
            Local top = items[items.Count - 1]
            items.RemoveAt(items.Count - 1)
            Return top
        End Method
    
        Method Map<U>(change: Func(T): U): Stack<U>
            Local result = Stack<U>()
            For item In items
                result.Push(change(item))
            End For
            Return result
        End Method
    End Class
    
    Local sizes = Stack<Int>()
    sizes.Push(3)
    sizes.Push(14)
    Local labels = sizes.Map(Function(n) => "{n} px")
    Print(labels.Pop() ?? "")
    Print(labels.Pop() ?? "")
    Output
    14 px
    3 px

    T comes from the object, sizes, so it’s Int, and U comes from the lambda, so it’s String. A generic Method has a copy for each set of type arguments, so it can’t be Abstract or overridden.

    A generic Struct

    Structs take type parameters too. Pair<A, B> holds two values of any two types.

    Import std.core
    
    Struct Pair<A, B>
        Field First: A
        Field Second: B
    End Struct
    
    Local born = Pair("Ada", 1815)
    Print("{born.First} was born in {born.Second}")
    
    Local moved: Pair<String, Int> = Pair("Grace", 1906)
    Print(moved.First)
    Output
    Ada was born in 1815
    Grace

    Pair("Ada", 1815) infers A = String and B = Int from its arguments. The second Local writes its type, Pair<String, Int>, and the compiler checks the value against it.

    A generic Interface

    An Interface’s type parameters are types in its members. A Class implements one instance of it, like Source<Int>, and its members then have Int where the Interface has T.

    Import std.core
    
    Interface Source<T>
        Method Take(): T
        Method TakeTwo(): (T, T) => (Take(), Take())
    End Interface
    
    Class Counter Implements Source<Int>
        Field next: Int
    
        Method Take(): Int
            next += 1
            Return next
        End Method
    End Class
    
    Local counter = Counter()
    Print(counter.TakeTwo())
    
    Local source: Source<Int> = counter
    Print(source.Take())
    Output
    (1, 2)
    3

    TakeTwo is a default, written in the Interface, so Counter gets it without writing it, and for Source<Int> it gives an (Int, Int). A type implements at most one instance of each generic Interface, so a Class can’t be both a Source<Int> and a Source<String>.

    What you learned

    • Classes, Structs, and Interfaces take type parameters, and each set of type arguments makes an instance, a type of its own.
    • A generic type is always written with its type arguments, like Stack<Int>, though making one can infer them from New’s arguments.
    • A Method can have type parameters of its own, after its type’s.
    • A generic Interface’s instance, like Source<Int>, is an Interface like any other.

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