-
Notifications
You must be signed in to change notification settings - Fork 24
FLIP for Attachments #11
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
Changes from all commits
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,286 @@ | ||
| # Attachments | ||
|
|
||
| | Status | (Proposed) | | ||
| :-------------- |:---------------------------------------------------- | | ||
| | **FLIP #** | [NNN](Link to FLIP) | | ||
| | **Author(s)** | Daniel Sainati (daniel.sainati@dapperlabs.com) | | ||
| | **Sponsor** | Daniel Sainati (daniel.sainati@dapperlabs.com) | | ||
| | **Updated** | 2022-09-21 | | ||
|
|
||
| ## Objective | ||
|
|
||
| This FLIP proposes to add a new `attachment` feature to Cadence, allowing users to extend existing | ||
| composite types (structs and resources) with additional fields and methods, without modifying the | ||
| original declaration. This feature is purely additive, i.e. no existing functionality is changed or removed. | ||
|
|
||
| ## Motivation | ||
|
|
||
| It is currently not possible to extend existing types unless the original author explicitly made provisions for future functionality. | ||
|
|
||
| For example, to make a resource declaration extensible, its author may add a field that allows any other code to store an extension. However, this requires a lot of boilerplate and is brittle. The original type must be prepared to store additional data with potentially additional functionality. | ||
|
|
||
| Instead, this would allow users to extend existing types whether or not the original author planned for that use case. | ||
|
|
||
| ## User Benefit | ||
|
|
||
| This enables a number of uses cases that were previously difficult or impossible, including [adding autographs to TopShot moments](https://github.com/onflow/cadence/issues/357#issuecomment-683387179)(or generally adding signature/edition info to existing NFTs), or [adding apparel to CryptoKitties](https://kittyhats.co/#/). | ||
|
|
||
| ## Design Proposal | ||
|
|
||
| ### Attachment Declarations | ||
|
|
||
| The new attachment feature would be used with a new `attachment` keyword, which would be declared using a new form of composite declaration: | ||
| `<access modifier> attachment <Name> for <Type> { ... }`, where the attachment methods and fields are declared in the body. As such, | ||
| the following would be examples of legal declarations of attachments: | ||
|
|
||
| ```cadence | ||
| pub attachment Foo for MyStruct { | ||
| ... | ||
| } | ||
|
|
||
| priv attachment Bar for MyResource { | ||
| ... | ||
| } | ||
| ``` | ||
|
|
||
| Specifying the kind (struct or resource) of an attachment is not necessary, as its kind will necessarily be the same as the type it is extending. At this time, | ||
| extensions can only be defined for a resource composite type. | ||
|
|
||
| The access modifier defines the scope in which the `attachment` can be used: a `pub attachment` can be attached to its original type anywhere that imports it, | ||
| while an `access(contract) attachment` can only be used within the contract that defines it. Note that this access is different than the access | ||
| of the fields or methods within the `attachment` itself; a `pub` extension can declare a `priv` field, for example. It is also worth noting that this access | ||
| modifier only applies to the "attaching" of the `attachment`; an `attachment` can be removed from a resource by the owner of that resource in any context. | ||
|
|
||
| Within the attachment declaration, fields and methods can be defined the same way they would be in a struct or resource declaration, with an access modifier and | ||
| a declaration kind. The fields of the base type for which the attachment are accessible to the attachment using the `super` value, which is an implicit field | ||
| of the attachment that is a reference to the base type. So, for an attachment declared `pub attachment Foo for Bar`, the `super` field of `Foo` would have type `&Bar`. | ||
| The fields and methods defined on the attachment itself would be accessible using the `self` value as normal. Note, however, that attachments only have access to the same fields and functions on the `super` field as other code declared in the same place would have; i.e. an attachment defined in the same contract as its original type would have access to `pub` and `access(contract)` fields and methods on `super`, but not `priv` fields or methods, while an attachment defined in a different contract and account to its original type would only be able to reference `pub` fields and methods on the `super` field. | ||
|
|
||
| So, for example, this would be a valid declaration of an attachment: | ||
|
|
||
| ``` | ||
| pub resource R { | ||
| pub let x: Int | ||
|
|
||
| init (_ x: Int) { | ||
| self.x = x | ||
| } | ||
|
|
||
| pub fun foo() { ... } | ||
| } | ||
|
|
||
| pub attachment A for R { | ||
| pub let derivedX: Int | ||
|
|
||
| init (_ scalar: Int) { | ||
| self.derivedX = super.x * scalar | ||
| } | ||
|
|
||
| pub fun foo() { | ||
| super.foo() | ||
| } | ||
| } | ||
|
|
||
| ``` | ||
|
|
||
| Any fields that are declared in an attachment must be initialized, just as any fields declared in a composite must be. An attachment | ||
| that declares fields must declare an initializer, which is run when the attachment is created. The `init` function on an attachment is run after | ||
| the attachment is attached to the base type, so `super` will have a non-`nil` value and the fields and methods of the base types that are accessible to | ||
dsainati1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| the base type will be present on that reference. | ||
|
|
||
| The same checks on normal initializers apply to attachment initializers; namely that all the fields declared in the attachment must receive a value in the | ||
| extension's initializer. So, the following would be a legal attachment: | ||
dsainati1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ```cadence | ||
| pub resource R {} | ||
|
|
||
| pub attachment A for R { | ||
| pub let x: String | ||
| init(_ x: String) { | ||
| self.x = x | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| while this would not: | ||
|
|
||
| ```cadence | ||
| pub resource R {} | ||
|
|
||
| pub attachment E for R { | ||
| pub let x: String | ||
| pub let y: String | ||
| init(_ x: String) { | ||
| self.x = x | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Any resource fields (which are only legal in resource attachments) must also be explicitly handled in a `destroy` method, which is run when | ||
| the extension is destroyed/removed from its base type. Like `init`, because `destroy` will be before the attachment is actually removed from the base | ||
| type, `super` will be populated and accessible in the method body. | ||
|
|
||
| If a resource with attachments on it is `destroy`ed, the `destroy` methods of all its attachments are all run in an unspecified order; `destroy` should not | ||
| rely on the presence of other attachments on the base type in its implementation. The only guarantee about the order in which attachments are destroyed in this case | ||
| is that the base type will be the last thing destroyed. | ||
|
|
||
| An attachment declared with `pub attachment A for C { ... }` will have a nominal type `A`. | ||
|
|
||
| ### Adding Attachments to a Type | ||
|
|
||
| An attachment can be attached to a base type using the `attach e1 to e2` expression. This expression requires that `e1` have an attachment type, | ||
| and that `e2` have the composite type to which `e1` is intended to be attached. It will fail to typecheck otherwise. | ||
|
|
||
| ```cadence | ||
| resource R {} | ||
| attachment A for R {} | ||
| ``` | ||
|
|
||
| The following would be valid ways to attach `A` to `@R`: | ||
|
|
||
| ```cadence | ||
| let r <- create R() | ||
| let r2 <- attach A() to <-r | ||
| ... | ||
| ``` | ||
|
|
||
| or | ||
|
|
||
| ```cadence | ||
| let r2 <- attach A() to <-create R() | ||
| ``` | ||
|
|
||
| An attachment can only be created in the same statement in which it is attached; so the `A()` expression is only legal inside an `attach` expression. | ||
| If the attachment has an initializer, the arguments to that initializer are provided in the creation of the attachment like so: | ||
|
|
||
| ```cadence | ||
| struct S { | ||
| pub let x: String | ||
| init(x: String) { | ||
| self.x = x | ||
| } | ||
| } | ||
|
|
||
| attachment A for S { | ||
| pub let y: Int | ||
| init(y:Int) { | ||
| self.y = y | ||
| } | ||
| } | ||
|
|
||
| let sa = attach A(y: 3) to S(x: "foo") | ||
| ``` | ||
|
|
||
| ### Removing Attachments from a Type | ||
|
|
||
| Attachments can be removed with a new statement: `remove t from e`. The `t` value here is a type name, rather than a value, | ||
|
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. Can a composite type have multiple attachments of the same type?
Contributor
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. Ah this is a good point; implicitly it would have to be no since the attachments are referenced/looked-up by type.
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. I feel like that is going to be something that people will want.
Contributor
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. Hmm, in any situation where someone would want to have two different attachments of type
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. my prediction is that most attachments will probably want to support multiple per resource, and I think it would just be better for the get attachments method to just return an array of attachments of the requested type, even if it is only a one-element array. I think requiring there be a wrapper type around multiple attachments of the same type just makes things more difficult, because then if you want to find a specific attachment for a resource, you can't just request its type any more. you also have to know the type of the wrapper
Contributor
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. Hmm, I think I am perhaps not following; can you give me an example of where resources would want to support multiple copies of the same attachment? Keep in mind that attachments cannot be removed from resources without destroying them; i.e. they are not objects in and of themselves. In this iteration of the proposal, for example, the This paradigm also allows for more flexibility in the implementation of
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. okay, that makes more sense |
||
| as the attachment being removed cannot be referenced as a value. In order to typecheck, if `t` is the name of some attachment type `T2`, `e` must have some composite type `T1` | ||
| such that `T1` is the intended base type for `T2`. Before the expression executes, `T2`'s `destroy` method (if present) will be executed. After the expression executes, the composite denoted by `e` will no longer contain the attachment `T1`. If the value denoted by `e` does not contain `t`, this statement is a no-op. | ||
|
|
||
| Attachments may be removed from a type in any order, so users should take care not to design any attachments that rely on specific behaviors of other attachments, as there is no | ||
| way in this proposal to require that an attachment depend on another or to require that a type has a given attachment when another attachment is present. | ||
|
|
||
| ### Accessing Attachments | ||
|
|
||
| Once an attachment has been added to a resource, it can be accessed using the `getAttachment` method, which is implicitly present on all resources, with the | ||
| following signature: | ||
|
|
||
| ```cadence | ||
| fun getAttachment<T: AnyAttachment>(): &T? | ||
| ``` | ||
|
|
||
| See the below section for a description of the new `AnyAttachment` type. `getAttachment` will query the resource for an attachment with that type, | ||
| returning a reference to it if it is present, while returning `nil` otherwise. So, given a resource `r` with an attachment of type `A`, accessing `A`'s `foo` method | ||
| would be done like so: | ||
|
|
||
| ```cadence | ||
| r.getAttachment<A>()!.foo() | ||
| ``` | ||
|
|
||
| ### Iterating over Attachments | ||
|
|
||
| All resource types will contain a new function `forEachAttachment` that iterates over all the attachments present on that resource, with the following signature: | ||
|
|
||
| ```cadence | ||
| fun forEachAttachment(_ f: ((AnyAttachment): Void)): Void | ||
| ``` | ||
|
|
||
| `AnyAttachment` is a new type that expresses the supertype of all attachments, | ||
| and contains two methods (that are implicitly present on all attachments): `getField` and `getMethod`, | ||
| with the following signatures: | ||
|
|
||
| ```cadence | ||
| getField<T: Any>(_ name: String): &T? | ||
| getMethod<T: Any>(_ name: String): T? | ||
| ``` | ||
|
|
||
| This functions takes the `name` of a member on an attachment and checks whether a member with that `name` exists on the attachment with the provided type argument. If it does, | ||
| `getField` will return a reference to that member is it is a field, but `nil` if it is a method, while `getMethod` will return that function if it is a method but `nil` if it is a field. If the type does not match or the member is not present, then both will return nil. These functions must be separate in order to support resource fields on attachments; | ||
| `getField` must return a reference to prevent duplicating any resource fields, while `getMethod` cannot return a reference because references to functions cannot be called. | ||
|
|
||
| So, for example, if the creator of a resource would like to have a method that returns the a descriptive string describing that resource and all its attachments, | ||
| they may implement it this way: | ||
|
|
||
| ``` | ||
| pub resource R { | ||
| ... | ||
| priv let descriptionString: String | ||
| pub fun description(): String { | ||
| let description = descriptionString | ||
| self.forEachAttachment(f: fun ((attachment: AnyAttachment): Void { | ||
| let descriptionFunction = attachment.getMethod<(():String)>("description") | ||
| if descriptionFunction != nil { | ||
| description.concat(descriptionFunction!()) | ||
| } | ||
| })) | ||
| } | ||
| } | ||
|
|
||
| ### Drawbacks | ||
|
|
||
| Adding a new language feature has the downside of complexity: users have to learn yet another | ||
| concept, and it also complicates the language implementation. | ||
|
|
||
| However, this language feature can be disclosed progressively, users can discover and use it when needed, | ||
| it is not necessary to be understood for core use-cases of the language, i.e. the target audience is mostly “power-users”. | ||
|
|
||
| ### Tutorials and Examples | ||
|
|
||
| * TODO: fill in later once the details of the design are decided | ||
|
|
||
| ### Compatibility | ||
|
|
||
| This is backwards compatible, as it does not invalidate any existing Cadence code. | ||
|
|
||
| ## Related Issues | ||
|
|
||
| * The previous extensions proposal (see the Alternatives section below) had support for extensions/attachments | ||
| implementing interfaces. Is this a valuable feature to have, or is it not necessary? Support for it could be added | ||
| in the future, but would require separate discussion. | ||
|
|
||
| * This proposal does not include a static type to express the type of a resource with attachments, despite this existing | ||
| in the previous proposal. This could be added later, along with a method to get a non-optional reference to an attachment | ||
| on resources that we can statically guarantee have that attachment. | ||
|
|
||
| ## Alternatives | ||
|
|
||
| In a previous [FLIP](https://github.com/onflow/flow/pull/1101), a solution to the extensibility | ||
| problems was proposed that suggested adding extensions to Cadence a la Swift or Scala. In this proposal, | ||
| extending a base type would create a subtype of that original type with the fields and methods of the | ||
| extension added to it, and had strong static typing guarantees at the expense of strict rules about | ||
| field/method name conflicts. The feedback about this proposal (summarized in comments on that FLIP as | ||
| well as in the [notes](https://github.com/onflow/cadence/blob/master/meetings/2022-09-20-Extensions.md) | ||
| from a meeting about it raised a number of concerns about this | ||
| design; in particular concerns about how name conflicts would be handled if contracts are updated, | ||
| how metadata for resources with extensions would be displayed, and the limitations posed by having | ||
| extended types be substitutable for their base types. This FLIP is intended to be an alternative to that | ||
| FLIP that has better handling for these specific issues, and contains much of the language and details of that | ||
| proposal, with some fundamental changes that result in a different system. | ||
|
|
||
| ## Prior Art | ||
|
|
||
| This has the same prior art mentioned in https://github.com/onflow/flow/pull/1101, although the style of | ||
| inspecting attachment methods and types is inspired by runtime reflection in langauges like Java or JavaScript. | ||
|
|
||
| ## Questions and Discussion Topics | ||
|
|
||
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.
This initializer uses
super- is this legal?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.
Yea; the initializer is run when the attachment is added, so super is safe to use here.