Yes, constraints can have helpful error messages!

Marvin Hannott i28muzw0n at relay.firefox.com
Sat Apr 9 01:03:32 UTC 2022


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.


More information about the Digitalmars-d mailing list