Skip to content
HN On Hacker News ↗

(Ab)using Overload Sets to Create Ad-Hoc Template APIs

▲ 14 points • 2 comments • by WalterBright • 4mo ago • HN discussion ↗

Pangram verdict · v3.3

We believe that this document is fully human-written

2 %

AI likelihood · overall

Human
100% human-written 0% AI-generated
SEGMENTS · HUMAN 4 of 4
SEGMENTS · AI 0 of 4
WORD COUNT 978
PEAK AI % 4% · §4
Analyzed
Jul 21
backend: pangram/v3.3
Segments scanned
4 windows
avg 245 words each
Distribution
100 / 0%
human / AI fraction
Verdict
Human
Pangram v3.3

Article text · 978 words · 4 segments analyzed

Human AI-generated
§1 Human · 1%

“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( ! __

§2 Human · 1%

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.

§3 Human · 4%

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.

§4 Human · 4%

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/