Multiple dispatch
A response renderer written as a set of small overloads rather than a growing switch.
An overload set is several top-level functions sharing a name. The call picks the most specific one that applies, looking at every argument rather than only the first. That is the whole idea, and most of what makes W# different follows from taking it seriously enough to compile it.
// Multiple dispatch over the HTTP status lattice.
//
// A response renderer written as a set of small overloads rather than a
// growing `switch`. Adding a special case for one status means adding one
// function -- nothing existing is edited, and the compiler checks that the
// new overload is unambiguously more specific than the ones it refines.
//
// The status types come from the standard library and are materialised on
// first mention; a program that never names one pays nothing for them.
const http = @import("std/http");
const Request = struct { path: str };
// The general case, then two families, then two exact codes.
fn render(r: Request, s: http.Status) str { return "HTTP/1.1 500 Internal Server Error"; }
fn render(r: Request, s: http.Status2xx) str { return "HTTP/1.1 200 OK"; }
fn render(r: Request, s: http.Status4xx) str { return "HTTP/1.1 400 Bad Request"; }
fn render(r: Request, s: http.NotFound404) str { return "HTTP/1.1 404 Not Found"; }
fn render(r: Request, s: http.Teapot418) str { return "HTTP/1.1 418 I'm a teapot"; }
// Routing decides a status at run time, so `serve` cannot know which overload
// it will need -- this is the call that compiles to a real dispatch.
fn serve(r: Request, s: http.Status) void { print(render(r, s)); }
fn main() i64 {
const req = Request{ .path = "/" };
// Resolved at compile time: each status here is exactly what it says.
print(render(req, http.Ok200));
print(render(req, http.NotFound404));
print(render(req, http.Teapot418));
print(render(req, http.Forbidden403));
print(render(req, http.ServiceUnavailable503));
// Resolved at run time, from the type id in the object's header.
serve(req, http.NotFound404);
serve(req, http.Created201);
serve(req, http.BadGateway502);
return 0;
}Run it:
wsharp run examples/status.wsHTTP/1.1 200 OK
HTTP/1.1 404 Not Found
HTTP/1.1 418 I'm a teapot
HTTP/1.1 400 Bad Request
HTTP/1.1 500 Internal Server Error
HTTP/1.1 404 Not Found
HTTP/1.1 200 OK
HTTP/1.1 500 Internal Server ErrorAdding a case means adding a function
Five render functions, and the call site names none of them. Forbidden403 has
no overload of its own, so it falls to Status4xx, and ServiceUnavailable503
falls all the way to Status.
Now suppose 503 deserves its own message. In a switch you would open the
existing function and insert a branch, which means touching code that already
works and that other cases depend on. Here you add:
fn render(r: Request, s: http.ServiceUnavailable503) str {
return "HTTP/1.1 503 Service Unavailable";
}Nothing existing is edited. The compiler checks that the new overload is unambiguously more specific than the ones it refines, and if it is not, it says so rather than quietly changing which code runs.
Two of these calls are not dispatch at all
Look at the two groups in main. The first five calls name a status directly, so
inference knows the argument’s exact type, and it knows that type has no subtypes
that could change the answer. The winner is decided during compilation and the
call lowers to an ordinary direct call. There is no dispatch code in the emitted
program at all.
serve is the other case. Its parameter is declared http.Status, so by the
time the body runs, s could be any status at all. This is where the compiler
emits a real runtime decision, and it is worth knowing what that costs:
0 Status
1 Status1xx
3 Status2xx
8 Status3xx
12 Status4xx ┐
13 BadRequest400 │
16 NotFound404 │ ids 12 through 20
19 Teapot418 │
20 TooManyRequests429 ┘
21 Status5xxType ids are assigned in a preorder walk of the subtype lattice, so every type’s
subtypes occupy a contiguous range of ids. Asking “is this a Status4xx” is
therefore id - 12 <= 8, evaluated unsigned so that the underflow on a smaller
id gives a large number and fails the compare. One subtract, one unsigned
compare. No vtable, no inline cache, no method-table lookup, and no hash.
Overloading is not only for structs
An abstract type stands for a set of concrete ones. Number is every numeric
type and Integer is the eight integer ones, so a general case can sit beside a
specific one:
fn show(x: i64) str { return "an integer"; }
fn show(x: Integer) str { return "some width of integer"; }
fn show(x: Number) str { return "a number"; } // catches f64Abstract types are ordered by their member sets, so Integer is more specific
than Number and wins wherever both apply.
A body annotated Number has to work for every type it lists, which is why it
may not use % (there is no float form) or negate (there are no unsigned
negatives). Integer is what such a body should claim.
An abstract type classifies values for dispatch and is never one itself. A parameter annotated with it is a generic parameter constrained to the members, compiled once per type it is used at, exactly as an unannotated parameter is. Nothing is tested at run time, because a scalar’s type is always known during compilation.
Ambiguity is a compile error
Selection is by specificity. An overload wins if it is at least as specific as every other applicable one in every argument, and strictly more specific in at least one. Two overloads that could both match the same call, with neither more specific, are rejected:
fn pick(a: Sub, b: Base) i64 { return 1; }
fn pick(a: Base, b: Sub) i64 { return 2; }
// pick(Sub, Sub) matches both: error, this call to `pick` is ambiguous
fn pick(a: Sub, b: Sub) i64 { return 3; } // and this settles itThis is the safety net that makes the “just add a function” style workable. You cannot accidentally shadow a case or create a silent tie.
One rule to remember
Every parameter of an overloaded function must be annotated. Dispatch chooses by parameter type, so those types cannot themselves be inferred from the calls being resolved. Everywhere else, annotations stay optional, which is the subject of the next page.