Feedback Thread: DIP 1041--Attributes for Higher-Order Functions--Community Review Round 1

Q. Schroll qs.il.paperinik at gmail.com
Mon Apr 12 14:44:38 UTC 2021


On Monday, 12 April 2021 at 11:05:14 UTC, Timon Gehr wrote:
> On 12.04.21 11:38, Mike Parker wrote:
>> https://github.com/dlang/DIPs/blob/11fcb0f79ce7ec209fb2a302d1371722d0c8ad82/DIPs/DIP1041.md ...
>
> Unfortunately, it is not written too well: The reader gets 
> flooded with details way before being told what the problem 
> actually is or how the proposal addresses it.

Doesn't the Abstract explain what the problem is and give a 
general idea how it is addressed?

> As far as I can tell, this is trying to introduce attribute 
> polymorphism without actually adding polymorphism, much like 
> `inout` attempted and ultimately failed to do. I am very 
> skeptical. It's taking a simple problem with a simple solution 
> and addressing it using an overengineered non-orthogonal mess 
> in the hopes of not having to add additional syntax.

You're mistaken. You can take a look at the Alternatives for 
seemingly simple solutions. There ain't any. Because D isn't an 
immutable-all-the-way-down language like e.g. Haskell, none of 
the easy solutions are sound. You always have the problem of 
assigning the parameter in the functional unless it's `const` or 
another flavor of non-mutable.
If you don't go the `const` route, you have to deal with 
assignments to the parameter before it's called. You have to 
disallow assignments that, looking at the types, are a 1-to-1 
assignment. IMO, going via `const` is far more intuitive.

In fact, "not having to add additional syntax" was never the 
motivation for the proposal. Not having to introduce attributes 
_specific_ to higher-order function was.

> To add insult to injury, the first example that's shown in the 
> DIP as motivation abuses an existing type system hole.

I disagree that it is a hole in the type system. When having 
`qual₁(R delegate(Ps) qual₂)` where `qual₁` and `qual₂` are type 
qualifiers (`const`, `immutable`, etc.) it is practically most 
useful if `qual₁` only applies to the function pointer and (the 
outermost layer of) the context pointer while `qual₂` refers to 
the property of the context itself.
Since the language gives no non-UB way to assign the function 
pointer and the context pointer separately, it is not unsound.
[Here](https://forum.dlang.org/post/gauloixsonnnlswhbiqe@forum.dlang.org) is the space for discussing this issue. Unfortunately, it's mostly us two that care.

> `toString` is `const`, `sink` is `const`, the only reference to 
> result accessible to `toString` is in the context of `sink`, 
> but somehow `result` is mutated anyway.

See the paragraph above.

> Unsoundness should be fixed, not embraced!

Yes, I guess no one disagrees on that one, but on the question of 
it being an instance of it.

> Finally, there's this concern: The DIP assumes that the only 
> reasonable way to manipulate delegates in higher-order 
> functions involves calling them, but this is not accurate.

It assumes that the the most common use-case of non-mutable 
delegate parameters is only calling them. Returning them is 
another, but a rarer one. The DIP details that in this case, the 
author of the `compose` function needs to remember not to make 
the parameters mutable.

> ```D
> auto compose(A,B,C)(C delegate(B) f, B delegate(A) g)pure{
>     return a=>f(g(a));
> }
> ```
>
> With the proposed changes, composing impure functions suddenly 
> becomes an impure operation as soon as you abstract it into a 
> higher-order function. This is pure nonsense. If you have a 
> `pure` expression and abstract it into a `pure` function, it 
> should not become less `pure` in the process!

You did it correctly in the sense of the DIP. `compose` takes `f` 
and `g` as mutable. None of the proposed changes apply to mutable 
delegate parameters. By the changes proposed by this DIP, 
`compose` is `pure`. However, all delegates you pass to it lose 
information of attributes because you _could_ assign `f` or `g` 
in `compose`, no problem.

But as you don't intend to mutate `f` or `g` in it, you could get 
the idea of making them `const` like this:
```D
C delegate(A) compose(A, B, C)(const C delegate(B) f, const B 
delegate(A) g) pure
{
     return a => f(g(a));
}
```
Then, by the proposed changes, only `pure` arguments lead to a 
`pure` call expression.
However, `compose` is a good example why this is not an issue: It 
is already a template. Why not go the full route and make the 
`delegate` part of the template type arguments like this:
```D
auto compose(F : C delegate(B), G : B delegate(A), A, B, C)(F f, 
G g) pure
{
     return delegate C(A arg) => f(g(arg));
}
```
Unfortunately (see 
[here](https://issues.dlang.org/show_bug.cgi?id=21823)), when 
calling `compose` with a `const` declared delegate value, D's 
IFTI infers `const` for the parameter type although the parameter 
is copied. I didn't realize that when writing the DIP. This is a 
problem, but it can be fixed (probably should). To be honest, it 
was my C++ background that tricked me into thinking copying stuff 
removes `const` in any case.

It's not without solution, however, the solution being a cast or 
explicit template instantiation, none of which is nice:
```D
const dgAtoB = ...;
const dgBtoC = ...;
auto dgAtoC1 = compose!(C delegate(B), B delegate(A))(dgBtoC, 
dgAtoB); // no IFTI, no cast
auto dgAtoC2 = compose(cast() dgBtoC, cast() dgAtoB); // IFTI 
with cast
```
The cast is `@safe`.

> E.g., consider this:
>
> ```D
> auto compose2(A,B,C)(C delegate(B) f, B delegate(A) g)pure{
>     return compose(f,g);
> }
> ```
>
> With the changes proposed in the DIP, this does not even 
> compile, breaking abstraction/perfect forwarding.

No, because with mutable parameters, nothing changes. The DIP is 
_very_ clear about that.


More information about the Digitalmars-d mailing list