Rant after trying Rust a bit
Jonathan M Davis via Digitalmars-d
digitalmars-d at puremagic.com
Sat Jul 25 17:31:46 PDT 2015
On Friday, 24 July 2015 at 01:09:19 UTC, H. S. Teoh wrote:
> I have trouble thinking of a template function that's actually
> *correct* when its sig constraints doesn't specify what
> operations are valid on the incoming type. Can you give an
> example?
>
> If such code is wrong, I'd say the language *should* reject it.
I see two issues here, both of which relate to maintenance. The
first one is that if the language were actually able to check
that you missed a requirement in your template constraint (like
you're suggesting) and then give you an error, that makes it way
easier to break valid code. Take code like this, for example
auto foo(T)(T t)
if(cond1!T && cond2!T)
{
...
auto b = bar(t);
...
}
auto bar(T)(T t)
if(cond2!T)
{
...
}
foo calls bar, and it does have all of bar's constraints in its
own constraints so that you don't end up with a compilation error
when you pass foo something that doesn't work with bar. Now,
imagine if bar gets updated, and now its
auto bar(T)(T t)
if(cond2!T && cond3!T)
{
...
}
but foo's constraint isn't updated (e.g. because foo is in a
different library or program that depends no bar, so the person
who updates bar isn't necessarily the same person who maintains
foo). If the compiler then caught the fact that foo didn't check
all of bar's constraints and gave an error, that would alert
anyone using foo that foo needed to be updated, but it would also
mean that foo would no longer compile, when it's quite possible
that the argument passed to foo does indeed pass bar's template
constraint and will work just fine with foo. So, working code no
longer compiles when there's no technical reason why it couldn't
continue to work. Presumably, once the maintainer of foo finds
out about this, they'll update foo, and the problem will be
fixed, but it still means that every time that the template
constraint for bar is adjusted at all, every template that uses
it risks breaking if the compiler insists that those templates
check all of bar's constraints.
So, yes. it does help ensure that users of foo don't end up with
error messages inside of foo thanks to foo's template constraint
not listing everything that it actually requires, but it also
breaks a lot of code when template constraints change when the
code itself will often work just fine as-is (particularly since
the change to bar that required a change to its template
constraint would usually be a change to its implementation and
not what it did, since if you changed what it did, everyone that
used it would be broken anyway). Code that's actually broken by
the change to bar will fail bar's new template constraint even if
the compiler doesn't complain about foo (or any other function)
not having updated its constraint, and it'll still get caught.
The error might not be as nice, since it'll often be in someone
else's templated code, but it'll still be an error, and it'll
still tell you what's failing. So, with the current state of
affairs, only code that's actually broken by a change to bar's
template constraint would be broken and not everyone, whereas
what you're suggesting would break all code that used bar that
didn't happen to also check the same thing that bar was now
checking for.
The second issue that I see with your suggestion is basically
what Walter is saying the problem is. Even if we assume that we
_do_ want to put all of the requirements for foo - direct or
indirect - in its template constraint, this causes a maintenance
problem. For instance, if foo were updated to call another
function
auto foo(T)(T t)
if(cond1!T && cond2!T && cond3!T && cond4!T)
{
...
auto b = bar(t);
...
auto c = baz(t);
...
}
auto bar(T)(T t)
if(cond2!T && cond3!T)
{
...
}
auto baz(T)(T t)
if(cond1!T && cond4!T)
{
...
}
you now have to update foo. Okay. That's not a huge deal, but now
you have two functions that you're using within foo whose
template constraints need to be duplicated in foo's template
constraint. And ever function that _they_ call ends up affecting
_their_ template constraints and then foo in turn.
auto foo(T)(T t)
if(cond1!T && cond2!T && cond3!T && cond4!T && cond5!T &&
cond6!T && cond7!T)
{
...
auto b = bar(t);
...
auto c = baz(t);
...
}
auto bar(T)(T t)
if(cond2!T && cond3!T)
{
...
auto l = lark(t);
...
}
auto baz(T)(T t)
if(cond1!T && cond4!T)
{
...
auto s = stork(t);
...
}
auto lark(T)(T t)
if(cond5!T && cond6!T)
{
...
}
auto stork(T)(T)
if(cond2!T && cond3!T && cond7!T)
{
auto w = wolf(t);
}
auto wolf(T)(T)
if(cond7!T)
{
...
}
So, foo's template constraint potentially keeps getting nastier
and nastier thanks to indirect calls that it's making. Now, often
there's going to be a large overlap between these constraints
(e.g. because they're all range-based functions using
isInputRange, isForwardRange, hasLength, etc.), so maybe foo's
constraint doesn't get that nasty. But where you still have a
maintenance problem even if that's the case is if a function
that's being called indirectly adds something to its template
constraint, then everything up the chain has to add it if you
want to make sure that foo gets no compilation internally due to
it failing to pass a template constraint of something that it's
calling. So, if wolf ends up with a slightly more restrictive
constraint in the next release, then every templated function on
the planet which used it - directly or indirectly - would need to
be updated. And much of that code could be maintained by someone
other than the person who made the change to wolf, and much of it
could be code that they've don't even know exists. So, if we're
really trying to put everything that a function requires -
directly or indirectly - in its template constraint, we
potentially have a huge maintenance problem here once you start
having templated functions call other templated functions -
especially if any of these functions are part of a library that's
distributed to others. But even if it's just your own code base,
a slight adjustment to a template constraint could force you to
change a _lot_ of the other template constraints in your code.
So, while I definitely agree that it's nicer from the user's
standpoint when the template constraint checks everything that
the function requires - directly or indirectly - I think that we
have a major maintenance issue in the making here if that's what
we insist on. Putting all of the sub-constraints in the top-level
constraint - especially with multiple levels of templated
functions - simply doesn't scale well, even if it's desirable.
Maybe some kind of constraint inference would solve the problem.
I don't know. But I think that it is a problem, and it's one that
we haven't really recognized yet.
At this point, even if we're going to try and have top-level
template constraints explicitly contain all of the constraints of
the templates that they use - directly or indirectly - I think
that we really need to make sure that the error messages from
within templated code are as good as we can make them, because
there's no way that all template constraints are going to contain
all of their sub-constraints as code is changed over time, not
unless the constraints are fairly simple and looking for the same
stuff.
Fortunately, the error messages are a lot better than they used
to be, but if we can improve them sufficiently, then it becomes
less critical to make sure that all sub-constraints be in the
top-level constraint, and it makes it a lot more palatable when
sub-constraints are missed.
But as I said in the first part, I really don't think that
detecting missing constraints and giving errors is a good
solution. It'll just break more code that way. Rather, what we
need is to either find a way to infer the sub-constraints into
the top-level constraint and/or to provide really good error
messages when errors show up inside templated code, because a
constraint didn't check enough.
- Jonathan M Davis
More information about the Digitalmars-d
mailing list