Thread overview
Yes, constraints can have helpful error messages!
Apr 09
vit
Apr 09
vit
April 09

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).


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.

April 09

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:


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...

April 09

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:

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 .

April 09

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:

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.

April 15

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 ?

April 15

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:

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.

April 16

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.

April 18

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.