www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Discussion Thread: DIP 1041--Attributes for Higher-Order

reply Mike Parker <aldacron gmail.com> writes:


This is the discussion thread for the first round of Community 
Review of DIP 1041, "Attributes for Higher-Order Functions":

https://github.com/dlang/DIPs/blob/11fcb0f79ce7ec209fb2a302d1371722d0c8ad82/DIPs/DIP1041.md

The review period will **end at 11:59 PM ET on April 26**, or 
when I make a post declaring it complete. Discussion in this 
thread may continue beyond that point.

Here in the discussion thread, you are free to discuss anything 
and everything related to the DIP. Express your support or 
opposition, debate alternatives, argue the merits, etc.

However, if you have any specific feedback on how to improve the 
proposal itself, then please post it in the Feedback Thread. The 
Feedback Thread will be the source for the review summary that I 
will write at the end of this review round. I will post a link to 
that thread immediately following this post. Just be sure to read 
and understand the Reviewer Guidelines before posting there:

https://github.com/dlang/DIPs/blob/master/docs/guidelines-reviewers.md

And my blog post on the difference between the Discussion and 
Feedback threads:

https://dlang.org/blog/2020/01/26/dip-reviews-discussion-vs-feedback/

Please stay on topic here. I will delete posts that are 
completely off-topic.
Apr 12
next sibling parent Mike Parker <aldacron gmail.com> writes:
On Monday, 12 April 2021 at 09:36:29 UTC, Mike Parker wrote:

 However, if you have any specific feedback on how to improve 
 the proposal itself, then please post it in the Feedback 
 Thread. The Feedback Thread will be the source for the review 
 summary that I will write at the end of this review round. I 
 will post a link to that thread immediately following this post.
The **Feedback Thread** is located here: https://forum.dlang.org/post/zureulajwutgsynerwzc forum.dlang.org
Apr 12
prev sibling next sibling parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 12.04.21 16:44, Q. Schroll wrote:
 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/11fcb0f79ce7ec209fb2a302d1371722d0c8 d82/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?
 ...
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".
 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.
 Because D isn't an immutable-all-the-way-down language like e.g. Haskell,
Haskell has plenty of support for mutable data.
 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. Assignments are not the problem, it's the inconsistent interpretation of types using incompatible, special-cased meanings.
 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.
 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`. It's completely unsound, e.g., it allows creating race conditions in ` safe` code. 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.
 ```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.
 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.
 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.
 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). (Your reinterpretation of what delegate qualifiers mean would need a DIP in its own right and it would hopefully be rejected.)
Apr 12
parent reply Q. Schroll <qs.il.paperinik gmail.com> writes:
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.
Apr 12
next sibling parent ag0aep6g <anonymous example.com> writes:
On Monday, 12 April 2021 at 21:59:50 UTC, Q. Schroll wrote:
 On Monday, 12 April 2021 at 17:21:47 UTC, Timon Gehr wrote:
[...]
 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.
I don't think you can cite DMD on the matter. It contradicts itself when it comes to qualified delegates. You could point at the following code as evidence that DMD allows a const delegate with a mutable context: ```d struct S { int field; void method() { field = 42; } } void main() { S s; const void delegate() d = &s.method; d(); } ``` But add one line, and DMD will say the opposite: ```d alias NotEvenUsed = const void delegate() const; /* the only change; rest of the code is identical */ struct S { int field; void method() { field = 42; } } void main() { S s; const void delegate() d = &s.method; /* this is now an error */ d(); } ``` https://issues.dlang.org/show_bug.cgi?id=16058 This is a case where we can't just say "let's write down in the spec what DMD is doing already", because what DMD is doing makes no sense.
Apr 12
prev sibling parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 4/12/21 11:59 PM, Q. Schroll wrote:
 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. ...
That's subtyping, not polymorphism. Polymorphism is when a term depends on a type: https://en.wikipedia.org/wiki/Lambda_cube https://en.wikipedia.org/wiki/Parametric_polymorphism In this case, a term would depend on an attribute.
 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". ...
I am not saying it is easy. I am saying humanity has a rich cultural history and this happens to be a solved problem.
 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. ...
It does not have to be higher-order specific at all. Might as well fix `inout` at the same time.
 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. ...
I get that, but it is impossible because you can use delegate contexts as arbitrary storage: int x; auto dg=(int* update){ if(update) x=*update; return x; }; If you can have "associated" delegate contexts, you can have "associated" struct fields. But we don't have those. Assuming the concept has merit, it would still be bad design to abitrarily tie it to delegate contexts.
 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.
 ...
You can store it in a `const` variable, but you can't call it, much like you can't call a mutable method on a `const` object.
 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.
std.parallelism.parallel cannot be annotated safe or trusted.
 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.
 ...
E.g., this: import std.concurrency; void main(){ int x; // this conversion should not go through shared(int delegate(int*)) dg=(int* update){ if(update) x=*update; return x; }; spawn((typeof(dg) dg){ int y=3; dg(&y); // this should not be callable },dg); import std.stdio; writeln(x); }
 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. ...
It's not what you need. Reassigning is fine, it just has to be something with compatible attributes.
 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. ...
The error message would have to say it was designed 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,
ugly, check, workaround, check.
 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.
 ...
None of this matters. You "solved" the problem by removing the need for attribute polymorphism using automated code duplication.
 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.
It defies attribute transitivity, which is a stated design goal.
 As factually the compiler 
 defines the language at places, you're probably right about the DIP part.
Unfortunately, the compiler has bugs. One can't take its behavior as holy gospel that just needs to be interpreted correctly.
Apr 12
parent Q. Schroll <qs.il.paperinik gmail.com> writes:
On Tuesday, 13 April 2021 at 00:14:38 UTC, Timon Gehr wrote:
 On 4/12/21 11:59 PM, Q. Schroll wrote:
 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. ...
That's subtyping, not polymorphism. Polymorphism is when a term depends on a type: https://en.wikipedia.org/wiki/Lambda_cube https://en.wikipedia.org/wiki/Parametric_polymorphism
generics). At least, it's very similar. I'd be glad if generics were introduced to D. The only language I know of that has templates and generics is C++/CLI. The main practical advantage of generics over templates is that if your generic construct compiles, it'll be type-correct for any arguments a user would supply. Unlike templates, generics never fail on instantiation.
 In this case, a term would depend on an attribute.
Attribute polymorphism as I understand you is that you have variables for attributes so that for every attribute, there's a separate "type" of variable, like ` safe-ty a` such that `a` can be applied as an attribute to stuff that can carry one. Giving D any kind of polymorphism (half-baked `inout` aside) would be a major language change.
 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. ...
It does not have to be higher-order specific at all. Might as well fix `inout` at the same time.
As I understand you, `inout` is a fixed-name qualifier variable and your sense is that it's broken in D, because one can only have one of them. I guess that was a design choice. I cannot estimate the extent of how conscious that decision was. Probably no one had a use-case in mind that really required more than one qualifier variable so `inout` sufficed. Could also be that you mean the fact that one cannot have `Array!(inout int) f(Array!(inout int))`. Unfortunately, `inout` is weird in many ways.
 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. ...
I get that, but it is impossible because you can use delegate contexts as arbitrary storage: ```D int x; auto dg = (int* update) { if (update) x = *update; return x; }; ``` If you can have "associated" delegate contexts, you can have "associated" struct fields. But we don't have those.
Associated is a concept in ones head, not a definition by the language. It is what Jonathan Davis described in [this article](http://jmdavisprog.com/articles/why-const-sucks.html) as "the object's state could actually live outside of the object itself and be freely mutated even though the function is `const`". Qualifiers and attributes have an idea behind them, but when it comes to practical use, there's often a compromise. Set `debug` blocks aside, set slices' `capacity` being marked `pure` aside, the fact that allocating objects (including arrays) is `pure` is a deliberate and explicitly stated decision that could easily be argued against: If `f(n)` is a `pure` operation, (where `n` is a mere `int`, just to be clear) how could it happen that if I do it twice, it succeeds once and fails the second time? But allocating a large amount of memory can amount to exactly that. There are deliberate boundaries to any concept. I have no idea of the state of `__metadata`, but it would be another one. `__metadata` would be to `const` what ` trusted` is to ` safe`.
 Assuming the concept has merit, it would still be bad design to 
 abitrarily tie it to delegate contexts.

 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.
 ...
You can store it in a `const` variable, but you can't call it, much like you can't call a mutable method on a `const` object.
Completely surprising behavior and a breaking change.
 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.
std.parallelism.parallel cannot be annotated safe or trusted.
 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.
 ...
E.g., this: ```D import std.concurrency; void main(){ int x; // this conversion should not go through shared(int delegate(int*)) dg = (int* update) { if (update) x = *update; return x; }; spawn((typeof(dg) dg) { int y = 3; dg(&y); // this should not be callable }, dg); import std.stdio; writeln(x); } ```
What if `shared` on the outer of `dg` wouldn't matter, but it being missing on the context annotation does? Then `dg(&y)` wouldn't be the problem, but `spawn` just cannot take delegates with mutable non-`shared` contexts. I get that it is theoretically sound to extend `shared` of `shared(int delegate(int*))` to make it identical to `shared(int delegate(int*) shared)`. The same way it was theoretically sound in C++11 to define that `constexpr` member functions are also `const` member functions. (They removed that rule in C++14 which was a breaking change.) To stick to D, it sounds as unpractical as only allowing `const` on variables of types that only have `const` member functions. As I explained above, qualifiers and attributes have practical limits and I do think this is one. It's not like we cannot express a shared context on a delegate type.
 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. ...
It's not what you need. Reassigning is fine, it just has to be something with compatible attributes.
 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. ...
The error message would have to say it was designed 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,
ugly, check, workaround, check.
Ugly is opinion. On that basis, almost every template is a workaround for lack of this or that kind of polymorphism. It's only ugly on the first glace. Apart form the `delegate C` part, it contains the bare minimum to express itself. One could leave out the `: Y delegate(X)` parts, too. I like to have expectations spelled out in code. That's a personal preference, I guess.
 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.
 ...
None of this matters. You "solved" the problem by removing the need for attribute polymorphism using automated code duplication.
Of course. Unless you have attribute polymorphism you need templates. If you don't use them, in the current state, you can have all attributes you want on your input delegates, you have to specify *one* set of attributes on the return type. So you need attribute polymorphism and type polymorphism, because without them together, you still need templates. Then you can just use templates. Practically speaking, if your types are concrete so you don't need template type parameters, it's very likely that the attributes are concrete, too, removing the need for attribute polymorphism. The requirement of being compositional could be unwarranted.
 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.
It defies attribute transitivity, which is a stated design goal.
Depends on how you interpret transitivity. Being practical is also a design goal. In contrast to e.g. C++, D doesn't "trust the programmer". I'm not advocating a "trust the programmer" solution (at least I'm convinced I'm not). In my estimation, I advocate a practical solution that minimizes language change.
 As factually the compiler defines the language at places, 
 you're probably right about the DIP part.
Unfortunately, the compiler has bugs. One can't take its behavior as holy gospel that just needs to be interpreted correctly.
I don't. And I was wrong about the DIP part. It's the other way around because it's a breaking change.
Apr 25
prev sibling next sibling parent reply Imperatorn <johan_forsberg_86 hotmail.com> writes:
On Monday, 12 April 2021 at 09:36:29 UTC, Mike Parker wrote:


 This is the discussion thread for the first round of Community 
 Review of DIP 1041, "Attributes for Higher-Order Functions":

 [...]
I think the DIP has a noble goal, but is too complex. Try to keep it minimal if possible
Apr 12
parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 12.04.21 20:09, Imperatorn wrote:
 On Monday, 12 April 2021 at 09:36:29 UTC, Mike Parker wrote:


 This is the discussion thread for the first round of Community Review 
 of DIP 1041, "Attributes for Higher-Order Functions":

 [...]
I think the DIP has a noble goal, but is too complex. Try to keep it minimal if possible
Unfortunately that's the nature of the chosen approach: non-compositional type system design is a game of whack-a-mole resulting in progressively more complexity while often maintaining a lack of soundness.
Apr 12
next sibling parent Walter Bright <newshound2 digitalmars.com> writes:
On 4/12/2021 11:14 AM, Timon Gehr wrote:
 non-compositional type 
 system design is a game of whack-a-mole resulting in progressively more 
 complexity while often maintaining a lack of soundness.
I could never have expressed the notion as clearly as that.
Apr 13
prev sibling parent reply jmh530 <john.michael.hall gmail.com> writes:
On Monday, 12 April 2021 at 18:14:30 UTC, Timon Gehr wrote:
 [snip]

 Unfortunately that's the nature of the chosen approach: 
 non-compositional type system design is a game of whack-a-mole 
 resulting in progressively more complexity while often 
 maintaining a lack of soundness.
Sorry, when you say non-compositional type system, what precisely do you mean?
Apr 14
next sibling parent Imperatorn <johan_forsberg_86 hotmail.com> writes:
On Wednesday, 14 April 2021 at 11:43:22 UTC, jmh530 wrote:
 On Monday, 12 April 2021 at 18:14:30 UTC, Timon Gehr wrote:
 [snip]

 Unfortunately that's the nature of the chosen approach: 
 non-compositional type system design is a game of whack-a-mole 
 resulting in progressively more complexity while often 
 maintaining a lack of soundness.
Sorry, when you say non-compositional type system, what precisely do you mean?
My guess is something like this: https://dl.acm.org/doi/abs/10.1145/2036918.2036930 http://set.ee/publications/cats06.pdf
Apr 14
prev sibling parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 14.04.21 13:43, jmh530 wrote:
 On Monday, 12 April 2021 at 18:14:30 UTC, Timon Gehr wrote:
 [snip]

 Unfortunately that's the nature of the chosen approach: 
 non-compositional type system design is a game of whack-a-mole 
 resulting in progressively more complexity while often maintaining a 
 lack of soundness.
Sorry, when you say non-compositional type system, what precisely do you mean?
Types in a program are usually syntactically generated from some recursive grammar. For example, if you have types `T` and `S`, then `T[]` and `T delegate(S)` are also types. Basically, the semantics of types is "compositional" when the meaning of the constituent types does not implicitly depend on the context in which they are plugged and the meaning of the context does not depend on the constituent types. For example, above, the semantics of `T[]` and `T delegate(S)` are not fully compositional, because they have special cases for `T = void`. Instances of such designs (here taken from C) tend to have ripple effects, slowly adding special cases in other corners of the language. For example, the compiler treats `return exp;` where exp is of type `void` like `exp; return;`. This is an improvement and I would not want to miss it, but it would not have been necessary nor needed any special documentation if `void` had not been special-cased in the first place. (In many languages, `void` is just treated like an ordinary type that has just one value, often identified with the empty tuple.) The DIP is a bit like that, but on steroids, and it already proposes some of the ripple effects. All the while, it is not very powerful. For example, it does not even attempt to solve this case: --- int apply(int delegate(int delegate(int)) f, int delegate(int) g){ return f(g); } --- We want `apply` to have the same qualifiers as `f` and the argument of `f` to have the same qualifiers as `g`. With proper attribute polymorphism, one can just state that in code: --- int apply[qual a,qual b](int delegate(int delegate(int)a)b f, int delegate(int)a g)b{ return f(g); } --- This solution is simple and more general, but most importantly, it does not redefine the meaning of existing types in non-compositional ways and the language does not paint itself into a corner by adopting such a solution. It does not break existing code nor prevent additional features to be added later.
Apr 14
next sibling parent jmh530 <john.michael.hall gmail.com> writes:
On Wednesday, 14 April 2021 at 22:23:38 UTC, Timon Gehr wrote:
 [snip]
 With proper attribute polymorphism, one can just state that in 
 code:

 ---
 int apply[qual a,qual b](int delegate(int delegate(int)a)b f, 
 int delegate(int)a g)b{
     return f(g);
 }
 ---

 This solution is simple and more general, but most importantly, 
 it does not redefine the meaning of existing types in 
 non-compositional ways and the language does not paint itself 
 into a corner by adopting such a solution. It does not break 
 existing code nor prevent additional features to be added later.
Thanks for the explanation! I'm not sure I really grok all the ways it could be used, but it makes sense.
Apr 14
prev sibling parent reply deadalnix <deadalnix gmail.com> writes:
On Wednesday, 14 April 2021 at 22:23:38 UTC, Timon Gehr wrote:
 For example, above, the semantics of `T[]` and `T delegate(S)` 
 are not fully compositional, because they have special cases 
 for `T = void`.
How is void delegate(S) a spcial case? Great post, BTW.
Apr 14
next sibling parent reply Paul Backus <snarwin gmail.com> writes:
On Wednesday, 14 April 2021 at 23:51:33 UTC, deadalnix wrote:
 On Wednesday, 14 April 2021 at 22:23:38 UTC, Timon Gehr wrote:
 For example, above, the semantics of `T[]` and `T delegate(S)` 
 are not fully compositional, because they have special cases 
 for `T = void`.
How is void delegate(S) a spcial case? Great post, BTW.
You can't use `void` as a type on its own, but you *can* use it in certain specific contexts, like `void delegate(S)` and `void[]`. Those specific contexts are special cases: they're not derived from any general rule, but have to be spelled out explicitly in the language spec. (For example, https://dlang.org/spec/arrays.html#void_arrays)
Apr 14
parent reply deadalnix <deadalnix gmail.com> writes:
On Thursday, 15 April 2021 at 01:15:34 UTC, Paul Backus wrote:
 On Wednesday, 14 April 2021 at 23:51:33 UTC, deadalnix wrote:
 On Wednesday, 14 April 2021 at 22:23:38 UTC, Timon Gehr wrote:
 For example, above, the semantics of `T[]` and `T 
 delegate(S)` are not fully compositional, because they have 
 special cases for `T = void`.
How is void delegate(S) a spcial case? Great post, BTW.
You can't use `void` as a type on its own, but you *can* use it in certain specific contexts, like `void delegate(S)` and `void[]`. Those specific contexts are special cases: they're not derived from any general rule, but have to be spelled out explicitly in the language spec. (For example, https://dlang.org/spec/arrays.html#void_arrays)
idk, it seems to be that not being able to declare a void variable is the special case. There are expression in D of type void, and they compose like the rest of it.
Apr 15
next sibling parent Johannes Loher <johannes.loher fg4f.de> writes:
On Thursday, 15 April 2021 at 13:33:37 UTC, deadalnix wrote:
 On Thursday, 15 April 2021 at 01:15:34 UTC, Paul Backus wrote:
 On Wednesday, 14 April 2021 at 23:51:33 UTC, deadalnix wrote:
 On Wednesday, 14 April 2021 at 22:23:38 UTC, Timon Gehr wrote:
 For example, above, the semantics of `T[]` and `T 
 delegate(S)` are not fully compositional, because they have 
 special cases for `T = void`.
How is void delegate(S) a spcial case? Great post, BTW.
You can't use `void` as a type on its own, but you *can* use it in certain specific contexts, like `void delegate(S)` and `void[]`. Those specific contexts are special cases: they're not derived from any general rule, but have to be spelled out explicitly in the language spec. (For example, https://dlang.org/spec/arrays.html#void_arrays)
idk, it seems to be that not being able to declare a void variable is the special case. There are expression in D of type void, and they compose like the rest of it.
You are right, but that’s just another special case for `void` (that should just be fixed imo, I think Dennis is working on a DIP for that). `void` arrays are still a special case because they are the super type of all arrays. This is completely contrary to how array subtyping works for basically all other types. But this is starting to get off topic.
Apr 15
prev sibling parent Timon Gehr <timon.gehr gmx.ch> writes:
On 15.04.21 15:33, deadalnix wrote:
 On Thursday, 15 April 2021 at 01:15:34 UTC, Paul Backus wrote:
 On Wednesday, 14 April 2021 at 23:51:33 UTC, deadalnix wrote:
 On Wednesday, 14 April 2021 at 22:23:38 UTC, Timon Gehr wrote:
 For example, above, the semantics of `T[]` and `T delegate(S)` are 
 not fully compositional, because they have special cases for `T = 
 void`.
How is void delegate(S) a spcial case? Great post, BTW.
You can't use `void` as a type on its own, but you *can* use it in certain specific contexts, like `void delegate(S)` and `void[]`. Those specific contexts are special cases: they're not derived from any general rule, but have to be spelled out explicitly in the language spec. (For example, https://dlang.org/spec/arrays.html#void_arrays)
idk, it seems to be that not being able to declare a void variable is the special case. There are expression in D of type void, and they compose like the rest of it.
Officially, `void` "has no value". (Whatever that means precisely). That makes it a special case throughout, even in cases where the type checker does not have to add specific checks for it because the same logic works to address both cases at once. In fact, that was probably the motivation: the design that was considered might have been to have separate syntax to declare functions that don't return a useful value and functions that do return something, then `void` was the "clever hack" that allows you to reuse certain compiler logic for "both cases", reducing the number of special cases in code. But there don't have to be two cases at all. However, precisely which aspects of a non-compositional design are or are not the special cases seems to be a purely philosophical question; reasonable people may disagree. :)
Apr 15
prev sibling parent Timon Gehr <timon.gehr gmx.ch> writes:
On 15.04.21 01:51, deadalnix wrote:
 On Wednesday, 14 April 2021 at 22:23:38 UTC, Timon Gehr wrote:
 For example, above, the semantics of `T[]` and `T delegate(S)` are not 
 fully compositional, because they have special cases for `T = void`.
How is void delegate(S) a spcial case? ...
`void` has no semantics as a type of a value, so any place where it is valid where ordinarily such a type would be requested is a special case.
 Great post, BTW.
 
Thanks!
Apr 15
prev sibling next sibling parent 12345swordy <alexanderheistermann gmail.com> writes:
On Monday, 12 April 2021 at 09:36:29 UTC, Mike Parker wrote:


 This is the discussion thread for the first round of Community 
 Review of DIP 1041, "Attributes for Higher-Order Functions":

 https://github.com/dlang/DIPs/blob/11fcb0f79ce7ec209fb2a302d1371722d0c8ad82/DIPs/DIP1041.md

 The review period will **end at 11:59 PM ET on April 26**, or 
 when I make a post declaring it complete. Discussion in this 
 thread may continue beyond that point.

 Here in the discussion thread, you are free to discuss anything 
 and everything related to the DIP. Express your support or 
 opposition, debate alternatives, argue the merits, etc.

 However, if you have any specific feedback on how to improve 
 the proposal itself, then please post it in the Feedback 
 Thread. The Feedback Thread will be the source for the review 
 summary that I will write at the end of this review round. I 
 will post a link to that thread immediately following this 
 post. Just be sure to read and understand the Reviewer 
 Guidelines before posting there:

 https://github.com/dlang/DIPs/blob/master/docs/guidelines-reviewers.md

 And my blog post on the difference between the Discussion and 
 Feedback threads:

 https://dlang.org/blog/2020/01/26/dip-reviews-discussion-vs-feedback/

 Please stay on topic here. I will delete posts that are 
 completely off-topic.
From speed reading this dip, any benefits that is to be gain by this dip is overweight by the sheer complexity that is introduced by this dip. -Alex
Apr 12
prev sibling parent Kagamin <spam here.lot> writes:
As I understand, the author proposes to implicitly cast gc 
delegates to nogc delegates, which sounds scary.
Apr 13