Skip to content
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

feat(docs): add generics section to the docs #3949

Merged
merged 3 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions docs/content/developer/iota-101/move-overview/generics.mdx
Original file line number Diff line number Diff line change
@@ -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=<rootDir>/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=<rootDir>/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=<rootDir>/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=<rootDir>/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=<rootDir>/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=<rootDir>/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=<rootDir>/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=<rootDir>/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=<rootDir>/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: <ability> + <ability>`.

```move file=<rootDir>/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=<rootDir>/docs/examples/move/move-overview/generics.move#L142-L152
```

## Further Reading

- [Generics](https://github.com/move-language/move/blob/main/language/documentation/book/src/generics.md) in the Move Reference.
1 change: 1 addition & 0 deletions docs/content/sidebars/developer.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const developer = [
'developer/iota-101/move-overview/package-upgrades/custom-policies',
],
},
'developer/iota-101/move-overview/generics',
{
type: 'category',
label: 'Patterns',
Expand Down
153 changes: 153 additions & 0 deletions docs/examples/move/move-overview/generics.move
Original file line number Diff line number Diff line change
@@ -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<T> has drop {
value: T,
}

/// Function that creates a new `Container` with a generic value `T`.
public fun new<T>(value: T): Container<T> {
Container { value }
}
// ANCHOR_END: container

// ANCHOR: test_container
#[test]
fun test_container() {
// these three lines are equivalent
let container: Container<u8> = new(10); // type inference
let container = new<u8>(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<T, U> {
first: T,
second: U,
}

/// Function that creates a new `Pair` with two generic values `T` and `U`.
public fun new_pair<T, U>(first: T, second: U): Pair<T, U> {
Pair { first, second }
}
// ANCHOR_END: pair

// ANCHOR: test_pair
#[test]
fun test_generic() {
// these three lines are equivalent
let pair_1: Pair<u8, bool> = new_pair(10, true); // type inference
let pair_2 = new_pair<u8, bool>(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<u8, bool> = new_pair(10u8, true);
let pair2: Pair<bool, u8> = 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<T> {
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<T>(user: &mut User<T>, name: String) {
user.name = name;
}

/// Updates the age of the user.
public fun update_age<T>(user: &mut User<T>, age: u8) {
user.age = age;
}
// ANCHOR_END: update_user

// ANCHOR: phantom
/// A generic type with a phantom type parameter.
public struct Coin<phantom T> {
value: u64
}
// ANCHOR_END: phantom

// ANCHOR: test_phantom
public struct USD {}
public struct EUR {}

#[test]
fun test_phantom_type() {
let coin1: Coin<USD> = Coin { value: 10 };
let coin2: Coin<EUR> = 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<T: drop> {
value: T,
}

/// A generic struct with a type parameter that has the `copy` and `drop` abilities.
public struct CopyableDroppable<T: copy + drop> {
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<NoAbilities> { value: 10 };

// Fails - `NoAbilities` does not have the `copy` and `drop` abilities
// let copyable_droppable = CopyableDroppable<NoAbilities> { value: 10 };
}
// ANCHOR_END: test_constraints
Loading