D UFCS anti-pattern

Steven Schveighoffer via Digitalmars-d digitalmars-d at puremagic.com
Thu Apr 24 19:21:32 PDT 2014


Recently, I observed a conversation happening on the github pull request  
system.

In phobos, we have the notion of output ranges. One is allowed to output  
to an output range by calling the function 'put'.

Here is the implementation of put:

void put(R, E)(ref R r, E e)
{
     static if(is(PointerTarget!R == struct))
         enum usingPut = hasMember!(PointerTarget!R, "put");
     else
         enum usingPut = hasMember!(R, "put");

     enum usingFront = !usingPut && isInputRange!R;
     enum usingCall = !usingPut && !usingFront;

     static if (usingPut && is(typeof(r.put(e))))
     {
         r.put(e);
     }
     else static if (usingPut && is(typeof(r.put((E[]).init))))
     {
         r.put((&e)[0..1]);
     }
     else static if (usingFront && is(typeof(r.front = e, r.popFront())))
     {
         r.front = e;
         r.popFront();
     }
     else static if ((usingPut || usingFront) && isInputRange!E &&  
is(typeof(put(r, e.front))))
     {
         for (; !e.empty; e.popFront()) put(r, e.front);
     }
     else static if (usingCall && is(typeof(r(e))))
     {
         r(e);
     }
     else static if (usingCall && is(typeof(r((E[]).init))))
     {
         r((&e)[0..1]);
     }
     else
     {
         static assert(false,
                 "Cannot put a "~E.stringof~" into a "~R.stringof);
     }
}

There is an interesting issue here -- put can basically be overridden by a  
member function of the output range, also named put. I will note that this  
function was designed and written before UFCS came into existence. So most  
of the machinery here is designed to detect whether a 'put' member  
function exists.

One nice thing about UFCS, now any range that has a writable front(), can  
put any other range whose elements can be put into front, via the  
pseudo-method put.

In other words:

void foo(int[] arr)
{
    int[] result = new int[arr.length];
    result.put(arr); // put arr into result.
}

But there is an issue with this. If the destination range actually  
implements the put member function, but doesn't implement all of the  
global function's niceties,
r.put(...) is not as powerful/useful as put(r,...). Therefore, the odd  
recommendation is to *always* call put(r,...)

I find this, at the very least, to be confusing. Here is a case where UFCS  
ironically is not usable via a function call that so obviously should be  
UFCS.

The anti-pattern here is using member functions to override or specialize  
UFCS behavior. In this case, we even hook the UFCS call with the member  
function, encouraging the name conflict!

As a possible solution, I would recommend simply change the name of the  
hook, and have the UFCS function forward to the hook. This way, calling  
put(r,...) and r.put(...) is always consistent.

Does this make sense? Anyone have any other possible solutions?

A relevant bug report (where I actually advocate for adding more of this  
horrible behavior): https://issues.dlang.org/show_bug.cgi?id=12583

-Steve


More information about the Digitalmars-d mailing list