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.