Pangram verdict · v3.3
We believe that this document is fully human-written
AI likelihood · overall
HumanArticle text · 978 words · 4 segments analyzed
“Overload set” is a term of art that seemingly arises from the reuse of overload rules across the years. Overload sets quietly power a lot of D’s generic programming, and once you understand them explicitly, a whole class of API design opens up.
import std;
void bar(int) { "int".writeln; } void bar(float) { "float".writeln; } void bar(bool) { "bool".writeln; }
void useIt(alias F)() { F(1); // prints "int" F(13.37); // prints "float" F(true); // prints "bool" }
unittest { useIt!bar; }
What exactly is bar when passed to useIt? Wait, does it survive as useIt.F? (Yes, it compiles as is.)
What Are Overload Sets?
The specification:
An Overload Set is the set of functions with the same name declared in the same scope that participate in overload resolution.
And from the alias spec:
Aliases can also ‘import’ a set of overloaded functions that can be overloaded with functions in the current scope…
The behavior of overload sets is scattered across the spec. A few sentences here, a few more tucked away in the template section. Template specialization lets you pattern-match against the whole set; pick the right implementation based on compile-time arguments.
Overload sets can work with several very different things. I use the following definition:
An overload set is a collection of things that share a name and a closely related template header.
You may not know everything that can be templatized, and real-world template usage is extremely concerned about making only one declaration match at a time.
Specialization on Integer Values
alias typeAt(int I : 0) = int; alias typeAt(int I : 1) = float; alias typeAt(int I : 2) = string; enum typeAtLength = Length!typeAt;
template Length(alias A, int acc=0){ // returns the pseudo-length of an overloadset static if( ! __
traits(compiles,A!acc) ){ enum Length=acc; } else { enum Length=Length!(A,acc+1); } }
static foreach (i; 0 .. typeAtLength) { pragma(msg, typeAt!i.stringof); } // prints: int, float, string
By treating value specialization of ints as an array, you can make an overload set that is foreachable. You can build compile-time lookup tables, generate code for each type in the set, and dispatch based on integer constants, etc.
Real-World Pattern: Unified Vector Interface
In Kap’s joka library, GVec2, GVec3, GVec4 are all parameterized by type, but what if I want dimension-generic code?
It’s an incomplete templatization:
// Separate types for each dimension alias BVec2 = GVec2!byte; alias IVec2 = GVec2!int; // ... more aliases
struct GVec2(T) { T x, y; } struct GVec3(T) { T x, y, z; } struct GVec4(T) { T x, y, z, w; }
You can’t write “works on any N-dimensional vector”; the dimension is stuck in the type name. To specialize on dimension, you can expand the shared template header to include an int parameter.
alias GVec2(T = float) = GVec!(2, T); // (optional, allows backwards compatibility) alias GVec3(T = float) = GVec!(3, T); alias GVec4(T = float) = GVec!(4, T);
// struct GVec(int N, T); // implicit, no primary definition exists
struct GVec(int N : 2, T) { T x = 0; T y = 0; } struct GVec(int N : 3, T) { T x = 0; T y = 0; T z = 0; } struct GVec(int N : 4, T) { T x = 0; T y = 0; T z = 0; T w = 0; }
Now you can write functions that take GVec!(N, T) and work across any dimension.
Pattern-matching on N at compile time allows you to easily access N in trivial meta-code:
void print(int N, T)(GVec!(N, T) v) { import std; writeln("Vector of dimension ", N, " with type ", T.stringof); static foreach (c; "xyzw"[0 .. N]) { writeln(c, ": ", mixin("v." ~ c)); } }
unittest { IVec3(1, 4, 7).print; DVec4(2, 6, 0, 0).print; }
The above is an Ad-hoc Template API. Note that no primary struct GVec(int N, T) exists in the source code. The symbol GVec exists only as a collection of specializations. The API is a coordinate map of successful matches. You are programming against the existence of a match in the resolution logic rather than a central definition.
The API that can be named is not the immortal API. -monkyyy-tzu
Swapping an Overload Set
std.conv.to is an overload set with the implied syntax of .to!T. You can let users pass their own instead for serialization.
template someSerializeFunc(alias TO_ = void, Args...)(Args args) { static if (is(TO_ == void)) { import std.conv; alias TO = std.conv.to; // Successful partial symbol resolution } else { alias TO = TO_; } // ... use args with .TO!T for conversions }
This code uses the default std.conv.to unless the user provides their own. So long as the user matches the ad-hoc API of to, they can extend it (and fix its edge cases). This flexible API, with an overloadable-overload-set, lets users fix behavior.
enum foo(int I : 0) = 0; int foo(int I : 1)(int) => 1; template foo(int I : 2) { int foo = 2; } struct foo(int I : 3) { int myint = 3; }
The overload set resolution and specialization mechanisms work on all types of templates in D. The compiler only cares about something matching the template header; you can define it however you want and make ad-hoc APIs.
With type and value specialization shared across at least four declaration patterns, which of your problems could be approached by asking, “How can I define a good template header”?
Conclusion
Overload sets aren’t exotic. Combine them with template specialization and you get ad-hoc APIs that support multiple access patterns without duplicating code. Build your types to play nice with meta-programs, design your APIs to take overload sets, and let users swap in their own implementations. This works even for more complex APIs such as std.conv.to.
Based on 2 chapters from my book, ‘Black Magic in D’.
https://crazymonkyyy.github.io/