From 824bf6000adc8f16c9c77009b6124eb97ece634f Mon Sep 17 00:00:00 2001 From: Joe Eli McIlvain Date: Mon, 25 May 2020 11:48:58 -0700 Subject: [PATCH 1/6] Add RFC for assign param syntax. --- text/0000-assign-param-syntax.md | 115 +++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 text/0000-assign-param-syntax.md diff --git a/text/0000-assign-param-syntax.md b/text/0000-assign-param-syntax.md new file mode 100644 index 00000000..718a93bd --- /dev/null +++ b/text/0000-assign-param-syntax.md @@ -0,0 +1,115 @@ +- Feature Name: explicit_self_member_access +- Start Date: 2020-05-24 +- RFC PR: +- Pony Issue: + +# Summary + +The syntax change would allow a field to be referenced in the position of a parameter, indicating that the argument passed for that parameter position should be immediately assigned to the field rather than given its own name in the local scope. + +# Motivation + +*Note: The syntax examples and text of this RFC assume that the RFC for "explicit self member access" with the `@` character have already been accepted and included in the language. If you haven't read the other RFC yet, please review that first, then read this one with that context.* + +In current Pony syntax, there can be significant code redundancy required to build simple data classes that take the arguments of their constructors and simply assign them to the field of the same name. + +One must declare the field, with its name and type, then declare the parameter with the same name and type in a particular position, then write an assignment statement in the body of the method. + +Consider the following real-world code example from the `ponycc` repository, in which a simple data class with six fields requires 22 lines of code with redundant type information, just to get the class off the ground with a constructor and before writing any useful methods: + +```pony +class val UseFFIDecl is (AST & UseDecl) + let attachments: (Attachments | None) + let name: (Id | LitString) + let return_type: TypeArgs + let params: Params + let partial: (Question | None) + let guard: (IfDefCond | None) + + new val create( + name: (Id | LitString), + return_type: TypeArgs, + params: Params, + partial: (Question | None), + guard: (IfDefCond | None) = None, + attachments: (Attachments | None) = None) + => + @attachments = attachments + @name = name + @return_type = return_type + @params = params + @partial = partial + @guard = guard +``` + +# Detailed design + +The change proposed in this RFC introduces "assign parameters", which allow a field to be referenced in the position of a parameter, indicating that the argument passed for that parameter position should be immediately assigned to the field rather than given its own name in the local scope. + +Each field will be referenced with its `@`-prefixed identifier, just as it would be referenced in the body of a method. Here is the same example from above, rewritten now in just 9 lines compared to the earlier 22: + +```pony +class val UseFFIDecl is (AST & UseDecl) + let attachments: (Attachments | None) + let name: (Id | LitString) + let return_type: TypeArgs + let params: Params + let partial: (Question | None) + let guard: (IfDefCond | None) + + new val create(@name, @return_type, @params, @partial, @guard, @attachments) +``` + +Note that there is now no syntactical body of the method, but the compiler will add a body that does the appropriate assignments. + +Of course, "assign parameters" can be used alongside normal parameters, and not every field need be associated to an "assign parameter". Assign parameters can also have a "default value" as normal parameters can, which specifies the value to use if the caller does not supply an argument for that position. The following refactored example demonstrates all of those concepts: + +```pony +class val UseFFIDecl is (AST & UseDecl) + let attachments: (Attachments | None) + let name: (Id | LitString) + let return_type: TypeArgs + let params: Params + let partial: (Question | None) + let guard: (IfDefCond | None) + + new val create(name: String, @return_type, @params, @attachments = None) => + @name = Id(name) + @partial = None + @guard = None +``` + +As you can see, this syntax opens new opportunities but shouldn't preclude any old ones. Thus, it is not a breaking change or required learning, and it merely offers convenience for those looking for it. There are no new keywords or symbols needed in the parser. + +For many more syntax examples, see [the `.mare` files in the Mare compiler's codebase](https://github.com/jemc/mare/tree/master/src/prelude), where this syntax already exists and is parsed by the parser, along with the other syntax changes included there. This RFC and others to come are attempting to try to get some general consensus agreement on the desireability of at least some of the syntax changes that have been introduced in that work. + +## Compiler implementation discussion + +Internally, the names of these parameters are anonymous/hygienic ids, meaning that they cannot be used by any explicit code and will not collide with any such names - they are are anonymous references that are immediately assigned at the beginning of the function body. + +Note that they must also be `consume`d in the assignment, so that the assignment will work properly for `iso`- and `trn`-typed fields. Again, this is an internal implementation detail just to make it behave as the user expects. + +One additional implementation wrinkle is how to handle the case of non-sendable field caps being used as assign params to functions/constructors that allow only sendable parameters. For example, the case of an `ref` field on an assign param of an actor's constructor. In this case, the assign param cannot be the same cap as the field as it usually would be, since `ref` parameters are not sendable to the actor's constructor. Instead we must "lift" the cap of the assign param to the "nearest" sendable cap that can be assigned to that field cap - in this case, it would be `iso`. This follows the principle of least surprise because this is exactly what the programmer would have to do explicitly to make such a code pattern work properly with capabilities. + +# How We Teach This + +Add a new section to the tutorial to teach this. + +Change some of the standard library to demonstrate it in action. + +# How We Test This + +New compiler tests that demonstrate this case, including the edge cases mentioned in the implementation details above. + +# Drawbacks + +* New patterns to learn (though no codebases will be required to use these patterns, you have to learn the new patterns if you want to write or read code that uses them). +* Added compiler complexity. + +# Alternatives + +- Don't do any of this, and leave the existing verbosity in place. + +# Unresolved questions + +- None at this time. From 12a15dac4a080e540b07349ce67f4e5a8ff3c992 Mon Sep 17 00:00:00 2001 From: Joe Eli McIlvain Date: Tue, 3 Jan 2023 07:40:23 -0800 Subject: [PATCH 2/6] Update "assign param" RFC to use `this.` syntax. The support for the `@` syntax is not there yet, so we can move forward with this RFC without that syntax yet in place. --- text/0000-assign-param-syntax.md | 38 +++++++++++++++----------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/text/0000-assign-param-syntax.md b/text/0000-assign-param-syntax.md index 718a93bd..dc5fe4a9 100644 --- a/text/0000-assign-param-syntax.md +++ b/text/0000-assign-param-syntax.md @@ -9,8 +9,6 @@ The syntax change would allow a field to be referenced in the position of a para # Motivation -*Note: The syntax examples and text of this RFC assume that the RFC for "explicit self member access" with the `@` character have already been accepted and included in the language. If you haven't read the other RFC yet, please review that first, then read this one with that context.* - In current Pony syntax, there can be significant code redundancy required to build simple data classes that take the arguments of their constructors and simply assign them to the field of the same name. One must declare the field, with its name and type, then declare the parameter with the same name and type in a particular position, then write an assignment statement in the body of the method. @@ -27,26 +25,26 @@ class val UseFFIDecl is (AST & UseDecl) let guard: (IfDefCond | None) new val create( - name: (Id | LitString), - return_type: TypeArgs, - params: Params, - partial: (Question | None), - guard: (IfDefCond | None) = None, - attachments: (Attachments | None) = None) + name': (Id | LitString), + return_type': TypeArgs, + params': Params, + partial': (Question | None), + guard': (IfDefCond | None) = None, + attachments': (Attachments | None) = None) => - @attachments = attachments - @name = name - @return_type = return_type - @params = params - @partial = partial - @guard = guard + this.attachments = attachments' + this.name = name' + this.return_type = return_type' + this.params = params' + this.partial = partial' + this.guard = guard' ``` # Detailed design The change proposed in this RFC introduces "assign parameters", which allow a field to be referenced in the position of a parameter, indicating that the argument passed for that parameter position should be immediately assigned to the field rather than given its own name in the local scope. -Each field will be referenced with its `@`-prefixed identifier, just as it would be referenced in the body of a method. Here is the same example from above, rewritten now in just 9 lines compared to the earlier 22: +Each field will be referenced with its `this.`-prefixed identifier, just as it would be referenced in the body of a method. Here is the same example from above, rewritten now in just 9 lines compared to the earlier 22: ```pony class val UseFFIDecl is (AST & UseDecl) @@ -57,7 +55,7 @@ class val UseFFIDecl is (AST & UseDecl) let partial: (Question | None) let guard: (IfDefCond | None) - new val create(@name, @return_type, @params, @partial, @guard, @attachments) + new val create(this.name, this.return_type, this.params, this.partial, this.guard, this.attachments) ``` Note that there is now no syntactical body of the method, but the compiler will add a body that does the appropriate assignments. @@ -73,10 +71,10 @@ class val UseFFIDecl is (AST & UseDecl) let partial: (Question | None) let guard: (IfDefCond | None) - new val create(name: String, @return_type, @params, @attachments = None) => - @name = Id(name) - @partial = None - @guard = None + new val create(name: String, this.return_type, this.params, this.attachments = None) => + this.name = Id(name) + this.partial = None + this.guard = None ``` As you can see, this syntax opens new opportunities but shouldn't preclude any old ones. Thus, it is not a breaking change or required learning, and it merely offers convenience for those looking for it. There are no new keywords or symbols needed in the parser. From e88fbf4c49bc42fd2078498b0e27ed363d25c9d4 Mon Sep 17 00:00:00 2001 From: Joe Eli McIlvain Date: Tue, 3 Jan 2023 07:42:04 -0800 Subject: [PATCH 3/6] Clarify required `this.` for assign param syntax --- text/0000-assign-param-syntax.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-assign-param-syntax.md b/text/0000-assign-param-syntax.md index dc5fe4a9..ad8d32db 100644 --- a/text/0000-assign-param-syntax.md +++ b/text/0000-assign-param-syntax.md @@ -44,7 +44,7 @@ class val UseFFIDecl is (AST & UseDecl) The change proposed in this RFC introduces "assign parameters", which allow a field to be referenced in the position of a parameter, indicating that the argument passed for that parameter position should be immediately assigned to the field rather than given its own name in the local scope. -Each field will be referenced with its `this.`-prefixed identifier, just as it would be referenced in the body of a method. Here is the same example from above, rewritten now in just 9 lines compared to the earlier 22: +Each field will be referenced with its `this.`-prefixed identifier, just as it would be referenced in the body of a method (though unlike in that context, the `this.` prefix would be required rather than optional here). Here is the same example from above, rewritten now in just 9 lines compared to the earlier 22: ```pony class val UseFFIDecl is (AST & UseDecl) From 2c48c1c48bfcc1d12640c33ce26a3c69c0a9f302 Mon Sep 17 00:00:00 2001 From: Joe Eli McIlvain Date: Tue, 3 Jan 2023 11:14:16 -0800 Subject: [PATCH 4/6] Update "assign param" RFC syntax for `=> None` requirement --- text/0000-assign-param-syntax.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/0000-assign-param-syntax.md b/text/0000-assign-param-syntax.md index ad8d32db..5c927753 100644 --- a/text/0000-assign-param-syntax.md +++ b/text/0000-assign-param-syntax.md @@ -55,10 +55,10 @@ class val UseFFIDecl is (AST & UseDecl) let partial: (Question | None) let guard: (IfDefCond | None) - new val create(this.name, this.return_type, this.params, this.partial, this.guard, this.attachments) + new val create(this.name, this.return_type, this.params, this.partial, this.guard, this.attachments) => None ``` -Note that there is now no syntactical body of the method, but the compiler will add a body that does the appropriate assignments. +Note that there is now no syntactical body of the method apart from the obligatory `None` expression, and the compiler will add a body that does the appropriate assignments. Of course, "assign parameters" can be used alongside normal parameters, and not every field need be associated to an "assign parameter". Assign parameters can also have a "default value" as normal parameters can, which specifies the value to use if the caller does not supply an argument for that position. The following refactored example demonstrates all of those concepts: From acbb2633554324c54e9ff1f470ac79b37bead9d1 Mon Sep 17 00:00:00 2001 From: Joe Eli McIlvain Date: Tue, 3 Jan 2023 11:21:28 -0800 Subject: [PATCH 5/6] Remove reference to Mare compiler with outdated link --- text/0000-assign-param-syntax.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/text/0000-assign-param-syntax.md b/text/0000-assign-param-syntax.md index 5c927753..639b27bc 100644 --- a/text/0000-assign-param-syntax.md +++ b/text/0000-assign-param-syntax.md @@ -79,8 +79,6 @@ class val UseFFIDecl is (AST & UseDecl) As you can see, this syntax opens new opportunities but shouldn't preclude any old ones. Thus, it is not a breaking change or required learning, and it merely offers convenience for those looking for it. There are no new keywords or symbols needed in the parser. -For many more syntax examples, see [the `.mare` files in the Mare compiler's codebase](https://github.com/jemc/mare/tree/master/src/prelude), where this syntax already exists and is parsed by the parser, along with the other syntax changes included there. This RFC and others to come are attempting to try to get some general consensus agreement on the desireability of at least some of the syntax changes that have been introduced in that work. - ## Compiler implementation discussion Internally, the names of these parameters are anonymous/hygienic ids, meaning that they cannot be used by any explicit code and will not collide with any such names - they are are anonymous references that are immediately assigned at the beginning of the function body. From c8af01bff33bee81460f740ab99a8f6a80e4e74e Mon Sep 17 00:00:00 2001 From: Joe Eli McIlvain Date: Tue, 3 Jan 2023 11:32:33 -0800 Subject: [PATCH 6/6] Add note about assign params not being usable as named arguments --- text/0000-assign-param-syntax.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/text/0000-assign-param-syntax.md b/text/0000-assign-param-syntax.md index 639b27bc..d05b0971 100644 --- a/text/0000-assign-param-syntax.md +++ b/text/0000-assign-param-syntax.md @@ -79,6 +79,8 @@ class val UseFFIDecl is (AST & UseDecl) As you can see, this syntax opens new opportunities but shouldn't preclude any old ones. Thus, it is not a breaking change or required learning, and it merely offers convenience for those looking for it. There are no new keywords or symbols needed in the parser. +Note that there these "assign params" don't have a name, so these parameters would not be usable with named arguments at the call site - only positional arguments. + ## Compiler implementation discussion Internally, the names of these parameters are anonymous/hygienic ids, meaning that they cannot be used by any explicit code and will not collide with any such names - they are are anonymous references that are immediately assigned at the beginning of the function body.