Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

duck typing in D

I'm new to D, and I was wondering whether it's possible to conveniently do compile-time-checked duck typing.

For instance, I'd like to define a set of methods, and require that those methods be defined for the type that's being passed into a function. It's slightly different from interface in D because I wouldn't have to declare that "type X implements interface Y" anywhere - the methods would just be found, or compilation would fail. Also, it would be good to allow this to happen on any type, not just structs and classes. The only resource I could find was this email thread, which suggests that the following approach would be a decent way to do this:

void process(T)(T s)
    if( __traits(hasMember, T, "shittyNameThatProbablyGetsRefactored"))
    // and presumably something to check the signature of that method
{
    writeln("normal processing");
}

... and suggests that you could make it into a library call Implements so that the following would be possible:

struct Interface {
    bool foo(int, float);
    static void boo(float);
    ...
}

static assert (Implements!(S, Interface));
struct S {
    bool foo(int i, float f) { ... }
    static void boo(float f) { ... }
    ...
}

void process(T)(T s) if (Implements!(T, Interface)) { ... }

Is is possible to do this for functions which are not defined in a class or struct? Are there other/new ways to do it? Has anything similar been done?

Obviously, this set of constraints is similar to Go's type system. I'm not trying to start any flame wars - I'm just using D in a way that Go would also work well for.

like image 570
Dan Avatar asked May 16 '13 03:05

Dan


1 Answers

This is actually a very common thing to do in D. It's how ranges work. For instance, the most basic type of range - the input range - must have 3 functions:

bool empty();  //Whether the range is empty
T front();  // Get the first element in the range
void popFront();  //pop the first element off of the range

Templated functions then use std.range.isInputRange to check whether a type is a valid range. For instance, the most basic overload of std.algorithm.find looks like

R find(alias pred = "a == b", R, E)(R haystack, E needle)
if (isInputRange!R &&
    is(typeof(binaryFun!pred(haystack.front, needle)) : bool))
{ ... }

isInputRange!R is true if R is a valid input range, and is(typeof(binaryFun!pred(haystack.front, needle)) : bool) is true if pred accepts haystack.front and needle and returns a type which is implicitly convertible to bool. So, this overload is based entirely on static duck typing.

As for isInputRange itself, it looks something like

template isInputRange(R)
{
    enum bool isInputRange = is(typeof(
    {
        R r = void;       // can define a range object
        if (r.empty) {}   // can test for empty
        r.popFront();     // can invoke popFront()
        auto h = r.front; // can get the front of the range
    }));
}

It's an eponymous template, so when it's used, it gets replaced with the symbol with its name, which in this case is an enum of type bool. And that bool is true if the type of the expression is non-void. typeof(x) results in void if the expression is invalid; otherwise, it's the type of the expression x. And is(y) results in true if y is non-void. So, isInputRange will end up being true if the code in the typeof expression compiles, and false otherwise.

The expression in isInputRange verifies that you can declare a variable of type R, that R has a member (be it a function, variable, or whatever) named empty which can be used in a condition, that R has a function named popFront which takes no arguments, and that R has a member front which returns a value. This is the API expected of an input range, and the expression inside of typeof will compile if R follows that API, and therefore, isInputRange will be true for that type. Otherwise, it will be false.

D's standard library has quite a few such eponymous templates (typically called traits) and makes heavy use of them in its template constraints. std.traits in particular has quite a few of them. So, if you want more examples of how such traits are written, you can look in there (though some of them are fairly complicated). The internals of such traits are not always particularly pretty, but they do encapsulate the duck typing tests nicely so that template constraints are much cleaner and more understandable (they'd be much, much uglier if such tests were inserted in them directly).

So, that's the normal approach for static duck typing in D. It does take a bit of practice to figure out how to write them well, but that's the standard way to do it, and it works. There have been people who have suggested trying to come up with something similar to your Implements!(S, Interface) suggestion, but nothing has really come of that of yet, and such an approach would actually be less flexible, making it ill-suited for a lot of traits (though it could certainly be made to work with basic ones). Regardless, the approach that I've described here is currently the standard way to do it.

Also, if you don't know much about ranges, I'd suggest reading this.

like image 63
Jonathan M Davis Avatar answered Sep 25 '22 14:09

Jonathan M Davis