A distinguishing feature of the Go programming language is static interfaces that do not need to be explicitly inherited. Using D's compile-time reflection and code generation capabilities, we can do something similar.
Let's start by defining a goal. We'll write an interface and a struct
block that could implement the interface, but which does not explicitly inherit from it (the struct
blocks in D can not inherit from the interfaces at all). We'll also write the following brief function that uses the interface to perform a task:
interface Animal { void speak(); void speak(string); } struct Duck { void speak() { import std.stdio; writeln("Quack!"); } void speak(string whatToSay) { import std.stdio; writeln("Quack! ", whatToSay); } } void callSpeak(Animal a) { a.speak(); a.speak("hello"); } void main() { Duck d; // callSpeak(wrap!Animal(&d)); // we want to make this line work }
The callSpeak
function will not work out of the box with Duck
, because Duck
does not statically inherit from the interface. If we wanted to do this manually, we could write a small class that implements the interface by forwarding all the required methods to a member, Duck
. Let's see how to do this automatically.
Let's execute the following steps to use duck typing in a statically-defined interface:
wrap
function that loops over all the virtual methods of the interface and builds code based on it.std.traits
or .stringof
to extract the signature of each method and reproduce it as a code string.struct
block.pragma(msg)
or writeln
(running it at runtime) to check it against what you write manually to help while debugging.The code related to the preceding steps is as follows:
T wrap(T, R)(R* r) { static string wrapperImpl() { if(__ctfe) { // this helper function will find matching parentheses // to navigate the string. It does not handle parentheses // in string literals, which may occur with a default // argument (which be seen in this string too), but // that's rare enough that we can ignore it for many cases int matchParens(string s, int idx) { if(s[idx] == '(') { // search forward for match int count; for(int i = idx; i < s.length; i++) { if(s[i] == '(') count++; else if(s[i] == ')') count--; if(count == 0) return i; } assert(0); // unbalanced } else if(s[idx] == ')') { // search backward for match int count; for(int i = idx; i >= 0; i--) { if(s[i] == '(') count++; else if(s[i] == ')') count--; if(count == 0) return i; } assert(0); // unbalanced } else assert(0); } // loop over the interface and find functions… string code; foreach(memberName; __traits(allMembers, T)) { static if(is(typeof(__traits(getMember, T, memberName)) == function)) { foreach(overload; __traits(getOverloads, T, memberName)) { // the string gives the return value, arguments list, and attributes // const pure @safe const(char)*(int, string) auto str = typeof(overload).stringof; // pragma msg can help a lot to see the strings // you're working with //pragma(msg, typeof(overload).stringof); auto argIdx = matchParens(str, str.length - 1); string argList = ""; string argsStr; int count = 0; // we'll build strings to make the class code // and forwarding function call argsStr = "("; const argStrInput = str[argIdx + 1 .. $ - 1]; // loops each character, looking for commas which // separate arguments foreach(idx, argc; argStrInput) { if(argc != ',') argsStr ~= argc; if(argc == ',' || idx == argStrInput.length - 1) { import std.conv; auto part = " a" ~ to!string(count++); argsStr ~= part; if(argList.length) argList ~= ","; argList ~= part; } if(argc == ',') argsStr ~= argc; } argsStr ~= ")"; code ~= str[0 .. argIdx] ~ " " ~ memberName ~ " " ~ argsStr; code ~= "{ return r." ~ memberName ~ "(" ~ argList ~ "); } "; } } } return code; } else return null; } //pragma(msg, wrapperImpl()); // for debugging help // create a class that statically inherits the interface static class Wrapped : T { R* r; this(R* r) { this.r = r; } // finally, add the code we generated above to the class mixin(wrapperImpl()); } return new Wrapped(r); // pass it a reference to the object }
The most difficult part of the wrap
function's implementation is reproducing the same function signatures the interface uses. Here, we took the shortest path: as simply as possible, using the string the compiler provides with the .stringof
property of each method.
The .stringof
property on a function gives its signature, including the return type, arguments, and any modifiers that affect its type. To successfully override the method, all these details need to be correct—except argument names, which do not need to match (indeed, function arguments don't need names at all, especially in the interfaces where the names are not referenced anyway).
Since the string returned by the compiler is always well-formed, we can depend on matching parentheses to navigate to the string. The only thing that can complicate this is the presence of a default string argument value containing the unmatched parentheses. We could also watch for quotes to handle this case. The example skipped this to keep the code shorter, since it is not needed for most of the functions.
Starting from the right-hand side, we match the parentheses to isolate the argument list. The .stringof
property always puts modifiers on the left-hand side, so we know the last character is always the closing parenthesis of the argument list. While this kind of assumption could be labelled a hack, it also gets the job done with the minimum of hassle.
Once the argument list, return type, and attributes are isolated, we start to build the string that implements the interface. The code generation method here is to build the D code as a string. This can result in a very messy implementation, but focus on just one part at a time: split the interface argument string using commas to get the types of each argument, and then build the new function's argument list by piecing them back together with new commas and a consistent pattern of argument names. Simultaneously, build a call argument list by putting together only the names. Here's what we aim to generate:
void foo(int, string); // the interface we need to match // the implementation must match types in the argument list, // but the names need not match. So we'll use a simple numbered // pattern override void foo(int arg1, string arg2) { // when calling the function, we only want the names, no types // in this string. return r.foo(arg1, arg2); }
The constructed strings build this code, one piece at a time.
While the void
values cannot be assigned to values or used as function arguments, they can be used in a return
statement. When forwarding to another function, we can therefore always use return f()
and the compiler will do the right thing without error, even if f()
returns void
. This greatly simplifies the generation process.
Once the code is built, we use the mixin
expression to compile it into our implementation class. This works like copying and pasting the string into the class at compile time. While the mixin
expression cannot create partial declarations, it can create a list of complete methods to be added to an incomplete class.
Here, we created a static nested class with a constructor that takes a pointer to the object we're wrapping and mixes in the methods needed to implement the interface. Finally, the instance of that class is returned via the interface. As far as the outside world is concerned, the specifics of this class aren't important, it just needs to implement the interface.
If it is impossible to implement the interface successfully by wrapping the input object, the class will not compile, causing the build to fail. It is still static compile-time duck typing, and not a dynamic type wrapper.
Phobos' std.typecons.wrap
function does this same job more thoroughly than we do here; however, it has the caveat that it currently only works on classes. With our wrap
function, we took the structure by pointer, allowing the wrapped class to operate on the original data and matching most user expectations since interfaces are expected to be able to act on the object. However, this has a potential trade-off that the struct
block might go out of scope and be destroyed while an instance of the wrapped class still exists, leaving a dangling reference.
A solution to that problem would be to have a private copy of the struct
block in the generated class. However, this no longer updates the original data, limiting the potential of duck typing to several interfaces at once. Neither solution is perfect, but both can be automatically generated, so you may pick the strategy that is right for your use case.
18.117.75.70