Duck typing to a statically-defined interface

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.

Getting ready

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.

How to do it…

Let's execute the following steps to use duck typing in a statically-defined interface:

  1. Write a wrap function that loops over all the virtual methods of the interface and builds code based on it.
  2. Use std.traits or .stringof to extract the signature of each method and reproduce it as a code string.
  3. Write the body of each method as a string which calls the corresponding method on the struct block.
  4. Print out the code with pragma(msg) or writeln (running it at runtime) to check it against what you write manually to help while debugging.
  5. Create a class which inherits from the static interface and use mixin to compile your generated code inside that class.
  6. Return the new object for use.

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
}

How it works…

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.

Tip

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.

There's more…

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.75.70