From 0cd8f46cb98985dbeb384258a3919a733c7bd520 Mon Sep 17 00:00:00 2001 From: salaheldinsoliman <49910731+salaheldinsoliman@users.noreply.github.com> Date: Tue, 12 Nov 2024 17:13:23 +0100 Subject: [PATCH] feat(docs): add generics section to the docs (#3949) * feat(docs): add generics section to the docs Signed-off-by: salaheldinsoliman * feat(docs): import generics and abilities from the Move Ref Signed-off-by: salaheldinsoliman --------- Signed-off-by: salaheldinsoliman --- .../iota-101/move-overview/generics.mdx | 116 +++++ docs/content/references/move/abilities.mdx | 240 +++++++++ docs/content/references/move/generics.mdx | 482 ++++++++++++++++++ docs/content/sidebars/developer.js | 1 + docs/content/sidebars/references.js | 2 + .../examples/move/move-overview/generics.move | 153 ++++++ 6 files changed, 994 insertions(+) create mode 100644 docs/content/developer/iota-101/move-overview/generics.mdx create mode 100644 docs/content/references/move/abilities.mdx create mode 100644 docs/content/references/move/generics.mdx create mode 100644 docs/examples/move/move-overview/generics.move diff --git a/docs/content/developer/iota-101/move-overview/generics.mdx b/docs/content/developer/iota-101/move-overview/generics.mdx new file mode 100644 index 00000000000..54c0c26c46f --- /dev/null +++ b/docs/content/developer/iota-101/move-overview/generics.mdx @@ -0,0 +1,116 @@ +# Generics + +Generics are a way to define a type or function that can work with any type. This is useful when you +want to write a function which can be used with different types, or when you want to define a type +that can hold any other type. Generics are the foundation of many advanced features in Move, such as +collections, abstract implementations, and more. + +## In the Standard Library + +An example of a generic type is the [vector](../../../references/framework/move-stdlib/vector.mdx) type, which is a container type that +can hold any other type. Another example of a generic type in the standard library is the +[Option](../../../references/framework/move-stdlib/option.mdx) type, which is used to represent a value that may or may not be present. + +## Generic Syntax + +To define a generic type or function, a type signature needs to have a list of generic parameters +enclosed in angle brackets (`<` and `>`). The generic parameters are separated by commas. + +```move file=/docs/examples/move/move-overview/generics.move#L8-L16 +``` + +In the example above, `Container` is a generic type with a single type parameter `T`, the `value` +field of the container stores the `T`. The `new` function is a generic function with a single type +parameter `T`, and it returns a `Container` with the given value. Generic types must be initialed +with a concrete type, and generic functions must be called with a concrete type. + +```move file=/docs/examples/move/move-overview/generics.move#L20-L31 +``` + +In the test function `test_generic` we demonstrate three equivalent ways to create a new `Container` +with a `u8` value. Because numeric types need to be inferred, we specify the type of the number +literal. + +## Multiple Type Parameters + +You can define a type or function with multiple type parameters. The type parameters are then +separated by commas. + +```move file=/docs/examples/move/move-overview/generics.move#L35-L44 +``` + +In the example above, `Pair` is a generic type with two type parameters `T` and `U`, and the +`new_pair` function is a generic function with two type parameters `T` and `U`. The function returns +a `Pair` with the given values. The order of the type parameters is important, and it should match +the order of the type parameters in the type signature. + +```move file=/docs/examples/move/move-overview/generics.move#L48-L63 +``` + +If we added another instance where we swapped type parameters in the `new_pair` function, and tried +to compare two types, we'd see that the type signatures are different, and cannot be compared. + +```move file=/docs/examples/move/move-overview/generics.move#L67-L80 +``` + +Types for variables `pair1` and `pair2` are different, and the comparison will not compile. + +## Why Generics? + +In the examples above we focused on instantiating generic types and calling generic functions to +create instances of these types. However, the real power of generics is the ability to define shared +behavior for the base, generic type, and then use it independently of the concrete types. This is +especially useful when working with collections, abstract implementations, and other advanced +features in Move. + +```move file=/docs/examples/move/move-overview/generics.move#L86-L92 +``` + +In the example above, `User` is a generic type with a single type parameter `T`, with shared fields +`name` and `age`, and the generic `metadata` field which can store any type. No matter what the +`metadata` is, all of the instances of `User` will have the same fields and methods. + +```move file=/docs/examples/move/move-overview/generics.move#L96-L104 +``` + +## Phantom Type Parameters + +In some cases, you may want to define a generic type with a type parameter that is not used in the +fields or methods of the type. This is called a _phantom type parameter_. Phantom type parameters +are useful when you want to define a type that can hold any other type, but you want to enforce some +constraints on the type parameter. + +```move file=/docs/examples/move/move-overview/generics.move#L108-L111 +``` + +The `Coin` type here does not contain any fields or methods that use the type parameter `T`. It is +used to differentiate between different types of coins, and to enforce some constraints on the type +parameter `T`. + +```move file=/docs/examples/move/move-overview/generics.move#L115-L126 +``` + +In the example above, we demonstrate how to create two different instances of `Coin` with different +phantom type parameters `USD` and `EUR`. The type parameter `T` is not used in the fields or methods +of the `Coin` type, but it is used to differentiate between different types of coins. It will make +sure that the `USD` and `EUR` coins are not mixed up. + +## Constraints on Type Parameters + +Type parameters can be constrained to have certain abilities. This is useful when you need the inner +type to allow certain behavior, such as _copy_ or _drop_. The syntax for constraining a type +parameter is `T: + `. + +```move file=/docs/examples/move/move-overview/generics.move#L130-L138 +``` + +Move Compiler will enforce that the type parameter `T` has the specified abilities. If the type +parameter does not have the specified abilities, the code will not compile. + + +```move file=/docs/examples/move/move-overview/generics.move#L142-L152 +``` + +## Further Reading + +- [Generics](../../../references/move/generics.mdx) in the Move Reference. \ No newline at end of file diff --git a/docs/content/references/move/abilities.mdx b/docs/content/references/move/abilities.mdx new file mode 100644 index 00000000000..1f34d29be8c --- /dev/null +++ b/docs/content/references/move/abilities.mdx @@ -0,0 +1,240 @@ +# Abilities + +Abilities are a typing feature in Move that control what actions are permissible for values of a given type. This system grants fine grained control over the "linear" typing behavior of values, as well as if and how values are used in global storage. This is implemented by gating access to certain bytecode instructions so that for a value to be used with the bytecode instruction, it must have the ability required (if one is required at all—not every instruction is gated by an ability). + +## The Four Abilities + +The four abilities are: + +* [`copy`](#copy) + * Allows values of types with this ability to be copied. +* [`drop`](#drop) + * Allows values of types with this ability to be popped/dropped. +* [`store`](#store) + * Allows values of types with this ability to exist inside a struct in global storage. +* [`key`](#key) + * Allows the type to serve as a key for global storage operations. + +### `copy` + +The `copy` ability allows values of types with that ability to be copied. It gates the ability to copy values out of local variables with the `copy` operator and to copy values via references with dereference `*e`. + +If a value has `copy`, all values contained inside of that value have `copy`. + +### `drop` + +The `drop` ability allows values of types with that ability to be dropped. By dropped, we mean that value is not transferred and is effectively destroyed as the Move program executes. As such, this ability gates the ability to ignore values in a multitude of locations, including: +* not using the value in a local variable or parameter +* not using the value in a sequence via `;` +* overwriting values in variables in assignments +* overwriting values via references when writing `*e1 = e2`. + +If a value has `drop`, all values contained inside of that value have `drop`. + +### `store` + +The `store` ability allows values of types with this ability to exist inside of a struct (resource) in global storage, *but* not necessarily as a top-level resource in global storage. This is the only ability that does not directly gate an operation. Instead it gates the existence in global storage when used in tandem with `key`. + +If a value has `store`, all values contained inside of that value have `store` + +### `key` + +The `key` ability allows the type to serve as a key for global storage operations. It gates all global storage operations, so in order for a type to be used with `move_to`, `borrow_global`, `move_from`, etc., the type must have the `key` ability. Note that the operations still must be used in the module where the `key` type is defined (in a sense, the operations are private to the defining module). + +If a value has `key`, all values contained inside of that value have `store`. This is the only ability with this sort of asymmetry. + +## Builtin Types + +Most primitive, builtin types have `copy`, `drop`, and `store` with the exception of `signer`, which just has `drop` + +* `bool`, `u8`, `u16`, `u32`, `u64`, `u128`, `u256`, and `address` all have `copy`, `drop`, and `store`. +* `signer` has `drop` + * Cannot be copied and cannot be put into global storage +* `vector` may have `copy`, `drop`, and `store` depending on the abilities of `T`. + * See [Conditional Abilities and Generic Types](#conditional-abilities-and-generic-types) for more details. +* Immutable references `&` and mutable references `&mut` both have `copy` and `drop`. + * This refers to copying and dropping the reference itself, not what they refer to. + * References cannot appear in global storage, hence they do not have `store`. + +None of the primitive types have `key`, meaning none of them can be used directly with the global storage operations. + +## Annotating Structs + +To declare that a `struct` has an ability, it is declared with `has ` after the struct name but before the fields. For example: + +```move +struct Ignorable has drop { f: u64 } +struct Pair has copy, drop, store { x: u64, y: u64 } +``` + +In this case: `Ignorable` has the `drop` ability. `Pair` has `copy`, `drop`, and `store`. + + +All of these abilities have strong guarantees over these gated operations. The operation can be performed on the value only if it has that ability; even if the value is deeply nested inside of some other collection! + +As such: when declaring a struct’s abilities, certain requirements are placed on the fields. All fields must satisfy these constraints. These rules are necessary so that structs satisfy the reachability rules for the abilities given above. If a struct is declared with the ability... + +* `copy`, all fields must have `copy`. +* `drop`, all fields must have `drop`. +* `store`, all fields must have `store`. +* `key`, all fields must have `store`. + * `key` is the only ability currently that doesn’t require itself. + +For example: + +```move +// A struct without any abilities +struct NoAbilities {} + +struct WantsCopy has copy { + f: NoAbilities, // ERROR 'NoAbilities' does not have 'copy' +} +``` + +and similarly: + +```move +// A struct without any abilities +struct NoAbilities {} + +struct MyResource has key { + f: NoAbilities, // Error 'NoAbilities' does not have 'store' +} +``` + +## Conditional Abilities and Generic Types + +When abilities are annotated on a generic type, not all instances of that type are guaranteed to have that ability. Consider this struct declaration: + +``` +struct Cup has copy, drop, store, key { item: T } +``` + +It might be very helpful if `Cup` could hold any type, regardless of its abilities. The type system can *see* the type parameter, so it should be able to remove abilities from `Cup` if it *sees* a type parameter that would violate the guarantees for that ability. + +This behavior might sound a bit confusing at first, but it might be more understandable if we think about collection types. We could consider the builtin type `vector` to have the following type declaration: + +``` +vector has copy, drop, store; +``` + +We want `vector`s to work with any type. We don't want separate `vector` types for different abilities. So what are the rules we would want? Precisely the same that we would want with the field rules above. So, it would be safe to copy a `vector` value only if the inner elements can be copied. It would be safe to ignore a `vector` value only if the inner elements can be ignored/dropped. And, it would be safe to put a `vector` in global storage only if the inner elements can be in global storage. + +To have this extra expressiveness, a type might not have all the abilities it was declared with depending on the instantiation of that type; instead, the abilities a type will have depends on both its declaration **and** its type arguments. For any type, type parameters are pessimistically assumed to be used inside of the struct, so the abilities are only granted if the type parameters meet the requirements described above for fields. Taking `Cup` from above as an example: + +* `Cup` has the ability `copy` only if `T` has `copy`. +* It has `drop` only if `T` has `drop`. +* It has `store` only if `T` has `store`. +* It has `key` only if `T` has `store`. + +Here are examples for this conditional system for each ability: + +### Example: conditional `copy` + +```move +struct NoAbilities {} +struct S has copy, drop { f: bool } +struct Cup has copy, drop, store { item: T } + +fun example(c_x: Cup, c_s: Cup) { + // Valid, 'Cup' has 'copy' because 'u64' has 'copy' + let c_x2 = copy c_x; + // Valid, 'Cup' has 'copy' because 'S' has 'copy' + let c_s2 = copy c_s; +} + +fun invalid(c_account: Cup, c_n: Cup) { + // Invalid, 'Cup' does not have 'copy'. + // Even though 'Cup' was declared with copy, the instance does not have 'copy' + // because 'signer' does not have 'copy' + let c_account2 = copy c_account; + // Invalid, 'Cup' does not have 'copy' + // because 'NoAbilities' does not have 'copy' + let c_n2 = copy c_n; +} +``` + +### Example: conditional `drop` + +```move +struct NoAbilities {} +struct S has copy, drop { f: bool } +struct Cup has copy, drop, store { item: T } + +fun unused() { + Cup { item: true }; // Valid, 'Cup' has 'drop' + Cup { item: S { f: false }}; // Valid, 'Cup' has 'drop' +} + +fun left_in_local(c_account: Cup): u64 { + let c_b = Cup { item: true }; + let c_s = Cup { item: S { f: false }}; + // Valid return: 'c_account', 'c_b', and 'c_s' have values + // but 'Cup', 'Cup', and 'Cup' have 'drop' + 0 +} + +fun invalid_unused() { + // Invalid, Cannot ignore 'Cup' because it does not have 'drop'. + // Even though 'Cup' was declared with 'drop', the instance does not have 'drop' + // because 'NoAbilities' does not have 'drop' + Cup { item: NoAbilities {}}; +} + +fun invalid_left_in_local(): u64 { + let n = Cup { item: NoAbilities {}}; + // Invalid return: 'n' has a value + // and 'Cup' does not have 'drop' + 0 +} +``` + +### Example: conditional `store` + +```move +struct Cup has copy, drop, store { item: T } + +// 'MyInnerResource' is declared with 'store' so all fields need 'store' +struct MyInnerResource has store { + yes: Cup, // Valid, 'Cup' has 'store' + // no: Cup, Invalid, 'Cup' does not have 'store' +} + +// 'MyResource' is declared with 'key' so all fields need 'store' +struct MyResource has key { + yes: Cup, // Valid, 'Cup' has 'store' + inner: Cup, // Valid, 'Cup' has 'store' + // no: Cup, Invalid, 'Cup' does not have 'store' +} +``` + +### Example: conditional `key` + +```move +struct NoAbilities {} +struct MyResource has key { f: T } + +fun valid(account: &signer) acquires MyResource { + let addr = signer::address_of(account); + // Valid, 'MyResource' has 'key' + let has_resource = exists>(addr); + if (!has_resource) { + // Valid, 'MyResource' has 'key' + move_to(account, MyResource { f: 0 }) + }; + // Valid, 'MyResource' has 'key' + let r = borrow_global_mut>(addr) + r.f = r.f + 1; +} + +fun invalid(account: &signer) { + // Invalid, 'MyResource' does not have 'key' + let has_it = exists>(addr); + // Invalid, 'MyResource' does not have 'key' + let NoAbilities {} = move_from(addr); + // Invalid, 'MyResource' does not have 'key' + move_to(account, NoAbilities {}); + // Invalid, 'MyResource' does not have 'key' + borrow_global(addr); +} +``` \ No newline at end of file diff --git a/docs/content/references/move/generics.mdx b/docs/content/references/move/generics.mdx new file mode 100644 index 00000000000..d2ea5e7dd89 --- /dev/null +++ b/docs/content/references/move/generics.mdx @@ -0,0 +1,482 @@ +# Generics + +Generics can be used to define functions and structs over different input data types. This language feature is sometimes referred to as *parametric polymorphism*. In Move, we will often use the term generics interchangeably with type parameters and type arguments. + +Generics are commonly used in library code, such as in vector, to declare code that works over any possible instantiation (that satisfies the specified constraints). In other frameworks, generic code can sometimes be used to interact with global storage many different ways that all still share the same implementation. + +## Declaring Type Parameters + +Both functions and structs can take a list of type parameters in their signatures, enclosed by a pair of angle brackets `<...>`. + +### Generic Functions + +Type parameters for functions are placed after the function name and before the (value) parameter list. The following code defines a generic identity function that takes a value of any type and returns that value unchanged. + +```move +fun id(x: T): T { + // this type annotation is unnecessary but valid + (x: T) +} +``` + +Once defined, the type parameter `T` can be used in parameter types, return types, and inside the function body. + +### Generic Structs + +Type parameters for structs are placed after the struct name, and can be used to name the types of the fields. + +```move +struct Foo has copy, drop { x: T } + +struct Bar has copy, drop { + x: T1, + y: vector, +} +``` + +Note that [type parameters do not have to be used](#unused-type-parameters) + +## Type Arguments + +### Calling Generic Functions + +When calling a generic function, one can specify the type arguments for the function's type parameters in a list enclosed by a pair of angle brackets. + +```move +fun foo() { + let x = id(true); +} +``` + +If you do not specify the type arguments, Move's [type inference](#type-inference) will supply them for you. + +### Using Generic Structs + +Similarly, one can attach a list of type arguments for the struct's type parameters when constructing or destructing values of generic types. + +```move +fun foo() { + let foo = Foo { x: true }; + let Foo { x } = foo; +} +``` + +If you do not specify the type arguments, Move's [type inference](#type-inference) will supply them for you. + +### Type Argument Mismatch + +If you specify the type arguments and they conflict with the actual values supplied, an error will be given: + +```move +fun foo() { + let x = id(true); // error! true is not a u64 +} +``` + +and similarly: + +```move +fun foo() { + let foo = Foo { x: 0 }; // error! 0 is not a bool + let Foo
{ x } = foo; // error! bool is incompatible with address +} +``` + +## Type Inference + +In most cases, the Move compiler will be able to infer the type arguments so you don't have to write them down explicitly. Here's what the examples above would look like if we omit the type arguments: + +```move +fun foo() { + let x = id(true); + // ^ is inferred + + let foo = Foo { x: true }; + // ^ is inferred + + let Foo { x } = foo; + // ^ is inferred +} +``` + +Note: when the compiler is unable to infer the types, you'll need annotate them manually. A common scenario is to call a function with type parameters appearing only at return positions. + +```move +address 0x2 { +module m { + using std::vector; + + fun foo() { + // let v = vector::new(); + // ^ The compiler cannot figure out the element type. + + let v = vector::new(); + // ^~~~~ Must annotate manually. + } +} +} +``` + +However, the compiler will be able to infer the type if that return value is used later in that function: + +```move +address 0x2 { +module m { + using std::vector; + + fun foo() { + let v = vector::new(); + // ^ is inferred + vector::push_back(&mut v, 42); + } +} +} +``` + +## Unused Type Parameters + +For a struct definition, +an unused type parameter is one that +does not appear in any field defined in the struct, +but is checked statically at compile time. +Move allows unused type parameters so the following struct definition is valid: + +```move +struct Foo { + foo: u64 +} +``` + +This can be convenient when modeling certain concepts. Here is an example: + +```move +address 0x2 { +module m { + // Currency Specifiers + struct Currency1 {} + struct Currency2 {} + + // A generic coin type that can be instantiated using a currency + // specifier type. + // e.g. Coin, Coin etc. + struct Coin has store { + value: u64 + } + + // Write code generically about all currencies + public fun mint_generic(value: u64): Coin { + Coin { value } + } + + // Write code concretely about one currency + public fun mint_concrete(value: u64): Coin { + Coin { value } + } +} +} +``` + +In this example, +`struct Coin` is generic on the `Currency` type parameter, +which specifies the currency of the coin and +allows code to be written either +generically on any currency or +concretely on a specific currency. +This genericity applies even when the `Currency` type parameter +does not appear in any of the fields defined in `Coin`. + +### Phantom Type Parameters + +In the example above, +although `struct Coin` asks for the `store` ability, +neither `Coin` nor `Coin` will have the `store` ability. +This is because of the rules for +[Conditional Abilities and Generic Types](./abilities.mdx#conditional-abilities-and-generic-types) +and the fact that `Currency1` and `Currency2` don't have the `store` ability, +despite the fact that they are not even used in the body of `struct Coin`. +This might cause some unpleasant consequences. +For example, we are unable to put `Coin` into a wallet in the global storage. + +One possible solution would be to +add spurious ability annotations to `Currency1` and `Currency2` +(i.e., `struct Currency1 has store {}`). +But, this might lead to bugs or security vulnerabilities +because it weakens the types with unnecessary ability declarations. +For example, we would never expect a resource in the global storage to have a field in type `Currency1`, +but this would be possible with the spurious `store` ability. +Moreover, the spurious annotations would be infectious, +requiring many functions generic on the unused type parameter to also include the necessary constraints. + +Phantom type parameters solve this problem. +Unused type parameters can be marked as *phantom* type parameters, +which do not participate in the ability derivation for structs. +In this way, +arguments to phantom type parameters are not considered when deriving the abilities for generic types, +thus avoiding the need for spurious ability annotations. +For this relaxed rule to be sound, +Move's type system guarantees that a parameter declared as `phantom` is either +not used at all in the struct definition, or +it is only used as an argument to type parameters also declared as `phantom`. + +#### Declaration + +In a struct definition +a type parameter can be declared as phantom by adding the `phantom` keyword before its declaration. +If a type parameter is declared as phantom we say it is a phantom type parameter. +When defining a struct, Move's type checker ensures that every phantom type parameter is either +not used inside the struct definition or +it is only used as an argument to a phantom type parameter. + +More formally, +if a type is used as an argument to a phantom type parameter +we say the type appears in _phantom position_. +With this definition in place, +the rule for the correct use of phantom parameters can be specified as follows: +**A phantom type parameter can only appear in phantom position**. + +The following two examples show valid uses of phantom parameters. +In the first one, +the parameter `T1` is not used at all inside the struct definition. +In the second one, the parameter `T1` is only used as an argument to a phantom type parameter. + +```move +struct S1 { f: u64 } + ^^ + Ok: T1 does not appear inside the struct definition + + +struct S2 { f: S1 } + ^^ + Ok: T1 appears in phantom position +``` + +The following code shows examples of violations of the rule: + +```move +struct S1 { f: T } + ^ + Error: Not a phantom position + +struct S2 { f: T } + +struct S3 { f: S2 } + ^ + Error: Not a phantom position +``` + +#### Instantiation + +When instantiating a struct, +the arguments to phantom parameters are excluded when deriving the struct abilities. +For example, consider the following code: + +```move +struct S has copy { f: T1 } +struct NoCopy {} +struct HasCopy has copy {} +``` + +Consider now the type `S`. +Since `S` is defined with `copy` and all non-phantom arguments have `copy` +then `S` also has `copy`. + +#### Phantom Type Parameters with Ability Constraints + +Ability constraints and phantom type parameters are orthogonal features in the sense that +phantom parameters can be declared with ability constraints. +When instantiating a phantom type parameter with an ability constraint, +the type argument has to satisfy that constraint, +even though the parameter is phantom. +For example, the following definition is perfectly valid: + +```move +struct S {} +``` + +The usual restrictions apply and `T` can only be instantiated with arguments having `copy`. + +## Constraints + +In the examples above, we have demonstrated how one can use type parameters to define "unknown" types that can be plugged in by callers at a later time. This however means the type system has little information about the type and has to perform checks in a very conservative way. In some sense, the type system must assume the worst case scenario for an unconstrained generic. Simply put, by default generic type parameters have no [abilities](./abilities.mdx). + +This is where constraints come into play: they offer a way to specify what properties these unknown types have so the type system can allow operations that would otherwise be unsafe. + +### Declaring Constraints + +Constraints can be imposed on type parameters using the following syntax. + +```move +// T is the name of the type parameter +T: (+ )* +``` + +The `` can be any of the four [abilities](./abilities.mdx), and a type parameter can be constrained with multiple abilities at once. So all of the following would be valid type parameter declarations: + +```move +T: copy +T: copy + drop +T: copy + drop + store + key +``` + +### Verifying Constraints + +Constraints are checked at call sites so the following code won't compile. + +```move +struct Foo { x: T } + +struct Bar { x: Foo } +// ^ error! u8 does not have 'key' + +struct Baz { x: Foo } +// ^ error! T does not have 'key' +``` + +```move +struct R {} + +fun unsafe_consume(x: T) { + // error! x does not have 'drop' +} + +fun consume(x: T) { + // valid! + // x will be dropped automatically +} + +fun foo() { + let r = R {}; + consume(r); + // ^ error! R does not have 'drop' +} +``` + +```move +struct R {} + +fun unsafe_double(x: T) { + (copy x, x) + // error! x does not have 'copy' +} + +fun double(x: T) { + (copy x, x) // valid! +} + +fun foo(): (R, R) { + let r = R {}; + double(r) + // ^ error! R does not have 'copy' +} +``` + +For more information, see the abilities section on [conditional abilities and generic types](./abilities.mdx#conditional-abilities-and-generic-types). + +## Limitations on Recursions + +### Recursive Structs + +Generic structs can not contain fields of the same type, either directly or indirectly, even with different type arguments. All of the following struct definitions are invalid: + +```move +struct Foo { + x: Foo // error! 'Foo' containing 'Foo' +} + +struct Bar { + x: Bar // error! 'Bar' containing 'Bar' +} + +// error! 'A' and 'B' forming a cycle, which is not allowed either. +struct A { + x: B +} + +struct B { + x: A + y: A +} +``` + +### Advanced Topic: Type-level Recursions + +Move allows generic functions to be called recursively. However, when used in combination with generic structs, this could create an infinite number of types in certain cases, and allowing this means adding unnecessary complexity to the compiler, vm and other language components. Therefore, such recursions are forbidden. + +Allowed: + +```move +address 0x2 { +module m { + struct A {} + + // Finitely many types -- allowed. + // foo -> foo -> foo -> ... is valid + fun foo() { + foo(); + } + + // Finitely many types -- allowed. + // foo -> foo> -> foo> -> ... is valid + fun foo() { + foo>(); + } +} +} +``` + +Not allowed: + +```move +address 0x2 { +module m { + struct A {} + + // Infinitely many types -- NOT allowed. + // error! + // foo -> foo> -> foo>> -> ... + fun foo() { + foo>(); + } +} +} +``` + +```move +address 0x2 { +module n { + struct A {} + + // Infinitely many types -- NOT allowed. + // error! + // foo -> bar -> foo> + // -> bar, T2> -> foo, A> + // -> bar, A> -> foo, A>> + // -> ... + fun foo() { + bar(); + } + + fun bar { + foo>(); + } +} +} +``` + +Note, the check for type level recursions is based on a conservative analysis on the call sites and does NOT take control flow or runtime values into account. + +```move +address 0x2 { +module m { + struct A {} + + fun foo(n: u64) { + if (n > 0) { + foo>(n - 1); + }; + } +} +} +``` + +The function in the example above will technically terminate for any given input and therefore only creating finitely many types, but it is still considered invalid by Move's type system. \ No newline at end of file diff --git a/docs/content/sidebars/developer.js b/docs/content/sidebars/developer.js index 9057eabce7c..7efdf0c6e2b 100644 --- a/docs/content/sidebars/developer.js +++ b/docs/content/sidebars/developer.js @@ -58,6 +58,7 @@ const developer = [ 'developer/iota-101/move-overview/package-upgrades/custom-policies', ], }, + 'developer/iota-101/move-overview/generics', { type: 'category', label: 'Patterns', diff --git a/docs/content/sidebars/references.js b/docs/content/sidebars/references.js index ef259f77162..a79d11e4980 100644 --- a/docs/content/sidebars/references.js +++ b/docs/content/sidebars/references.js @@ -223,6 +223,8 @@ const references = [ }, 'references/move/move-toml', 'references/move/move-lock', + 'references/move/abilities', + 'references/move/generics', { type: 'link', label: 'Move Language (GitHub)', diff --git a/docs/examples/move/move-overview/generics.move b/docs/examples/move/move-overview/generics.move new file mode 100644 index 00000000000..c0068862a39 --- /dev/null +++ b/docs/examples/move/move-overview/generics.move @@ -0,0 +1,153 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 +#[allow(unused_variable, unused_field)] +module book::generics; + +// ANCHOR: container +/// Container for any type `T`. +public struct Container has drop { + value: T, +} + +/// Function that creates a new `Container` with a generic value `T`. +public fun new(value: T): Container { + Container { value } +} +// ANCHOR_END: container + +// ANCHOR: test_container +#[test] +fun test_container() { + // these three lines are equivalent + let container: Container = new(10); // type inference + let container = new(10); // create a new `Container` with a `u8` value + let container = new(10u8); + + assert!(container.value == 10, 0x0); + + // Value can be ignored only if it has the `drop` ability. + let Container { value: _ } = container; +} +// ANCHOR_END: test_container + +// ANCHOR: pair +/// A pair of values of any type `T` and `U`. +public struct Pair { + first: T, + second: U, +} + +/// Function that creates a new `Pair` with two generic values `T` and `U`. +public fun new_pair(first: T, second: U): Pair { + Pair { first, second } +} +// ANCHOR_END: pair + +// ANCHOR: test_pair +#[test] +fun test_generic() { + // these three lines are equivalent + let pair_1: Pair = new_pair(10, true); // type inference + let pair_2 = new_pair(10, true); // create a new `Pair` with a `u8` and `bool` values + let pair_3 = new_pair(10u8, true); + + assert!(pair_1.first == 10, 0x0); + assert!(pair_1.second, 0x0); + + // Unpacking is identical. + let Pair { first: _, second: _ } = pair_1; + let Pair { first: _, second: _ } = pair_2; + let Pair { first: _, second: _ } = pair_3; + +} +// ANCHOR_END: test_pair + +// ANCHOR: test_pair_swap +#[test] +fun test_swap_type_params() { + let pair1: Pair = new_pair(10u8, true); + let pair2: Pair = new_pair(true, 10u8); + + // this line will not compile + // assert!(pair1 == pair2, 0x0); + + let Pair { first: pf1, second: ps1 } = pair1; // first1: u8, second1: bool + let Pair { first: pf2, second: ps2 } = pair2; // first2: bool, second2: u8 + + assert!(pf1 == ps2); // 10 == 10 + assert!(ps1 == pf2); // true == true +} +// ANCHOR_END: test_pair_swap + +use std::string::String; + +// ANCHOR: user +/// A user record with name, age, and some generic metadata +public struct User { + name: String, + age: u8, + /// Varies depending on application. + metadata: T, +} +// ANCHOR_END: user + +// ANCHOR: update_user +/// Updates the name of the user. +public fun update_name(user: &mut User, name: String) { + user.name = name; +} + +/// Updates the age of the user. +public fun update_age(user: &mut User, age: u8) { + user.age = age; +} +// ANCHOR_END: update_user + +// ANCHOR: phantom +/// A generic type with a phantom type parameter. +public struct Coin { + value: u64 +} +// ANCHOR_END: phantom + +// ANCHOR: test_phantom +public struct USD {} +public struct EUR {} + +#[test] +fun test_phantom_type() { + let coin1: Coin = Coin { value: 10 }; + let coin2: Coin = Coin { value: 20 }; + + // Unpacking is identical because the phantom type parameter is not used. + let Coin { value: _ } = coin1; + let Coin { value: _ } = coin2; +} +// ANCHOR_END: test_phantom + +// ANCHOR: constraints +/// A generic type with a type parameter that has the `drop` ability. +public struct Droppable { + value: T, +} + +/// A generic struct with a type parameter that has the `copy` and `drop` abilities. +public struct CopyableDroppable { + value: T, // T must have the `copy` and `drop` abilities +} +// ANCHOR_END: constraints + +// ANCHOR: test_constraints +/// Type without any abilities. +public struct NoAbilities {} + +#[test] +fun test_constraints() { + // Fails - `NoAbilities` does not have the `drop` ability + // let droppable = Droppable { value: 10 }; + + // Fails - `NoAbilities` does not have the `copy` and `drop` abilities + // let copyable_droppable = CopyableDroppable { value: 10 }; +} +// ANCHOR_END: test_constraints \ No newline at end of file