January 17, 2020
On Fri, Jan 17, 2020 at 11:01:29AM -0500, Steven Schveighoffer via Digitalmars-d wrote: [...]
> Yes, concepts would be useful, but also would be limited for how D does duck typing. D's constraints are very much like concepts, but without having to declare everything up front for use in the function.

IMO, not declaring everything up front for use is an anti-pattern. Basically, by accepting a template parameter T and performing operations on it, you're assuming the T supports said operations. If T doesn't support such operations, you shouldn't be receiving it in the first place. By not specifying what operations you expect T to support in your sig constraints, you're basically declaring that you accept *any* T, including one that supports no operations, yet you proceed to operate on it, which ought to be an error (not just a silent failure to instantiate).

IOW, you're actually making assumptions on what T supports, yet you never declared such assumptions.  Conversely, if you declare no assumptions on T, then neither should you be allowed to operate on it. A parameter T received without any assumptions should be treated as an opaque object that allows *no* operations.


[...]
> However, the real problem is things like typos that cause the function not to compile for reasons other than the intended failures.
> 
> For example:
> 
> T foo(T)(T x)
> {
>    return X + 5; // typo on parameter name
> }
> 
> This will NEVER compile, because X will never exist. The intention was for it to compile with types that support addition to integers (i.e. it's EXPECTED to fail for strings), but instead it fails for all types.

See, this is why this kind of code is bad.  You basically wish to accept all T that support +, but by not declaring it as such, the compiler has no way to know what you intended. So it assumes that you somehow expect X to spring into existence given some magic value of T, and when that never happens, the compiler always skips over foo.  Had you declared that you expect T to support +, then the compiler would know to instantiate foo when T==int, then it would have caught the typo as a compile error.  As a bonus, your code would even accept custom types that support +, without any further effort on your part.

Another side effect of not declaring assumptions up-front is that the compiler cannot produce better error messages. You're stuck with error gagging that hides typos, because the compiler simply doesn't have enough information to know what was intended. What if you intended for foo never to compile?  Without declaring any assumptions the compiler couldn't know better.


> But it's hard for the compiler to deduce intention from such typos. It would be great if the compiler could figure out this type of error, but I don't think it can.

It can if there was a requirement that template arguments are not allowed to be operated on unless their ability to support the operation is either declared in a sig constraint, or else tested with an appropriate static if condition.


> The only "fix" really is to judiciously unittest with the calls you expect to work. And that generally doesn't happen (we are a lazy bunch!).

Exactly, that's why you have to force users to declare what assumptions they're making on the template parameters. It's more "convenient" to just take the easy way and allow everything without checking, but it only leads to pain and more pain down the road.


> Perhaps, an "expected" clause or something to ensure it compiles for the expected types like: expected(T == int), which wouldn't unittest anything, just make sure it can compile for something that is expected.

Just do this instead:

	unittest {
		// will generate compile error if instantiation fails:
		alias A = myTemplate!int;
	}


T

-- 
Кто везде - тот нигде.
January 18, 2020
On Friday, 17 January 2020 at 16:01:29 UTC, Steven Schveighoffer wrote:
> However, the real problem is things like typos that cause the function not to compile for reasons other than the intended failures.
>
> For example:
>
> T foo(T)(T x)
> {
>    return X + 5; // typo on parameter name
> }
>
> This will NEVER compile, because X will never exist. The intention was for it to compile with types that support addition to integers (i.e. it's EXPECTED to fail for strings), but instead it fails for all types. The result is that your code selects some other path when you don't want it to. Sometimes this just means it works but is slower, which is even harder to figure out.
>
> But it's hard for the compiler to deduce intention from such typos. It would be great if the compiler could figure out this type of error, but I don't think it can.
>

Yes it can!

D already has support for exactly this kind of thing, with std.traits and `if()` constraints. If you have a metafunction that tries to see if "an alias is usable with string", and it checks that with __traits(canInstantiateCall, foo, string.init) or whatever, then you *should* get a syntax error even in the "return x + 5" case. If you want to avoid that, we don't need language changes, you just need to use the *already existing* mechanism of "if (isNumeric!T)".
January 21, 2020
On 1/18/20 10:21 AM, FeepingCreature wrote:
> On Friday, 17 January 2020 at 16:01:29 UTC, Steven Schveighoffer wrote:
>> However, the real problem is things like typos that cause the function not to compile for reasons other than the intended failures.
>>
>> For example:
>>
>> T foo(T)(T x)
>> {
>>    return X + 5; // typo on parameter name
>> }
>>
>> This will NEVER compile, because X will never exist. The intention was for it to compile with types that support addition to integers (i.e. it's EXPECTED to fail for strings), but instead it fails for all types. The result is that your code selects some other path when you don't want it to. Sometimes this just means it works but is slower, which is even harder to figure out.
>>
>> But it's hard for the compiler to deduce intention from such typos. It would be great if the compiler could figure out this type of error, but I don't think it can.
>>
> 
> Yes it can!
> 
> D already has support for exactly this kind of thing, with std.traits and `if()` constraints. If you have a metafunction that tries to see if "an alias is usable with string", and it checks that with __traits(canInstantiateCall, foo, string.init) or whatever, then you *should* get a syntax error even in the "return x + 5" case. If you want to avoid that, we don't need language changes, you just need to use the *already existing* mechanism of "if (isNumeric!T)".

I don't see how that helps. isNumeric!T is ignoring the problem that X is the wrong symbol name.

In the usage of foo, if I say if __traits(compiles, foo(5)) or __traits(canInstantiate, foo, int), how does it know the error during instantiation was not intended?

In other words, I need to know, was foo *intended* to compile with T, not *does* it compile with T.

-Steve

January 21, 2020
On 1/18/20 10:21 AM, FeepingCreature wrote:
> Yes it can!
> 
> D already has support for exactly this kind of thing, with std.traits and `if()` constraints. If you have a metafunction that tries to see if "an alias is usable with string", and it checks that with __traits(canInstantiateCall, foo, string.init) or whatever, then you *should* get a syntax error even in the "return x + 5" case. If you want to avoid that, we don't need language changes, you just need to use the *already existing* mechanism of "if (isNumeric!T)".

Wait, I think I get it.

You want something that says if the constraints allow the instantiation to proceed, then instantiate, and display the errors as generated. If not, then return false.

So to further explore the stupid example I have:

T foo(T)(T x) if(isNumeric!T)
{
   return X + 5;
}

Then __traits(canInstantiate, foo, "") returns false because the constraints are false, and __traits(canInstantiate, foo, 5) produces an error because the implementation is bad.

This is definitely possible.

But, it could get really annoying. For instance, you may have to repeat the actual implementation in the constraints to get this to match up correctly. e.g.:

T foo(T)(T x) if(__traits(compiles, T.init + 5))
{
   return X + 5;
}

I mean, isNumeric might not be what you want exactly, for example, you may want foo!BigInt to compile.

But, a constraint-free template might be exactly what you wanted to do. Many range types don't care what their element type does, they are just focused on being a container for them. So there's no constraints based on the type.

The idea has potential.

-Steve
January 22, 2020
On Wednesday, 15 January 2020 at 18:13:08 UTC, H. S. Teoh wrote:
> On Wed, Jan 15, 2020 at 03:13:08PM +0000, FeepingCreature via Digitalmars-d wrote:
>> Right now it's impossible to check "is there a valid (template) specialization for this function call" without also saying "and by the way, if there's a syntax error in the function, suppress the error and return false."
>> 
>> This makes many Phobos and library functions very annoying to use.
>
> Yeah, the error-gagging method of speculative template instantiation has been costing me lots of wasted time, too,

I've somehow only just discovered that `-verrors=spec` is a thing that one can pass to dmd. Does that help?

January 22, 2020
On Wed, Jan 22, 2020 at 03:53:15PM +0000, Atila Neves via Digitalmars-d wrote:
> On Wednesday, 15 January 2020 at 18:13:08 UTC, H. S. Teoh wrote:
> > On Wed, Jan 15, 2020 at 03:13:08PM +0000, FeepingCreature via Digitalmars-d wrote:
> > > Right now it's impossible to check "is there a valid (template) specialization for this function call" without also saying "and by the way, if there's a syntax error in the function, suppress the error and return false."
> > > 
> > > This makes many Phobos and library functions very annoying to use.
> > 
> > Yeah, the error-gagging method of speculative template instantiation has been costing me lots of wasted time, too,
> 
> I've somehow only just discovered that `-verrors=spec` is a thing that one can pass to dmd. Does that help?

Only barely, because it generally produces pages upon pages of irrelevant errors, and I have to save the output to a file and search for keywords just to find approximately where the error is. And *then* I have to decipher which of the errors are actually relevant.

IOW it's a pretty awful user experience.


T

-- 
I don't trust computers, I've spent too long programming to think that they can get anything right. -- James Miller
January 22, 2020
On Tuesday, 21 January 2020 at 22:32:45 UTC, Steven Schveighoffer wrote:
> On 1/18/20 10:21 AM, FeepingCreature wrote:
>> Yes it can!
>> 
>> D already has support for exactly this kind of thing, with std.traits and `if()` constraints. If you have a metafunction that tries to see if "an alias is usable with string", and it checks that with __traits(canInstantiateCall, foo, string.init) or whatever, then you *should* get a syntax error even in the "return x + 5" case. If you want to avoid that, we don't need language changes, you just need to use the *already existing* mechanism of "if (isNumeric!T)".
>
> Wait, I think I get it.
>
> You want something that says if the constraints allow the instantiation to proceed, then instantiate, and display the errors as generated. If not, then return false.
>
> [...]
>
> -Steve

Yes and there's an API for that
https://github.com/dlang/dmd/blob/ab2ea3880a144957811d3a8f7c63dfab0e351202/src/dmd/dtemplate.d#L762
1 2
Next ›   Last »