Rant after trying Rust a bit

Jonathan M Davis via Digitalmars-d digitalmars-d at puremagic.com
Sat Jul 25 21:34:53 PDT 2015


On Sunday, 26 July 2015 at 02:15:20 UTC, jmh530 wrote:
> On Sunday, 26 July 2015 at 00:31:48 UTC, Jonathan M Davis wrote:
>> On Friday, 24 July 2015 at 01:09:19 UTC, H. S. Teoh wrote:
>>
>> 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
>>
>
> I think from your post I finally understand what Walter was 
> getting at.
>
> Not sure if this simplifies things, but what if instead you do 
> something like
>
> void foo(T)(T t)
> 	if (__traits(compiles, bar(t) && __traits(compiles, baz(t)))
> {
>     ...
>     auto b = bar(t);
>     ...
>     auto c = baz(t);
>     ...
> }
>
> This only really works in the case where it's obvious that you 
> are calling some bar(t). It might not work more generally...
>
> Anyway, in your next example, you have additional layers with 
> some more condn!T constraints. If instead you just have more 
> __traits(compiles, x) for whatever templates they are calling, 
> then checking that bar compiles necessarily also tests whether 
> those other functions can compile as well. In this way, you're 
> testing all the constraints at lower levels. So I guess you 
> would still be checking that each constraint works, but at the 
> highest level you only have to specify that the templates you 
> are calling compile.
>
> In my opinion this is superior (for this case) because if you 
> change bar and baz then you don't have to make changes to foo.
>
> Am I wrong? Is this just another way of doing the same thing?

I suggested the same in response to Walter earlier. It is one way 
to combat the problem. However, it's really only going to work in 
basic cases (at least, without getting ugly). What if what you 
passed to bar wasn't t but was a result from calling a function 
on t? Or maybe it was a result of calling a function on the 
return value of a function that was called on t? Or perhaps you 
passed t through a chain of free functions and ended up with some 
other type from that, and it doesn't pass bar's template 
constraint? In order to deal with that sort of thing, pretty 
soon, you have to put most of the function inside its own 
constraint. It's _far_ cleaner in general to just be putting the 
sub-constraints in the top-level constraint - e.g. maybe all that 
it means is using isForwardRange and hasLength instead of just 
isInputRange rather than putting a whole chain of function calls 
inside of __traits(compiles, ...) test in the template 
constraint. It just gets ugly quickly to try and get it to work 
for you automatically by putting the calls you're making in the 
constraint so that the actual constraint conditions are inferred.

The other problem is that if you're putting all of those 
__traits(compiles, ...) tests in template constraints rather than 
putting the sub-constraints in there, it makes it a lot more of a 
pain for the user to figure out why they're failing the 
constraint. The constraint for what's failing in 
__traits(compiles, ...) isn't shown, whereas it would be if you 
just let it get past the template constraint and fail the 
sub-constraint at the point where that function is being called, 
you'd see the actual condition that's failing. So, as annoying as 
it would be, it would actually be easier to figure out what you 
were doing wrong. Also, if you really didn't put the 
sub-constraint in the top-level constraint at all, then the 
constraint is split out so that when you get a failure at the 
top-level, you see only the stuff that the function requires 
directly, and when you get a failure internally, you see it the 
condition that that function requires and can see that 
separately. So, instead of having to figure out which part of 
condition1 && condition2 is failing, you know which it is, 
because the conditions are tested in separate places.

I suspect that the best way to go with this is that a template 
constraint only require the stuff that a function uses directly  
and let the constraints on any functions being called internally 
report their own errors and then have the compiler provide really 
good error messages to make that sane. Then it can be a lot 
clearer what condition you're failing when you call the function 
with a bad argument. But we need to improve the error messages 
further if we want to go that way.

The other alternative would be to just make a best faith effort 
to put all of the sub-constraints in the top-level constraint 
initially and then have better error messages for when the 
constraint is incomplete due to a change to a function being 
called. But that would still require better error messages (which 
is the main problem with the other suggestion), and it actually 
has the problem that if a function being called has its 
constraint lessened (rather than made more strict) such that your 
outer function could then accept more types of arguments, if you 
put all of the sub-constraints at the top-level, then it won't 
accept anything more until you realize that the sub-constraints 
have changed and update the top-level constraint.

Right now, we're more or less living with the second option, but 
if we can get the error messages to be good enough, I think that 
the first option is actually better. But either way, we need to 
find ways to improve the error messages inside of templates to 
reduce the need to look at their source code when a template 
constraint doesn't prevent an argument being used with it that 
doesn't compile with it (particularly in the cases where it's due 
to a function being called rather than that function itself 
having a bug).

- Jonathan M Davis


More information about the Digitalmars-d mailing list