D UFCS anti-pattern

Jonathan M Davis via Digitalmars-d digitalmars-d at puremagic.com
Thu Apr 24 21:58:51 PDT 2014


On Thu, 24 Apr 2014 22:21:32 -0400
Steven Schveighoffer via Digitalmars-d <digitalmars-d at puremagic.com>
wrote:

> 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

If it doesn't work to override a free function with a member
function, I honestly don't see much point to UFCS. The whole idea
behind it is to make it so that you don't have to care whether a
function is a free function or a member function. The current situation
essentially forces you to not use UFCS except in cases where you're
trying to add "member functions" to built-in types. And as such,
calling functions on user-defined types using UFCS runs a high risk of
not compiling, because all it takes is for the user-defined type to
define a function with the same name - even if it takes completely
different arguments - and now the compiler won't even try to use the
free function anymore.

I really think that we should fix it so that stuff like
outputRange.put(foo) works - including when types define put
themselves. AFAIK, that means changing the overload rules so that
member functions conflict with free functions only when they take the
same arguments - in which case the member function would be called, as
it is now, except that the cases where a free function matches the
arguments would also work, allowing us to override free functions with
member functions where appropriate and prevent simple name collisions
from making UFCS not work (i.e. when the member function takes
completely different arguments, UFCS would still use the free
function). Without a change along those lines, I'd be strongly inclined
to argue against using UFCS in any situation except in those where you
need to add "member functions" to the built-in types. And the only
common case for that that I'm aware of is making it so that arrays can
function as ranges.

But this issue goes far beyond put.

- Jonathan M Davis


More information about the Digitalmars-d mailing list