Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Overload dispatch with collections

Background

When Andy C++ can determine at compile time which function overload to call, it does so — the call is free of any type-checking overhead at runtime. When it cannot (because an argument was inferred as Any), the VM performs dynamic dispatch: it tests each candidate overload at runtime to find the best match.

The reverse also holds. Sometimes every argument type is known at compile time, but no overload can accept them: each candidate is fully annotated, and none matches the call’s arity and argument types. Such a call could only ever fail at runtime. Instead of waiting for that, it is rejected at compile time with a No function called '…' found that matches the arguments error.

O(1) dispatch guarantee

For dynamic dispatch the VM checks whether a value conforms to the parameter type without iterating the container contents. Specifically:

Parameter typeCheck performed
ListIs the value a list?
MapIs the value a map?
DequeIs the value a deque?
SequenceIs the value any sequence type?
String, Int, …Exact kind check

This means that dispatch is O(1) regardless of how many elements are in the collection.

Limitation: element types are not checked at runtime

Because the element-type check is skipped, the VM cannot distinguish overloads that differ only in their container element types via dynamic dispatch. For example, two hypothetical overloads:

fn process(List<Int>)
fn process(List<String>)

would both fail to match under dynamic dispatch if the list type cannot be resolved at compile time, because verifying element types would require scanning the entire container.

Standard library overloads do differ by element type. Numeric sequence functions preserve the concrete numeric type, so sum has three typed overloads:

fn sum(Sequence<Int>) -> Int
fn sum(Sequence<Float>) -> Float
fn sum(Sequence<Number>) -> Number

A value the analyser only knows as List<Any> matches none of them. Rather than dispatch on it and scan, the call is rejected at compile time:

let values: List<Any> = [1, 2, 3];
values.sum()
// error[resolver]: No function called 'sum' found that matches the arguments 'List<Any>'
// = An overload would accept a narrower argument type. Cast to say what the value holds,
//   as in `value as List<Int>`.

Overloads that differ only by container kind (e.g. pop(List<Any>) vs pop(MinHeap<Any>)) are still distinguished by the kind check alone. User-defined functions cannot yet declare typed container parameters (the syntax is not implemented), so user overloads always use Any.

Workaround

State the element type with a cast. A cast scans the value, but the cost lands at a site you wrote rather than inside every dispatch:

let values = %{};
values.insert(1);
values.insert(2);

// keys is typed List<Any>; the cast recovers Int so sum can resolve.
assert_eq((values.keys as List<Int>).sum(), 3);

Otherwise, move the call to a location where Andy C++ can infer the argument types statically — for example, directly at the call site rather than through an intermediate untyped function parameter:

// The type of `data` is Any here — dynamic dispatch used
fn handle(data) {
    process(data)
}

// Preferred: call process() directly where the type is known
process(my_list)