digitalmars.D - Discussion Thread: DIP 1041--Attributes for Higher-Order
- Mike Parker (22/22) Apr 12 2021 ## Discussion Thread
- Mike Parker (3/8) Apr 12 2021 The **Feedback Thread** is located here:
- Timon Gehr (95/148) Apr 12 2021 https://github.com/dlang/DIPs/blob/11fcb0f79ce7ec209fb2a302d1371722d0c8a...
- Q. Schroll (55/202) Apr 12 2021 I had a more detailed Abstract in previous drafts, but if you
- ag0aep6g (40/46) Apr 12 2021 I don't think you can cite DMD on the matter. It contradicts
- Timon Gehr (49/228) Apr 12 2021 That's subtyping, not polymorphism. Polymorphism is when a term depends
- Q. Schroll (82/320) Apr 25 2021 That sounds a lot like generics (in the sense of Java or C#
- Imperatorn (3/7) Apr 12 2021 I think the DIP has a noble goal, but is too complex. Try to keep
- Timon Gehr (5/15) Apr 12 2021 Unfortunately that's the nature of the chosen approach:
- Walter Bright (2/5) Apr 13 2021 I could never have expressed the notion as clearly as that.
- jmh530 (3/8) Apr 14 2021 Sorry, when you say non-compositional type system, what precisely
- Imperatorn (4/13) Apr 14 2021 My guess is something like this:
- Timon Gehr (40/50) Apr 14 2021 Types in a program are usually syntactically generated from some
- jmh530 (4/18) Apr 14 2021 Thanks for the explanation!
- deadalnix (3/6) Apr 14 2021 How is void delegate(S) a spcial case?
- Paul Backus (7/14) Apr 14 2021 You can't use `void` as a type on its own, but you *can* use it
- deadalnix (4/20) Apr 15 2021 idk, it seems to be that not being able to declare a void
- Johannes Loher (7/28) Apr 15 2021 You are right, but that’s just another special case for `void`
- Timon Gehr (14/36) Apr 15 2021 Officially, `void` "has no value". (Whatever that means precisely).
- Timon Gehr (4/13) Apr 15 2021 `void` has no semantics as a type of a value, so any place where it is
- 12345swordy (5/28) Apr 12 2021 From speed reading this dip, any benefits that is to be gain by
- Kagamin (2/2) Apr 13 2021 As I understand, the author proposes to implicitly cast gc
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 2021
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 2021
On 12.04.21 16:44, Q. Schroll wrote:On Monday, 12 April 2021 at 11:05:14 UTC, Timon Gehr wrote:https://github.com/dlang/DIPs/blob/11fcb0f79ce7ec209fb2a302d1371722d0c8 d82/DIPs/DIP1041.md ...On 12.04.21 11:38, Mike Parker wrote:with details way before being told what the problem actually is or how the proposal addresses it.Unfortunately, it is not written too well: The reader gets floodedDoesn't the Abstract explain what the problem is and give a generalidea 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".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.As far as I can tell, this is trying to introduce attributeYou're mistaken. You can take a look at the Alternatives forseemingly 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 ofassigning 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 assignmentsto 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 themotivation 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.as motivation abuses an existing type system hole.To add insult to injury, the first example that's shown in the DIPI 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 pointerand 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.result accessible to `toString` is in the context of `sink`, but somehow `result` is mutated anyway.`toString` is `const`, `sink` is `const`, the only reference toSee the paragraph above.being an instance of it.Unsoundness should be fixed, not embraced!Yes, I guess no one disagrees on that one, but on the question of itreasonable way to manipulate delegates in higher-order functions involves calling them, but this is not accurate.Finally, there's this concern: The DIP assumes that the onlyIt assumes that the the most common use-case of non-mutable delegateparameters 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.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!```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 suddenlyYou 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 theidea 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 Bdelegate(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 isalready 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 2021
On Monday, 12 April 2021 at 17:21:47 UTC, Timon Gehr wrote:On 12.04.21 16:44, Q. Schroll wrote:I had a more detailed Abstract in previous drafts, but if you think I watered it down too much, I can add more details.On Monday, 12 April 2021 at 11:05:14 UTC, Timon Gehr wrote: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".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?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.I know there are, and I literally state how to do it in the quoted excerpt.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.Maybe I'm not creative enough for a proper solution, but I should be, since the problem is "easy".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.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.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 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.")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.)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.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.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.Yes. I meant to say: "needs to remember to make the parameters mutable.".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.I guess you mean the other way around.`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.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.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.```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 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.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.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.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.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.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 2021
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:[...]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.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 2021
On 4/12/21 11:59 PM, Q. Schroll wrote:On Monday, 12 April 2021 at 17:21:47 UTC, Timon Gehr wrote: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.On 12.04.21 16:44, Q. Schroll wrote:I had a more detailed Abstract in previous drafts, but if you think I watered it down too much, I can add more details.On Monday, 12 April 2021 at 11:05:14 UTC, Timon Gehr wrote: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".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?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. ...I know there are, and I literally state how to do it in the quoted excerpt.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 am not saying it is easy. I am saying humanity has a rich cultural history and this happens to be a solved problem.Maybe I'm not creative enough for a proper solution, but I should be, since the problem is "easy". ...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.It does not have to be higher-order specific at all. Might as well fix `inout` at the same time.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. ...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 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.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. ...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.)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.That allows building a gadget to completely bypass transitivity of qualifiers, including `immutable` and `shared`.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.")std.parallelism.parallel cannot be annotated safe or trusted.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. ...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); }It's not what you need. Reassigning is fine, it just has to be something with compatible attributes.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. ...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.The error message would have to say it was designed improperly.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. ...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.ugly, check, workaround, check.This isn't an ugly workaround,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.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.It defies attribute transitivity, which is a stated design goal.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.Unfortunately, the compiler has bugs. One can't take its behavior as holy gospel that just needs to be interpreted correctly.
Apr 12 2021
On Tuesday, 13 April 2021 at 00:14:38 UTC, Timon Gehr wrote:On 4/12/21 11:59 PM, Q. Schroll wrote: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.On Monday, 12 April 2021 at 17:21:47 UTC, Timon Gehr wrote: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_polymorphismOn 12.04.21 16:44, Q. Schroll wrote:I had a more detailed Abstract in previous drafts, but if you think I watered it down too much, I can add more details.On Monday, 12 April 2021 at 11:05:14 UTC, Timon Gehr wrote: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".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?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. ...I know there are, and I literally state how to do it in the quoted excerpt.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.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.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.It does not have to be higher-order specific at all. Might as well fix `inout` at the same time.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. ...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.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`.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.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. ...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.)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.That allows building a gadget to completely bypass transitivity of qualifiers, including `immutable` and `shared`.Assuming the concept has merit, it would still be bad design to abitrarily tie it to delegate contexts.Completely surprising behavior and a breaking change.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.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.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.")std.parallelism.parallel cannot be annotated safe or trusted.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. ...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); } ```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.It's not what you need. Reassigning is fine, it just has to be something with compatible attributes.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. ...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.The error message would have to say it was designed improperly.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. ...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.ugly, check, workaround, check.This isn't an ugly workaround,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.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.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.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.It defies attribute transitivity, which is a stated design goal.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.I don't. And I was wrong about the DIP part. It's the other way around because it's a breaking 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.
Apr 25 2021
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 2021
On 12.04.21 20:09, Imperatorn wrote:On Monday, 12 April 2021 at 09:36:29 UTC, Mike Parker wrote: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.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 2021
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 2021
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 2021
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:My guess is something like this: https://dl.acm.org/doi/abs/10.1145/2036918.2036930 http://set.ee/publications/cats06.pdf[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 2021
On 14.04.21 13:43, jmh530 wrote:On Monday, 12 April 2021 at 18:14:30 UTC, Timon Gehr wrote: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.[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 2021
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 2021
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 2021
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: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)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 2021
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: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.On Wednesday, 14 April 2021 at 22:23:38 UTC, Timon Gehr wrote: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)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 15 2021
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: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.On Wednesday, 14 April 2021 at 23:51:33 UTC, deadalnix wrote: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.On Wednesday, 14 April 2021 at 22:23:38 UTC, Timon Gehr wrote: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)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 15 2021
On 15.04.21 15:33, deadalnix wrote:On Thursday, 15 April 2021 at 01:15:34 UTC, Paul Backus wrote: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. :)On Wednesday, 14 April 2021 at 23:51:33 UTC, deadalnix wrote: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.On Wednesday, 14 April 2021 at 22:23:38 UTC, Timon Gehr wrote: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)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 15 2021
On 15.04.21 01:51, deadalnix wrote:On Wednesday, 14 April 2021 at 22:23:38 UTC, Timon Gehr wrote:`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.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.Thanks!
Apr 15 2021
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 2021
As I understand, the author proposes to implicitly cast gc delegates to nogc delegates, which sounds scary.
Apr 13 2021