Generality creep

Andrei Alexandrescu SeeWebsiteForEmail at erdani.org
Tue Mar 19 02:52:34 UTC 2019


Walter and I were looking at indexOf in the standard library. It has 
several overloads, of which let's look at the first two:

https://dlang.org/library/std/string/index_of.html

Now, why couldn't they be merged into a single function with this 
constraint (their bodies are identical):

ptrdiff_t indexOf(Range) (
   Range s,
   dchar c,
   CaseSensitive cs = Yes.caseSensitive
)
if (isInputRange!Range && isSomeChar!(ElementType!Range));

It makes sense to look for a character in any range of characters.

Attempting to build that fails because of this type fails to work:

     struct TestAliasedString
     {
         string get() @safe @nogc pure nothrow { return _s; }
         alias get this;
         @disable this(this);
         string _s;
     }

The intuition is that the function should be general enough to figure 
out that, hey, TestAliasedString is kinda sorta a subtype of string. So 
the call should work.

So let's see why it doesn't work - i.e. why is TestAliasedString an 
input range? The definition of isInputRange (in std.range.primitives) is:

enum bool isInputRange(R) =
     is(typeof(R.init) == R)
     && is(ReturnType!((R r) => r.empty) == bool)
     && is(typeof((return ref R r) => r.front))
     && !is(ReturnType!((R r) => r.front) == void)
     && is(typeof((R r) => r.popFront));

Turns out the second clause fails. That takes us to the definition of 
empty in the same module:

@property bool empty(T)(auto ref scope const(T) a)
if (is(typeof(a.length) : size_t))
{
     return !a.length;
}

The intent is fairly clear - if a range defines empty as a size_t 
(somewhat oddly relaxed to "convertible to size_t"), then empty can be 
nicely defined in terms of length. Cool. But empty doesn't work with 
TestAliasedString due to an overlooked matter: the "const". A mutable 
TestAliasedString converts to a string, but a const or immutable 
TestAliasedString does NOT convert to a const string! So this fixes that 
matter:

     struct TestAliasedString
     {
         string get() @safe @nogc pure nothrow { return _s; }
         const(string) get() @safe @nogc pure nothrow const { return _s; }
         alias get this;
         @disable this(this);
         string _s;
     }

That makes empty() work, but also raises a nagging question: what was 
the relationship of TestAliasedString to string before this change? 
Surely that wasn't subtyping. (My response would be: "Odd.") And why was 
Phobos under the obligation to cater for such a type and its tenuous 
relationship to a range?

But wait, there's more. Things still don't work because of popFront. 
Looking at its definition:

void popFront(C)(scope ref inout(C)[] str) @trusted pure nothrow
if (isNarrowString!(C[]))
{ ... }

So, reasonably this function takes the range by reference so it can 
modify its internals. HOWEVER! The implementation of 
TestAliasedString.get() returns an rvalue, i.e. it's the equivalent of a 
conversion involving a temporary. Surely that's not to match, whether in 
the current language or the one after the rvalue DIP.

The change that does make the code work is:

     struct TestAliasedString
     {
         ref string get() @safe @nogc pure nothrow { return _s; }
         ref const(string) get() @safe @nogc pure nothrow const { return 
_s; }
         alias get this;
         @disable this(this);
         string _s;
     }

This indeed does implement a subtyping relationship, and passes the 
isInputRange test.

What's the moral of the story here? Generality is good, but it seems in 
several places in phobos (of which this is just one example), a 
combination of vague specification and aiming for a nice ideal of "work 
with anything remotely reasonable" has backfired into a morass of 
inconsistently defined and supported corner cases.

For this case in particular - I don't think we should support all types 
that support some half-hearted form of subtyping, at the cost of 
reducing generality and deprecating working code.


More information about the Digitalmars-d mailing list