-
-
Notifications
You must be signed in to change notification settings - Fork 48
Add RFC for Recover blocks with receiver #182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
89376d5
1295c1c
0f61c73
6a7e65d
96bd3c3
b3d46a1
80beda3
377a1be
2ddc1b1
28a3dfc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| - Feature Name: recover-with-receiver | ||
| - Start Date: 2020-08-26 | ||
| - RFC PR: (leave this empty) | ||
| - Pony Issue: (leave this empty) | ||
|
|
||
| # Summary | ||
|
|
||
| This feature will expand recover syntax to allow general usage of | ||
| recovery as appears in automatic receiver recovery. The change | ||
| will make some use cases possible, while improving the performance | ||
| or ergonomics of some other use cases. | ||
|
|
||
| # Motivation | ||
|
|
||
| Currently, Pony supports two forms of recovery, `recover` blocks, | ||
| as well as automatic receiver recovery. In some cases, these are | ||
| equivalent. If we have a single variable `x: T iso`, then we can | ||
| temporarily use it as another capability inside a recover block, | ||
| with something like: | ||
| ``` | ||
| x = recover | ||
| let x_ref = consume ref x | ||
| // do something with x ... | ||
| x_ref | ||
| end | ||
| ``` | ||
| Alternatively, if the action being taken is precisely a ref method call, | ||
| then automatic receiver recovery can be used if the | ||
| arguments and return types meet the isolation guarantees for `x`. | ||
| ``` | ||
| x.foo(y, z) | ||
| ``` | ||
| But this can't be used for every type of action, it needs to be those | ||
| actions thought of by the original class developer. We can add these methods, | ||
| but this is anti-modular. | ||
|
|
||
| This automatic receiver recovery syntax also works for expressions more complicated than a single variable of course. | ||
| The recover block is less flexible. The recover block method can be used only when the thing being modified is a mutable location. | ||
| It can be used with var fields, but not with let or embed. It can be used when we have update methods, but not with getters alone. | ||
| ``` | ||
| // defined elsehwere | ||
| class Foo | ||
| fun box getSomething(): this->Bar ref | ||
| ... | ||
| fun box values(): this->FooIterator ref | ||
|
|
||
| class FooHolder | ||
| embed foo: Foo iso = Foo | ||
| // or let | ||
|
|
||
| fun ref doSomethingWithFoo() => | ||
| // error, iso->ref = tag | ||
| foo.getSomething().somethingElse() | ||
|
|
||
| // try to recover to use foo as ref: error, can't assign | ||
| foo = recover | ||
| ... consume foo | ||
| end | ||
| end | ||
| ``` | ||
| We might also have read-only methods. Imagine we take in an Iter over iso objects. We don't want to be coupled to | ||
| the class used, such as Array, and allow a generic, potentially chained iterator. | ||
| ``` | ||
| class UsesIter[T: SomeInterface] | ||
| fun process(iter: Iterator[T]) => | ||
| // want to call a few complicated methods on T | ||
| // if T might be unique, we can't store in a variable, | ||
| // so we want to recover, but we can't! | ||
|
|
||
| // error, not a subtype | ||
| let next: T = iter.next()? | ||
|
|
||
| // ???? | ||
| iter.next() = recover | ||
| ... | ||
| end | ||
|
|
||
| // works... but only if we can | ||
| // express *all* of the things we want | ||
| // to do as multiple methods | ||
| // still anti-modular! | ||
| iter.next()?.foo().>bar() | ||
| end | ||
| ``` | ||
|
|
||
| This RFC will add a syntax to expand the design of recover blocks to allow a receiver, subsuming automatic receiver recovery. | ||
| In both cases above, the recover with receiver may be used in order to temporarily use these values as ref, allowing free | ||
| usage of methods, without requiring that the methods were defined ahead of time in the interface or class, and without | ||
| requiring extra potentially erroring accesses or allocating and swapping new values via update methods. | ||
|
|
||
| # Detailed design | ||
|
|
||
| We will add new syntactic forms to allow recover blocks based around an existing receiver expression. | ||
|
|
||
| ``` | ||
| e.recover as x => | ||
| e | ||
| end | ||
| ``` | ||
| and a shorthand | ||
| ``` | ||
| e.recover | ||
| e | ||
| end | ||
| ``` | ||
| Where in both cases, `e` is an expression, and `x` is a variable binding. In the second case, the first `e` should be either a variable or a field access. | ||
| Inside the body of the recover block, the variable `x` will be bound as a `let` binding. For the shorthand, the name of this variable will be the name | ||
| of the variable that the expression is, or the rightmost field name. | ||
|
|
||
| The capability of the new binding will depend on the capability of the expression. If it is a unique capability, `iso` or `trn`, then the resulting capability | ||
| will be the strongest aliasable type: `ref`. If it is any self-aliasing capability `k`, then the resulting capability will be `k`. | ||
| Acknowledging that there may be better choices available, at this time `iso^` or `trn^` will take the capability `ref` and act identically to their | ||
| non-ephemeral counterparts. Any variables syntactically present in the receiver expression are considered in-use for the duration of the block and cannot be consumed or re-assigned. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is that strong enough of a restriction? Is it possible that some unsafe chicanery could be done with destructive assign to fields of an in-use variable, even if the in-use variable is not consumed?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmmm, you're right that this wasn't written nearly as restrictive as it should be. This condition should not deviate from the soundness guaranteed by normal function calls and by recover blocks. In order to not stretch beyond what would be available to a function call, we can require that the capability be After looking back at ponylang/ponyc#3596 I'm not convinced that the cases we gave were unsound (an
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It may not be clear what the restriction to |
||
|
|
||
| The body of the recover expression will be type-checked similarly to how recover blocks are checked today, with two exceptions. The block will have | ||
| a capability associated with it, and instead of restricting to sendable variable usage, they are restricted to capabilities which are safe-to-write. | ||
| In practice, the only special case here is writing `trn` to `trn. The result of the recover block will be adapted in the the viewpoint of the | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just leaving a note to discuss this ( |
||
| recover block. This subsumes the existing conditions of being either unused or safe to extract. | ||
|
|
||
| If the receiver capability is not a unique cap (`iso` or `trn`), then this environment is always treated as `ref` and there are no restrictions on used or returned variables. | ||
|
|
||
| For a method call to a `ref` method, it is treated as being wrapped in an implicit receiver recovery block. That is, | ||
| `x.f(y, z)` can be de-sugared to `x.recover x.f(y, z) end`, using the shorthand syntax above. | ||
|
|
||
| For implementation, each recover block will have an optional receiver and a capability of the recover (note that this capability is different than the return capability of a regular recover block). Until the adoption of the more permissive viewpoint adaptation for ephemerals, we will have to treat recover blocks without receiver a special case. A sensible choice would be to mark all such blocks as capability `iso^`. When checking expressions for the recover block, sendability restrictions will be checked relative to the block. Return values would be checked with viewpoint adaptation as specified, except for standard recover blocks, which will use existing rules. | ||
|
|
||
| # How We Teach This | ||
|
|
||
| We can refer to this feature as either reciever recovery or recovery with receiver. The section on recover blocks will be modified with an additional section to | ||
| reflect the new type of recover blocks. In this setting we may wish to make a footnote as to `trn` receivers having looser isolation requirements. | ||
| Examples should reflect some of the previously impossible use cases above, as this helps in explaining usage of isolated capabilities in data structures. | ||
|
|
||
| The existing cases of automatic recovery, when calling ref methods, and constructors, will be presented together as conveniences. | ||
|
|
||
| # How We Test This | ||
|
|
||
| This will require additional tests for different receivers and both of the unique capabilities. Existing tests around automatic receiver recovery should be maintained and should continue to pass. | ||
|
|
||
| # Drawbacks | ||
|
|
||
| Why should we *not* do this? Things you might want to note: | ||
|
|
||
| * This may frontload recovery concepts slightly sooner for learners, rather than just presenting receiver recovery for functions | ||
| * Generic technical costs of new features | ||
|
|
||
| # Alternatives | ||
|
|
||
| We may try to expand automated recovery to handle more cases like the above, at the cost of a lack of simplicity. | ||
|
|
||
| # Unresolved questions | ||
|
|
||
| The syntax may still need work. | ||
| Research has not fully caught up to more powerful recovery mechanisms as a general detail. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There have been discussions in the past about syntax in which we have decided to try to avoid "overloading" a keyword or symbol with multiple meanings. In this case I think we should avoid using
asfor this syntax, since it is entirely different from how we useasfor runtime type matching with the right side being a type name.Not that I'm saying I like everything about Pony syntax as it is today (I don't), but I'm just raising this comment for consistency purposes.
In Pony syntax I think the most applicable symbol/keyword to use here would be
|(as used inmatchblocks).Also I think you meant to use
xinside the block instead ofe, so I included that change in this suggestion as well.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I applied this manually, but the use of
edenotes an expression, hence whyxis not used. These are grammatical definitions, not examples.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jasoncarr0 that confused me as well; I think
e.recover | x => f(x) end(or similar) would be more readable.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about
e[x]? would that make it clear, or... x .... Of course at the end of the day this is a pretty meaningless subset of the RFC, and I can go to what makes it clear, but on a personal level those ones feel incorrect.