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

[Move 2024] Enums #15653

Open
tzakian opened this issue Jan 10, 2024 · 2 comments
Open

[Move 2024] Enums #15653

tzakian opened this issue Jan 10, 2024 · 2 comments
Labels
design devx move Type: Major Feature Major new functionality or integration, which has a significant impact on the network

Comments

@tzakian
Copy link
Contributor

tzakian commented Jan 10, 2024

Enumerations (or enums) allow programmers to define a single type that contains different shapes of data (or “variants”). A new syntax for declaring enums in Move and pattern matching on enum values will be introduced to the source language, and support for enums will be added to the bytecode and VM. This document will only look at the source syntax and usage of enums and not discuss the lower-level details.

See the main issue for more on Move 2024 changes

For example if we wanted an enum that allowed us to represent the current temperature in Fahrenheit, Celsius, or if the current temperature is unknown we could do so with the following enum

public enum Temperature {
   Fahrenheit(u16),
   Celsius { temp: u16 },
   Unknown
}

We could then write a function to tell is if the temperature is at or above boiling point using the new pattern-matching functionalities that will be introduced at the same time

public fun is_boiling(t: &Temperature): bool {
   match (t) {
     Temperature::Fahrenheit(temp) => *temp >= 212,
     Temperature::Celsius { temp } => *temp >= 100,
     Temperature::Unknown => false,
   }
}

Defining Enums

Move enums are made up of zero or more variants. Each variant can either contain no data (such as the Temperature::Unknown variant above), or additional data in variant fields. These fields can either be named (like the temp field in the Temperature::Celsius variant), or positional (like the Temperature::Fahrenheit variant). Enum variants, just like struct fields are private to the module in which the enum is declared.

Move enums can be generic and have abilities just like structs in Move. However, enums cannot have the key ability because there is no way of defining a common id: UID amongst the different variants of the enum. One important implication of this is that enums cannot be objects (but they can be stored in objects).

In the Move 2024 release, Move enums will not support recursive fields, but we intend on supporting them in the future. The following is an example of an enum that would not be allowed currently, but would in the future:

// Not allowed right now, but will be in the future.
public enum BTree<T> {
   Leaf,
   Node { data: T, left: BTree<T>, right: BTree<T> }
}

Some more examples of what enum declarations in Move will look like and will be allowed:

// Variants can have phantom type parameters
public enum EnumWithPhantom<phantom T> { 
  Variant(u64)
}

// An enum representing an action in a video game: 
// you can go left, right, jump, or stop.
public enum Action<NextAction> has copy, drop {
   Done,
   Left(NextAction),
   Right(NextAction),
   Jump { height: u16, then: NextAction }
}

// Note that while recursive enums are not currently allowed, you can
// instantiate a generic enum with another instantiation of that enum.
// 
// Bounded actions: Can represent any video game action up-to 3 moves.
// The inner-most type doesn't matter.
Action<Action<Action<bool>>>

Referencing and Constructing Enum Values

Variants are always referenced through the enum that defines them, although the type of the enum can be aliased. The following are all valid ways of referencing variants inside the Temperature enum above:

let x = Temperature::Unknown;

// Note: the reference to the module needs to be fully qualified!
use my_package::my_module::Temperature as T;
let y = T::Unknown;

We do not allow using specific variants, opening enums, or renaming variants. So the following would all be invalid

use Temperature::Celsius; // Invalid!
use Temperature::*; // Invalid!
use Temperature::Celsius as C; // Invalid!

You can create enum values using one of the enums variants in a similar way to the way you would create struct values

// Note: since the Celsius variant is defined using named fields you have to use
// a named field to construct that variant.
let c_boiling = Temperature::Celsius { temp: 100 };

// Since the Fahrenheit variant was defined with positional fields you have
// construct it positionally.
let f_boiling = Temperature::Fahrenheit(212);

Pattern Matching

We also plan on introducing basic pattern matching to allow easily working with and destructuring enum values. Pattern matches consist of a sequence of match statements. Each match statement has a left-hand-side consisting of a pattern an optional guard and a right-hand side. Pattern matching proceeds from the top-down, and matches the first statement that matches the value.

A pattern can consist of literals (e.g., 0, true), references to enum variants or structs (e.g., Temperature::Fahrenheit or SomeStruct, bindings (e.g., x, y), wildcards _, and or-patterns pattern | pattern (note: bindings must be the same across or-patterns).

Bindings in a pattern match anything in that (structural) position, and are bound both in the optional guard expression, and on the right hand side.

The guard is an expression that returns a boolean, it will be executed only if the pattern matches and before the right hand side is run. If it returns false the right-hand side will not be run, and other match statements below it will be checked to see if the value being pattern matched matches any of them.

To see some examples:

fun is_temperature_fahrenheit(t: &Temperature): bool {
   match (t) {
      Temperature::Fahrenheit(_) => true,
      _ => false,
   }
}

// Another way of writing is_boiling using guards and wildcards.
fun is_temperature_boiling(t: &Temperature): bool {
   match (t) {
      Temperature::Fahrenheit(temp) if (*temp >= 212) => true,
      Temperature::Celsius { temp } if (*temp >= 100) => true,
      _ => false, 
   }
}

Note that the wild cards in the match expressions above only work because we are taking the Temperature by value, and therefore we can drop the reference. If we however took the Temperature by value, and used a wildcard like the following:

fun is_temperature_fahrenheit_by_val(t: Temperature): bool {
   match (t) {
      Temperature::Fahrenheit(_) => true,
      _ => false,
   }
}

It would result in an error because Temperature does not have the drop ability. The two ways to solve this would be to either:

  1. Add the drop ability to the Temperature type: public enum Temperature has drop { ... }; or
  2. To destructure each enum variant in the match like so
fun is_temperature_fahrenheit_by_val(t: Temperature): bool {
   match (t) {
      Temperature::Fahrenheit(_) => true,
      Temperature::Celsius { temp: _ } | Temperature::Unknown => false,
   }
}

We additionally have pattern exhaustivity which means that we will ensure that the all possible variants of data are considered. Note however, that in the presence of guard expressions, that you will be required to provide a “default” case like in the above definition of is_temperature_boiling. Because of this it’s generally encouraged to not use guard expressions unless it will simplify the match expression, as you will not be notified of an error if you later-on add a new variant to the enum and forget to extend the pattern match.

// Non-exhaustive match: missing `Temperature::Unknown` variant
public fun is_freezing(t: &Temperature): bool {
   match (t) {
     Temperature::Fahrenheit(temp) => *temp >= 212,
     Temperature::Celsius { temp } => *temp >= 100,
     // Missing Temperature::Unknown variant
   }
}

// Non-exhaustive match: `Option::Some(false)` is not covered
public fun is_some_true(o: Option<bool>): bool {
   match (o) {
     Option::Some(true) => true,
     Option::None => false,
   }
}

// This works, but isn't the best
public fun is_some_true(o: Option<bool>): bool {
   match (o) {
     Option::Some(true) => true,
     Option::Some(false) => false,
     Option::None => false,
   }
}

// This would be the best way to write this function
public fun is_some_true(o: Option<bool>): bool {
   match (o) {
     Option::Some(x) => x,
     Option::None => false,
   }
}

More Examples

As another example you can write a simple Reverse Polish calculator using enums and pattern-matching:

public enum Exp has drop {
   Done,
   Add,
   Mul,
   Num(u64),
}

const EINVALIDEXP: u64 = 0;

public fun evaluate(mut expressions: vector<Exp>): u64 {
    let mut stack = vector[];
    while (!expressions.is_empty()) {
        match (expressions.pop_back()) {
            Exp::Done => break,
            Exp::Add => {
                let e1 = stack.pop_back();
                let e2 = stack.pop_back();
                stack.push_back(e1 + e2);
            },
            Exp::Mul => {
                let e1 = stack.pop_back();
                let e2 = stack.pop_back();
                stack.push_back(e1 * e2);
            },
            Exp::Num(number) => {
                stack.push_back(number);
            }
        }
    };
   let result = vector::pop_back(&mut stack);
   assert!(expressions.is_empty(), EINVALIDEXP);
   assert!(stack.is_empty(), EINVALIDEXP);
   result
}
@tzakian tzakian added Type: Major Feature Major new functionality or integration, which has a significant impact on the network devx move design labels Jan 10, 2024
Copy link
Contributor

This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days.

@github-actions github-actions bot added the Stale label Mar 12, 2024
@tzakian tzakian removed the Stale label Mar 13, 2024
@fl0ydj
Copy link

fl0ydj commented Mar 18, 2024

Hey @tzakian,
love the concept of enums and for our project it would make things so much simpler.
We are currently in the architecture and early prototyping phase and plan to go live on mainnet in August.

2 questions accordingly:

  • Would you advise us to do our architecture with enums considering our mainnet launch? A bit scared of the required byte code changes?
  • Any estimate when they will be available as part of Move 2024 to prototype with? Only estimate we found was Q2.

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
design devx move Type: Major Feature Major new functionality or integration, which has a significant impact on the network
Projects
None yet
Development

No branches or pull requests

2 participants