Hello, Cadence Community!
We would like to add support for extending existing types with additional data and functionality, retro-actively, i.e. without modifying the original declaration.
This was requested in the following GitHub issue: https://github.com/onflow/cadence/issues/357.
The goal of this topic is to work out a pitch and eventually concrete proposal for how to achieve this in Cadence.This could be for example in the form of a Flow Improvement Proposal (template), and/or a Cadence RFC.
We can use this topic to collect e.g. prior art, use-cases, answer open questions, and work together on the design proposal.
Once we have a proposal, we can move on to work on the implementation of it (potentially starting with an MVP/subset).
I’ll update the remainder with the latest state of the proposal.
Summary
This proposal describes a new language feature: Support for extending existing types with additional data and functionality, retro-actively, i.e. 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 did explicitly make provisions for future extensions.
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.
Use cases
- Adding hats, apparel and accessories to CryptoKitties
- Storing e.g. a signature or edition information for a given NFT, e.g. adding autographs to NBA TopShot moments
- Paying the original creator of an NFT a royalty on resale
- Storing ownership trail information (a NFT owned by somebody might be more valuable? Lets say I own a TopShot moment of Zion that he has owned himself?)
- Add utility functionality, e.g. export a HTML representation of an NFT
Prior art
- Extension methods in C#
- Extension methods in Scala/Dotty
- Extensions in Kotlin
- Extensions in Swift
- EIP-998: ERC-998 Composable Non-Fungible Token Standard
Design
This proposal specifies the ability to extend resources with additional functions. Other extensions are out-of-scope and could be proposed in the future. See the “Future / Out-of-scope” section below.
Declaration
Extension function declarations are able to refer to self
, just like original function declarations. This makes them natural to declare and to read.
Extension function declarations have the same access to fields and functions as code outside the original type declaration, i.e. they should be able perform everything that is possible externally, but also not more. This ensures that an extension cannot exploit the extended type by extending original functionality. As a result, an extension function declaration is not able to access private fields or function of the extended type.
Extension function declarations are not able to override existing function declarations. For example, if a resource declares a function named use
, extensions may not declare a function named use
, even if it has a different signature (different parameters and/or different return type).
Syntax
TBD
Use
The owner of a resource may freely add and remove extensions.
Extensions have an order, the order in which the extensions where added and removed from the extended object. This allows distinguish usage patterns. For example, an art piece might be first framed, and then signed; or it could be first signed and then framed.
TBD
Syntax
TBD
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”.
Future / Out-of-scope
Kinds
This proposal only specifies the extension of resources with additional functions.
Other extensions are out-of-scope and could be proposed in the future:
- Extension of other kinds of types, such as structs, contracts, events
- Extension of interfaces
- Providing default implementations of interfaces
- Extension of types to conform to more interfaces
Unremovable extensions
Some extension should not be removable. A future proposal may restrict certain extensions to be removed from an extended object.
Composability
Some code authors may declare up-front that the type they declare should always have an extension, i.e. they want to re-use code that has been written as an extension.
A future proposal may define how this could be achieved.
Open questions
Declaration
-
Extension functions:
-
Can extensions be referred to, i.e. do they have a name?
-
Can and if so how are nested types extended, e.g. a resource declared in a contract?
-
What is the syntax for extension function declarations?
-
How are declaration conflicts handled? For example, if two accounts both declare a function with the same name, is it possible to import both, and if so how?
-
-
Should it be possible to declare additional fields? If so
-
How are the extension’s fields initialized, and if they are resource-typed, how are they destroyed?
- Are extensions able to extend the destructor of resources with arbitrary code?
- This could prevent resource owners from destroying the extended resource
- Are extensions able to extend the destructor of resources with arbitrary code?
-
How and where are the values in the fields stored?
-
Usage
-
How can extension be used and what is the syntax for it?
-
Approaches:
- Import
- Mixin (like Scala):
- e.g.
R with 0x1.R with 0x2.R
- order important
- super calls, in order
- how can conflicts be handled? see below
- e.g.
-
How are declaration conflicts handled? For example, if two accounts both declare a function with the same name, how can both be used?
- Approaches:
- Renaming in import statements? In Kotlin (?) and in C#
- Mixins: How?
- Approaches:
-
How are extensions resolved/dispatched? (See “How do extensions interact with interfaces?”). Statically (like in Kotlin and Swift), or dynamically?
-
Is the extension effective implicitly or explicitly,
i.e. does the developer need to explicitly specify that an extension is effective for an object,
or is the extension implicitly effective after an import of code that declares an extension?
-
-
If extensions can declare additional fields, how are they initialized, i.e if the extension would declare an additional initializer for those extension’s fields, how is the initializer called?
-
What is the subtyping relation for extension types? When type
T
is extended withU
and thenV
, is it a subtype of a typeT + V + U
? (note the difference in order)
Other
-
How do extensions interact with interfaces? (See “How are extensions resolved/dispatched?”)
-
Can this proposal be added in a backwards-compatible way? Does existing code or stored data need to be migrated?