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