Skip to content
FutureBlitz

    Install

    Generics, 4 of 4

    Inference and defaults

    How the compiler works out type arguments, when you write some of them, and how generic code makes a value of T from nothing.

    What you’ll build

    Most generic calls don’t write their type arguments, because the compiler works them out. In this tutorial you see each place it takes them from, write only the first of a call’s type arguments, and use Defaultable to make values of a T that nothing passed in.

    From the arguments

    The compiler matches each argument’s type against the parameter’s. An Int[] passed for a T[] gives T = Int. An argument whose type implements an instance of a generic Interface gives that instance’s type arguments, so a Set<Int> passed for an Iterable<T> gives T = Int, and so does an array.

    Import std.core
    Import std.collections
    
    Function Total<T: Numeric>(values: Iterable<T>): T
        Local total = T.Zero()
        For v In values
            total += v
        End For
        Return total
    End Function
    
    Local primes = Set<Int>()
    primes.Add(2)
    primes.Add(3)
    primes.Add(5)
    Print(Total(primes))
    Print(Total([1.5, 2.5]))
    Output
    10
    4

    From literals

    Number literals passed for the same type parameter agree on one type. Two whole numbers give Int, and a number with a decimal point among them makes it Float.

    Import std.core
    
    Function Max<T: Comparable>(a: T, b: T): T => a > b ? a : b
    
    Print(Max(1, 2))
    Print(Max(1, 2.5))
    Output
    2
    2.5

    From lambdas and the expected type

    A lambda’s parameters get their types once the type arguments they need are known, and its result then gives the rest. And when a call has no arguments to go by, the type the result is assigned to gives them.

    Import std.core
    Import std.collections
    
    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
    
    Print(Transform([1, 2, 3], Function(n) => n * n))
    
    Local pending: Stack<String> = Stack()
    pending.Push("save")
    Print(pending.Count)
    Output
    [1, 4, 9]
    1

    In Transform, T comes from the array, so n is an Int, and U from what the lambda gives, n * n. std.collections’ Stack() takes T = String from the Local’s type.

    Writing the first type arguments

    A call can write its first type arguments and leave the rest to inference. Convert<T, A> takes a value of any type and a function that changes it into a T. Writing Convert<String> gives T, and A still comes from the argument.

    Import std.core
    
    Function Convert<T, A>(value: A, change: Func(A): T): T => change(value)
    
    Print(Convert<String>(42, Function(n) => "#{n}"))
    Print(Convert<Bool>(3, Function(n) => n > 2))
    Output
    #42
    True

    What you write is taken as given: Convert<Int>(42, Function(n) => "#{n}") is an error, since the lambda gives a String where an Int is wanted.

    When the compiler can’t tell a type argument, the call is an error that names the type parameter and asks you to write the type arguments, as First([]) did in Generic functions.

    A value from nothing

    A T with no constraint has no zero. Generic code that needs a value to start from, without one being passed in, constrains T with Defaultable, whose T.DefaultValue() gives the type’s default: 0 for numbers, False for Bools, "" for Strings, U+0000 for Chars, and an Enum’s first member, or no flags for a @Flags Enum.

    Import std.core
    
    Function Filled<T: Defaultable>(count: Int): T[]
        Local items: T[] = []
        For i = 1 To count
            items.Add(T.DefaultValue())
        End For
        Return items
    End Function
    
    Print(Filled<Int>(3))
    Print(Filled<Bool>(2))
    
    Local names: String[] = Filled(2)
    Print(names.Count, names[0] == "")
    Output
    [0, 0, 0]
    [False, False]
    2 True

    Filled takes no argument of type T, so a call writes T, or assigns its result to a variable whose type gives it, as names does.

    What you learned

    • Type arguments come from the arguments’ types, from literals, from lambdas, and from the type a result is assigned to.
    • A call can write its first type arguments, as Convert<String>(...), and the compiler infers the rest.
    • When a type argument can’t be told, the compiler asks you to write it.
    • <T: Defaultable> gives generic code T.DefaultValue(), a value of T to start from.

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