www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Template constraints should introduce identifiers inside their scopes

reply HuskyNator <HuskyNator protonmail.ch> writes:
Consider these semantically identical functions:

```d
void foo(M)(M list) {
	static if (is(M : T[L], T, uint L)) {
		pragma(msg, "Length: " ~ L.to!string); // Length: 2
	}
}

void bar(M)(M list) if (is(M : T[L], T, uint L)) {
	pragma(msg, "Length: " ~ L.to!string); // undefined identifier 
'L'
}

void main(string[] args) {
	int[2] list = [1, 2];
	foo(list);
	bar(list);
}
```

Although semantically the same, `bar` will fail.
This can of course be solved by changing the template arguments, 
but could lead to issues in more complex scenarios:

```d
alias UnsignedType(T) = mixin(getUnsignedType!(T));

string getUnsignedType(T)() {
	static if (is(T == byte))
		return "ubyte";
	else static if (is(T == short))
		return "ushort";
	else static if (is(T == int))
		return "uint";
	else
		assert(0, "Type not supported: " ~ T.stringof);
}

auto foo(T)(T number) if (is(UnsignedType!T T2)) {
	alias T2 = UnsignedType!T; // still required
	return cast(T2) number;
}

void main(string[] args) {
	foo(1).writeln; // 1
	foo(-1).writeln; // 4294967295
}
```

Apart from these examples, depending on complexity, one might 
even think of a scenario in which a type returned inside the 
template condition should be matched against, in which case the 
body of the function would be required to contain a duplicate 
statement:
`is(aliasMixin!M M2: T[L], T, uint L);` (eg. when getUnsignedType 
returns a more complex type)

---

Given the above consideration, assuming this is not a bug (tested 
on both dmd & ldc), I believe identifiers introduced inside a 
template constraint should be visible inside the scope of the 
template.
Sep 21 2022
next sibling parent Petar Kirov [ZombineDev] <petar.p.kirov gmail.com> writes:
On Wednesday, 21 September 2022 at 11:23:57 UTC, HuskyNator wrote:
 [..]

 ---

 Given the above consideration, assuming this is not a bug 
 (tested on both dmd & ldc), I believe identifiers introduced 
 inside a template constraint should be visible inside the scope 
 of the template.
Agreed, it's been bothering me as well. (It is a language design question, not a implementation bug.)
Sep 21 2022
prev sibling next sibling parent Paul Backus <snarwin gmail.com> writes:
On Wednesday, 21 September 2022 at 11:23:57 UTC, HuskyNator wrote:
 Given the above consideration, assuming this is not a bug 
 (tested on both dmd & ldc), I believe identifiers introduced 
 inside a template constraint should be visible inside the scope 
 of the template.
+1, this has annoyed me in the past too.
Sep 21 2022
prev sibling next sibling parent reply Quirin Schroll <qs.il.paperinik gmail.com> writes:
On Wednesday, 21 September 2022 at 11:23:57 UTC, HuskyNator wrote:
 Consider these semantically identical functions:

 ```d
 void foo(M)(M list) {
 	static if (is(M : T[L], T, uint L)) {
 		pragma(msg, "Length: " ~ L.to!string); // Length: 2
 	}
 }

 void bar(M)(M list) if (is(M : T[L], T, uint L)) {
 	pragma(msg, "Length: " ~ L.to!string); // undefined identifier 
 'L'
 }

 void main(string[] args) {
 	int[2] list = [1, 2];
 	foo(list);
 	bar(list);
 }
 ```

 Although semantically the same,
They are not semantically the same. The first can be instantiated with any type and conditionally makes an output (it is empty otherwise), the other says it cannot be instantiated unless the arguments have specific properties. The first one is Design by Introspection (DbI), the second is a template with requirements.
 `bar` will fail.
 This can of course be solved by changing the template 
 arguments, but could lead to issues in more complex scenarios:

 ```d
 alias UnsignedType(T) = mixin(getUnsignedType!(T));

 string getUnsignedType(T)() {
 	static if (is(T == byte))
 		return "ubyte";
 	else static if (is(T == short))
 		return "ushort";
 	else static if (is(T == int))
 		return "uint";
 	else
 		assert(0, "Type not supported: " ~ T.stringof);
 }
 ```
There are better ways than a string `mixin` to do this. If for some reason [`std.traits.Unsigned`](https://dlang.org/phobos/std_traits.html#Unsigned) is not for you, I’d do: ```d alias UnsignedType(T) = typeof({ static if (is(T == byte)) return cast(ubyte)0; else static if (is(T == short)) return cast(ushort)0; else static if (is(T == int)) return cast(uint)0; else static assert(0, "Type not supported: " ~ T.stringof); }()); ```
 ```d
 auto foo(T)(T number) if (is(UnsignedType!T T2)) {
 	alias T2 = UnsignedType!T; // still required
 	return cast(T2) number;
 }

 void main(string[] args) {
 	foo(1).writeln; // 1
 	foo(-1).writeln; // 4294967295
 }
 ```

 […]

 Given the above consideration, assuming this is not a bug 
 (tested on both dmd & ldc), I believe identifiers introduced 
 inside a template constraint should be visible inside the scope 
 of the template.
Your suggestion is very much independent of the DbI vs constraints. You want identifiers defined in a constraint to be visible in the body of the template. This is possible in simple cases like yours, but what if the `is` check is nested or negated? In your case, ```d void foo(M : T[L], T, uint L)(M list) { } ``` does the trick. It works unless you’d have a `||` concatenated `static if` condition: ```d void foo(M)(M list) { static if (is(M : T[n], T, size_t n) || is(M : T[], T)) { // on either way the condition is true, `T` is defined. pragma(msg, T); // Coming from second condition, `n` is undefined pragma(msg, is(typeof(n))); } } void main() { int[3] xs = [1, 2, 3]; foo(xs); foo([1, 2]); } ``` In that case, you can still come very close with this: ```d void boo(M : T[n], T, size_t n)(M list) => boo_impl!T(list); void boo(M : T[], T)(M list) if (!is(M : T[n], T, size_t n)) => boo_impl!T(list); private void boo_impl(T, M)(M list) { pragma(msg, T); } ```
Sep 21 2022
next sibling parent reply Paul Backus <snarwin gmail.com> writes:
On Wednesday, 21 September 2022 at 12:18:19 UTC, Quirin Schroll 
wrote:
 On Wednesday, 21 September 2022 at 11:23:57 UTC, HuskyNator 
 wrote:
 Consider these semantically identical functions:

 ```d
 void foo(M)(M list) {
 	static if (is(M : T[L], T, uint L)) {
 		pragma(msg, "Length: " ~ L.to!string); // Length: 2
 	}
 }

 void bar(M)(M list) if (is(M : T[L], T, uint L)) {
 	pragma(msg, "Length: " ~ L.to!string); // undefined 
 identifier 'L'
 }
 ```
 [...]

 Although semantically the same,
They are not semantically the same. The first can be instantiated with any type and conditionally makes an output (it is empty otherwise), the other says it cannot be instantiated unless the arguments have specific properties.
If you want the semantics to match exactly you can replace the `static if` with a `static assert` (or add `else static assert(0);`).
 Your suggestion is very much independent of the DbI vs 
 constraints. You want identifiers defined in a constraint to be 
 visible in the body of the template. This is possible in simple 
 cases like yours, but what if the `is` check is nested or 
 negated?
`static if` already handles these cases. I don't know exactly what the rules are (needs better documentation), but presumably they would work the same way for constraints.
 In your case,
 ```d
 void foo(M : T[L], T, uint L)(M list) { }
 ```
 does the trick.
As I'm sure you're aware, there are cases where the desired constraint cannot be expressed using template specializations. For example: ```d auto fun(R)(R r) if (isInputRange!R && is(ElementType!R == T[], T)) ```
Sep 21 2022
next sibling parent HuskyNator <HuskyNator protonmail.ch> writes:
On Wednesday, 21 September 2022 at 12:32:38 UTC, Paul Backus 
wrote:
 `static if` already handles these cases. I don't know exactly 
 what the rules are (needs better documentation), but presumably 
 they would work the same way for constraints.
Apologies, I only read your comment now.
 As I'm sure you're aware, there are cases where the desired 
 constraint cannot be expressed using template specializations. 
 For example:

 ```d
 auto fun(R)(R r)
     if (isInputRange!R && is(ElementType!R == T[], T))
 ```
Thank you, this is indeed what I was aiming for :)
Sep 21 2022
prev sibling parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 21.09.22 14:32, Paul Backus wrote:
 They are not semantically the same. The first can be instantiated with 
 any type and conditionally makes an output (it is empty otherwise), 
 the other says it cannot be instantiated unless the arguments have 
 specific properties.
If you want the semantics to match exactly you can replace the `static if` with a `static assert` (or add `else static assert(0);`).
I think this is not true. ```d module a; template foo(T){ static assert(is(T==int)); } template bar(T)if(is(T==int)){} ``` ```d module b; template foo(T)if(is(T==float)){} template bar(T)if(is(T==float)){} ``` ```d module c; import a,b; alias foo1=foo!int; // ok alias foo2=foo!float; // error alias bar1=bar!int; // ok alias bar2=bar!float; // ok ``` `static assert` can give you a custom error message, but template constraints have much better overloading behavior. The two features are not comparable. Ideally template constraints would be improved so they are a strictly better choice than static assert whenever they are appropriate.
Sep 22 2022
parent reply Paul Backus <snarwin gmail.com> writes:
On Thursday, 22 September 2022 at 16:52:54 UTC, Timon Gehr wrote:
 On 21.09.22 14:32, Paul Backus wrote:
 They are not semantically the same. The first can be 
 instantiated with any type and conditionally makes an output 
 (it is empty otherwise), the other says it cannot be 
 instantiated unless the arguments have specific properties.
If you want the semantics to match exactly you can replace the `static if` with a `static assert` (or add `else static assert(0);`).
I think this is not true.
[...]
 `static assert` can give you a custom error message, but 
 template constraints have much better overloading behavior. The 
 two features are not comparable.
In general, yes. In the context of the specific example in this thread, they are the same. In any case, the difference has no bearing on the main topic of discussion here, which is name visibility.
Sep 22 2022
parent jmh530 <john.michael.hall gmail.com> writes:
On Thursday, 22 September 2022 at 18:00:53 UTC, Paul Backus wrote:
 On Thursday, 22 September 2022 at 16:52:54 UTC, Timon Gehr 
 wrote:
 On 21.09.22 14:32, Paul Backus wrote:
 They are not semantically the same. The first can be 
 instantiated with any type and conditionally makes an output 
 (it is empty otherwise), the other says it cannot be 
 instantiated unless the arguments have specific properties.
If you want the semantics to match exactly you can replace the `static if` with a `static assert` (or add `else static assert(0);`).
I think this is not true.
[...]
 `static assert` can give you a custom error message, but 
 template constraints have much better overloading behavior. 
 The two features are not comparable.
In general, yes. In the context of the specific example in this thread, they are the same. In any case, the difference has no bearing on the main topic of discussion here, which is name visibility.
I think the underlying principle is that the user generally expects them to behave similarly. Enhancing template constraint name visibility is probably a good thing on that principle. Figuring out some way for Timon's example to compile might also be good (perhaps overload sets would need an ordering to put templated functions with template constraints above those without template constraints?).
Sep 22 2022
prev sibling parent HuskyNator <HuskyNator protonmail.ch> writes:
On Wednesday, 21 September 2022 at 12:18:19 UTC, Quirin Schroll 
wrote:
 They are not semantically the same. The first can be 
 instantiated with any type and conditionally makes an output 
 (it is empty otherwise), the other says it cannot be 
 instantiated unless the arguments have specific properties.
I am aware of the differences when the if statement fails, though the intent remains the same.
 There are better ways than a string `mixin` to do this.
I am aware, this is merely an example.
 You want identifiers defined in a constraint to be visible in 
 the body of the template. This is possible in simple cases like 
 yours, but what if the `is` check is nested or negated?
I would presume it to work identically to its behavior inside a static if statement.
Sep 21 2022
prev sibling parent Nick Treleaven <nick geany.org> writes:
On Wednesday, 21 September 2022 at 11:23:57 UTC, HuskyNator wrote:
 Consider these semantically identical functions:

 ```d
 void bar(M)(M list) if (is(M : T[L], T, uint L)) {
 	pragma(msg, "Length: " ~ L.to!string); // undefined identifier 
 'L'
 }
 ```
This is in bugzilla: https://issues.dlang.org/show_bug.cgi?id=6269
Sep 22 2022