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

Q. Schroll qs.il.paperinik at gmail.com
Mon Apr 12 21:59:50 UTC 2021


On Monday, 12 April 2021 at 17:21:47 UTC, Timon Gehr wrote:
> On 12.04.21 16:44, Q. Schroll wrote:
>>
>> On Monday, 12 April 2021 at 11:05:14 UTC, Timon Gehr wrote:
>>> 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?
>
> It does not. It's generic fluff. It's only marginally more 
> explicit than: "there are problems and to address them we 
> should change the language".

I had a more detailed Abstract in previous drafts, but if you 
think I watered it down too much, I can add more details.

>>> 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.
>
> I know there are, and I literally state how to do it in the 
> quoted excerpt.

If by "quoted excerpt" you mean "As far as I can tell, this", I 
read it, but to be honest, I didn't really understand what 
attribute polymorphism really means. Googling "polymorphism" the 
closet I come to would be that a `@safe` delegate can be used in 
place of a `@system` delegate. This is already the case, I can't 
see how anything would "introduce" it.

> > You always have the problem of assigning the parameter in the 
> > functional unless it's `const` or another flavor of 
> > non-mutable.
>
> Assignments are not the problem, it's the inconsistent 
> interpretation of types using incompatible, special-cased 
> meanings.

Maybe I'm not creative enough for a proper solution, but I should 
be, since the problem is "easy".

>> 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.
>
> It's a bad, non-orthogonal solution building on a compiler bug.
>
>> 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.
>
> It adds higher-order specific rules to existing attributes 
> without a good reason, which is a lot worse.

I guess removing higher-order functions as a road-bump when it 
comes to attributes is a good reason. It's adding higher-order 
specific rules vs. adding another higher-order specific something.

>>> 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.
>
> You are wrong, and I am not sure how to make that point to you. 
> (When I tried last time, you just claimed that some other 
> well-documented intentionally-designed feature, like attribute 
> transitivity, is actually a bug.)
>
>> 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.
>
> That allows building a gadget to completely bypass transitivity 
> of qualifiers, including `immutable` and `shared`.

I had a look at [issue 
1983](https://issues.dlang.org/show_bug.cgi?id=1983) again where 
(I guess) the source of disagreement is how delegates should be 
viewed theoretically. If I understand you correctly, you say 
delegates *cannot possibly* be defined differently than having 
their contexts be literally part of them. I tried to explore 
definitions in which the context is *associated with* but not 
*literally part of* the delegate.

My goal was to find a theoretic foundation that is practically 
useful and doesn't defy expectations. For if a closure mutates a 
captured variable, one can't assign that closure to a `const` 
variable, notably, you cannot bind it to a functional's `const` 
parameter, I guess does defy expectations greatly.

Trying to draw a comparison with it, I found out today that 
slice's `capacity` is `pure` and also that it's a bug admitted in 
`object.d` ("This is a lie. [It] is neither `nothrow` nor `pure`, 
but this lie is necessary for now to prevent breaking code.")

> It's completely unsound, e.g., it allows creating race 
> conditions in `@safe` code.

Maybe I'm just too uncreative or too dumb to come up with one 
myself. I once ran into something like that trying out 
`std.parallelism.parallel` and how much it could gain me. It's 
years ago and I cannot remember a lot. I figured it wasn't 
applicable in my case. The

I'd really appreciate an example from your side.

> You can't say qualifiers are transitive except in this one 
> case, that translates to them not being transitive at all. A 
> lot of D's type system design is built on qualifiers being 
> transitive.
>
>> 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.
>
> The obfuscated DIP is unfortunately not helping with that.
>
> In any case, this is now the place to discuss this issue as you 
> have chosen to use this bug as a basis for evolving the 
> language.
>
>>> `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.
>
> I guess you mean the other way around.

Yes. I meant to say: "needs to remember 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.
>
> Fair, but that's a technicality tangential to my point, and as 
> you may have been able to tell, I have certain reservations 
> about reusing `const` in this fashion.
>
>> 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 that's a terrible reason to not be able to annotate them 
> `const`. `const` means "this won't change", it does not mean 
> "if you compose this, it won't be recognized as `pure`" and 
> there is no clear way to get from one to the other. It's a 
> textbook example of a messy non-orthogonal design that does not 
> make any sense upon closer inspection.

Maybe use `in` (i.e. `const scope`) then? It clearly signifies: 
This is to read information from, not to assign to it, assign it 
to a global, not even to return it in any fashion.

>> But as you don't intend to mutate `f` or `g` in it, you could 
>> get the idea of making them `const` like this:
>
> Yes, let's assume that was my intention.
>
>> ```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.
>
> Which was my point. This is indefensible.

It suffices to write this and one `@safe` unit test: The compile 
error will tell you there's a problem. I can add to the Error 
Messages section that in this case, the error message should hint 
that the `const` might be used improperly.

>> 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));
>> }
>> ```
>
> The fact that there is some ugly workaround for my illustrative 
> example that also defeats the point of your DIP does not 
> eliminate the problem with the DIP.

This isn't an ugly workaround, but merely an attempt to stick to 
the example. Simply omitting the specialization syntax isn't 
possible. `return a => f(g(a));` doesn't compile, you need the 
`(A a)` part and for that, you need `A`. You can get it 
alternatively with `Parameters!f`; but `auto compose(F, G)(F f, G 
g)` with `return a => f(g(a));` doesn't work.

> Your reinterpretation of what delegate qualifiers mean would 
> need a DIP in its own right and it would hopefully be rejected.

I'm not sure it's a *re-*interpretation. As factually the 
compiler defines the language at places, you're probably right 
about the DIP part.


More information about the Digitalmars-d mailing list