Jump to page: 1 2
Thread overview
Template constraints should introduce identifiers inside their scopes
Sep 21, 2022
HuskyNator
Sep 21, 2022
Paul Backus
Sep 21, 2022
Quirin Schroll
Sep 21, 2022
Paul Backus
Sep 21, 2022
HuskyNator
Sep 22, 2022
Timon Gehr
Sep 22, 2022
Paul Backus
Sep 22, 2022
jmh530
Sep 21, 2022
HuskyNator
Sep 22, 2022
Nick Treleaven
September 21, 2022

Consider these semantically identical functions:

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:

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.

September 21, 2022

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

September 21, 2022

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.

September 21, 2022

On Wednesday, 21 September 2022 at 11:23:57 UTC, HuskyNator wrote:

>

Consider these semantically identical functions:

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:

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 is not for you, I’d do:

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);
}());
>
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,

void foo(M : T[L], T, uint L)(M list) { }

does the trick. It works unless you’d have a || concatenated static if condition:

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:

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);
}
September 21, 2022

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:

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,

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:

auto fun(R)(R r)
    if (isInputRange!R && is(ElementType!R == T[], T))
September 21, 2022

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.

September 21, 2022

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:

auto fun(R)(R r)
    if (isInputRange!R && is(ElementType!R == T[], T))

Thank you, this is indeed what I was aiming for :)

September 22, 2022

On Wednesday, 21 September 2022 at 11:23:57 UTC, HuskyNator wrote:

>

Consider these semantically identical functions:

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

September 22, 2022
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.
September 22, 2022
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.
« First   ‹ Prev
1 2