www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Yes, constraints can have helpful error messages!

reply Marvin Hannott <i28muzw0n relay.firefox.com> writes:
I was a bit surprised, reading the discussions about how 
constraints aren't very helpful when it comes to figuring out 
what is wrong. And I was even more surprised that of all things 
`pragma` was called to the rescue. But isn't there a much better, 
simpler way?

Now, I don't like being the idiot who thinks he found a gold vein 
in no man's land, thinking that no one has ever considered my 
approach, so please be critical. And though I got some experience 
in D, I am by no means an expert.

But let's just have a look at the constraint `isFilter`, which 
checks whether a function is a suitable filter (for FilterRange, 
or whatever. Let's not overthink it).

```D

template isFilter(alias filter, bool asserts = false)
{
     enum isFilter=
     {
         import std.traits : ReturnType, Parameters, isMutable, 
isScalarType;
         alias RT = ReturnType!(typeof(filter));
         static if(!is(RT == bool))
         {
             static assert(!asserts,expect!(RT, bool, "Return"));
             return false;
         }

         alias params = Parameters!filter;
         static if(params.length != 1)
         {
             static assert(!asserts, expect!(cast(int) 
params.length, 1,
             "Number of arguments"));
             return false;
         }

         alias param = params[0];
         static if(isMutable!param && !isScalarType!param)
         {
             static assert!(!asserts, "Argument must be constant 
or a scalar type");
             return false;
         }

         return true;
     }();
}
template expect(alias actual, alias expected,string descr)
{
     enum expect=
     descr~": Got '"~actual.stringof~"', but expected 
'"~expected.stringof~"'";
}

template expect(Actual, Expected, string descr, bool convertable 
= false)
{
     enum expect =
     {
         static if(convertable)
         {
             enum equalType = is(Actual : Expected);
         }
         else
         {
             enum equalType = is(Actual == Expected);
         }

         return !equalType ?
         descr~": Got '"~Actual.stringof~"', but expected 
'"~Expected.stringof~"'" : "";
     }();
}

```
I think this is a reasonably complex example that demonstrates my 
point. (Let's not focus on whether these conditions are actually 
reasonable or not.)

As you can see, there is just a simple template switch which 
controls whether an `AssertionError` will be thrown at 
compile-time or not. What that means is that `isFilter` can be 
used as regular constraint in an `if()`-statement, but also as 
compile-time interface (in this case most likely inside a 
function body) that gives helpful error messages. And I would 
daresay that this method is reasonably convenient, and could 
certainly be made even more convenient with more helper functions 
and/or mixins.

And best of all: it's completely backwards compatible (if 
`asserts` defaults to `false`)!

So, why aren't we doing this? Is it really just because of 
`__traits(compiles,...)`, which some people have suggested (and 
it is kinda everywhere)? But even if so, there is no reason this 
wouldn't work with `__traits(compiles,...)` as well. Just need 
some good helper functions.
Apr 08 2022
next sibling parent reply vit <vit vit.vit> writes:
On Saturday, 9 April 2022 at 01:03:32 UTC, Marvin Hannott wrote:
 I was a bit surprised, reading the discussions about how 
 constraints aren't very helpful when it comes to figuring out 
 what is wrong. And I was even more surprised that of all things 
 `pragma` was called to the rescue. But isn't there a much 
 better, simpler way?

 [...]
Many constraitns are one use only and crating special templates for them is cumbersome. It create template bloat and for example your code doesn't work with templates or generic lambdas: ```d bool pred_a(int i){return false;} bool pred_b(T)(auto ref T x){return true;} void main(){ static assert(isFilter!(pred_a)); static assert(isFilter!(pred_b)); //fail } ``` Now you must forawrd parameters to isFilter and make more checks, template bloat is even bigger, interface of isFilter break because parameters must be variadic...
Apr 08 2022
parent Marvin Hannott <i28muzw0n relay.firefox.com> writes:
On Saturday, 9 April 2022 at 05:01:51 UTC, vit wrote:
 Many constraitns are one use only and crating special templates 
 for them is cumbersome.
Obviously, this is meant for (non trivial) public constraints. What you do privately is no one's business. But public constraints should always be helpful instead of confusing. That's at least my opinion, for what it's worth. I would still claim that this approach is reasonably convenient. And when it becomes too hard to bother about good user experience because of language barriers, well... But I could certainly think of a few ways (involving string mixins) to make this approach really shine. When I got some time to experiment I might release it as (experimental) library. I don't know what's "special" about my template. Aren't all constraints necessarily templates? At least they must be computable at compile-time.
It create template bloat and for
 example your code doesn't work with templates or generic 
 lambdas:
Well, I never expected this example to be water tight. It was merely an experiment. But I would daresay that being restrictive instead of allowing everything isn't a bad thing. Some interfaces in Phobos are really obscure because they overdid it, trying to be as general as possible. And if it becomes hard to test your interface, then maybe it is wrong. (Apologies for sounding like a smartass.) But I am curious: why wouldn't the caller instantiate the template first? I don't think there is more meta-magic necessary, or at least it shouldn't be. And generic lambdas are a mess, and I am not sure they should exist. The error messages when something goes wrong are unhelpful to say the least. Which is the exact point that brings us here. But sorry, for going off-topic. Wouldn't want to go down a different rabbit whole. But on your point on template bloat: isn't that only the case in the debug version?
interface of isFilter
 break because parameters must be variadic...
I am not sure I understand this point. Do you mean functions with more than one parameter should be accepted? Why would that be useful? Like I said, I think being restrictive isn't a bad thing. But sure, that is only my opinion.
There is another big problem with your solution:
```D template isInt(T){ static assert(is(T == int), "T is not int" ); enum isInt = is(T == int); } void test(T)(T val) if(isInt!T){ } void test(double val){ } void main() { test(int.init); test(double.init); } ```
Function overloading doesn't work .
Well, `isInt` always asserts, which is the complete opposite of what I was suggesting. Of course it *shouldn't* assert when used as constraint in an `if()`-statement like in `test`, but it should assert at "implementation"-site inside a class/struct/function to make certain it fulfills some constraint / implements some interface, and to give helpful error messages when it doesn't.
Apr 09 2022
prev sibling next sibling parent vit <vit vit.vit> writes:
On Saturday, 9 April 2022 at 01:03:32 UTC, Marvin Hannott wrote:
 I was a bit surprised, reading the discussions about how 
 constraints aren't very helpful when it comes to figuring out 
 what is wrong. And I was even more surprised that of all things 
 `pragma` was called to the rescue. But isn't there a much 
 better, simpler way?

 [...]
There is another big problem with your solution: ```d template isInt(T){ static assert(is(T == int), "T is not int" ); enum isInt = is(T == int); } void test(T)(T val) if(isInt!T){ } void test(double val){ } void main() { test(int.init); test(double.init); } ``` Function overloading doesn't work .
Apr 09 2022
prev sibling parent reply Atila Neves <atila.neves gmail.com> writes:
On Saturday, 9 April 2022 at 01:03:32 UTC, Marvin Hannott wrote:
 I was a bit surprised, reading the discussions about how 
 constraints aren't very helpful when it comes to figuring out 
 what is wrong. And I was even more surprised that of all things 
 `pragma` was called to the rescue. But isn't there a much 
 better, simpler way?

 [...]
Have you looked at https://github.com/atilaneves/concepts ?
Apr 15 2022
parent reply Marvin Hannott <i28muzw0n relay.firefox.com> writes:
On Friday, 15 April 2022 at 10:07:10 UTC, Atila Neves wrote:
 Have you looked at https://github.com/atilaneves/concepts ?
I have, but, ehm, to be honest, I don't quite understand how this is different from just not using constraints at all. Referring to your example: ```D import concepts.models: models; void checkFoo(T)() { T t = T.init; t.foo(); } enum isFoo(T) = is(typeof(checkFoo!T)); models!(Foo, isFoo) //as a UDA struct Foo { void foo() {} static assert(models!(Foo, isFoo)); //as a static assert } // can still be used as a template constraint: void useFoo(T)(auto ref T foo) if(isFoo!T) { } ``` I mean, where's the difference between `checkFoo(T)` failing or `useFoo(T)`? And to check that a type satisfies a constraint with `static assert` can be done with only `checkFoo(T)`, letting it return `true`. Being able to implement compile-time interfaces is really cool, though. And I apologize if I am getting this completely wrong. Sometimes it can be difficult to immediately see why something is useful.
Apr 15 2022
next sibling parent Marvin Hannott <i28muzw0n relay.firefox.com> writes:
Whops, now I got it. Sorry for being an idiot. I thought 
`isFoo()` also triggers a compiler error. Maybe you could clarify 
that in the documentation.
Apr 16 2022
prev sibling parent Atila Neves <atila.neves gmail.com> writes:
On Friday, 15 April 2022 at 16:39:48 UTC, Marvin Hannott wrote:
 On Friday, 15 April 2022 at 10:07:10 UTC, Atila Neves wrote:
 [...]
I have, but, ehm, to be honest, I don't quite understand how this is different from just not using constraints at all. Referring to your example: [...]
The difference is you get a compiler error message telling you *why* the concept wasn't satisfied.
Apr 18 2022