Rant after trying Rust a bit
Jonathan M Davis via Digitalmars-d
digitalmars-d at puremagic.com
Fri Jul 24 22:59:56 PDT 2015
On Friday, 24 July 2015 at 04:42:59 UTC, Walter Bright wrote:
> On 7/23/2015 3:12 PM, Dicebot wrote:
>> On Thursday, 23 July 2015 at 22:10:11 UTC, H. S. Teoh wrote:
>>> OK, I jumped into the middle of this discussion so probably
>>> I'm speaking
>>> totally out of context...
>>
>> This is exactly one major advantage of Rust traits I have been
>> trying to
>> explain, thanks for putting it up in much more understandable
>> way :)
>
> Consider the following:
>
> int foo(T: hasPrefix)(T t) {
> t.prefix(); // ok
> bar(t); // error, hasColor was not specified for T
> }
>
> void bar(T: hasColor)(T t) {
> t.color();
> }
>
> Now consider a deeply nested chain of function calls like this.
> At the bottom, one adds a call to 'color', and now every
> function in the chain has to add 'hasColor' even though it has
> nothing to do with the logic in that function. This is the pit
> that Exception Specifications fell into.
>
> I can see these possibilities:
>
> 1. Require adding the constraint annotations all the way up the
> call tree. I believe that this will not only become highly
> annoying, it might make generic code impractical to write
> (consider if bar was passed as an alias).
>
> 2. Do the checking only for 1 level, i.e. don't consider what
> bar() requires. This winds up just pulling the teeth of the
> point of the constraint annotations.
>
> 3. Do inference of the constraints. I think that is
> indistinguishable from not having annotations as being
> exclusive.
>
>
> Anyone know how Rust traits and C++ concepts deal with this?
I don't know about this. The problem is that if you don't list
everything in the constraint, then the user is going to get an
error buried in your templated code somewhere rather than in
their code, which is _not_ user friendly and is why we usually
try and put everything required in the template constraint. On
the other hand, you're very much right in that this doesn't scale
if you have enough levels of template constraints, especially if
some of the constraints in the functions being called internally
change. And yet, the caller needs to know what the requirements
are of the template or templated function actually are when they
pass it something. So, it does kind of need to be at the top
level from that aspect of usability as well. So, this is just
plain ugly regardless.
One option which would work at least some of the time would be to
do something like
void foo(T)(T t)
if(hasPrefix!T && is(typeof(bar(t))))
{
t.prefix();
bar(t);
}
void bar(T)(T t)
if(hasColor!T)
{
t.color();
}
then you don't have to care what the current constraint for bar
is, and it still gets checked in foo's template constraint.
...
Actually, I just messed around with some of this to see what
error messages you get when foo doesn't check for bar's
constraints in its template constraint, and it's a _lot_ better
than it used to be. This code
void foo(T)(T t)
if(hasPrefix!T)
{
t.prefix();
bar(t);
}
void bar(T)(T t)
if(hasColor!T)
{
t.color();
}
struct Both { void prefix() { } void color() { } }
struct OneOnly { void prefix() { } }
enum hasPrefix(T) = __traits(hasMember, T, "prefix");
enum hasColor(T) = __traits(hasMember, T, "color");
void main()
{
foo(Both.init);
bar(Both.init);
foo(OneOnly.init);
}
results in these error messages:
q.d(5): Error: template q.bar cannot deduce function from
argument types !()(OneOnly), candidates are:
q.d(8): q.bar(T)(T t) if (hasColor!T)
q.d(25): Error: template instance q.foo!(OneOnly) error
instantiating
It tells you exactly which line in your code is wrong (which it
didn't used to when the error was inside the template), and it
clearly gives you the template constraint which is failing,
whereas if you foo tests for bar in its template constraint, you
get this
q.d(25): Error: template q.foo cannot deduce function from
argument types !()(OneOnly), candidates are:
q.d(1): q.foo(T)(T t) if (hasPrefix!T &&
is(typeof(bar(t))))
And that doesn't tell you anything about what bar requires.
Actually putting bar's template constraint in foo's template
constraint would fix that, but then you wouldn't necessarily know
which is failing, and you have the maintenance problem caused by
having to duplicate bar's constraint.
So, I actually think that how the current implementation reports
errors makes it so that maybe it's _not_ a good idea to put all
of the sub-constraints within the top-level constraint, because
it actually makes it harder to figure out what you've done wrong.
Unfortunately, it probably requires that you look at the source
code of the templated function that you're calling regardless,
since the error message doesn't actually make it clear that it's
the argument that you passed to foo that's being passed to bar
rather than an actual bug in foo (and to make matters more
complicated, it could actually be something that came from what
you passed to foo rather than actually being what you passed in).
So, maybe we could improve the error messages further to make it
clear that it was what you passed in or something about where it
came from so that you wouldn't necessarily have to look at the
source code, and if so, I think that that solves the problem
reasonably well. It would avoid the maintenance problem of having
to propagate the constraints, and it would actually give clearer
error messages than propagating the constraints. And having
overly complicated template constraints is one of the most
annoying aspects of dealing with template constraints, because it
makes it a lot harder to figure out why they're failing. So,
_not_ putting the sub-constraints in the top-level constraint
could make it easier to figure out what's gone wrong.
So, honestly, I think that we have the makings here of a far
better solution than trying to put everything in the top-level
template constraint. This could be a good part of the solution
that we've needed to improve error-reporting associated with
template constraints.
In any case, looking at this, I have to agree with you that this
is the same problem you get with checked exceptions / exceptions
specifications - only worse really, because you can't do "throws
Exception" and be done with it like you can in Java (as hideous
as that is). Rather, you're forced to do the equivalent of
listing all of the exception types being thrown and maintain that
list as the code changes - i.e. you have to make the top-level
template constraint list all of the sub-constraints and keep that
list up-to-date as the sub-constraints change, which is a mess,
especially with deep call stacks of templated functions.
- Jonathan M Davis
More information about the Digitalmars-d
mailing list