Endless
  • 🚀README
  • Discovery
    • 🚀Endless Web3 Genesis Cloud
    • 💎Business Model
    • 🎯Vision
    • ✈️Roadmap
    • 🪙Economics
    • 👤Team
      • Yu Xiong
      • Amit Kumar Jaiswal
      • Ned
      • 0xfun
      • Scott Trowbridge
      • Neeraj Sharma LLB
      • Amjad Suleman
      • Binu Paul
      • Eduard Romulus GOEAN
    • ❤️Developer Community
  • Endless Chain
    • Tech Docs
      • Account Address Format
      • Endless Account
      • Endless Coin(EDS)
      • Sponsored Transaction
      • On-Chain Multisig
      • Randomness
      • Safety Transaction
      • Token Locking & Distribution
    • Start
      • Learn about Endless
        • Accounts
        • Resources
        • Events
        • Transactions and States
        • Gas and Storage Fees
        • Computing Transaction Gas
        • Blocks
        • Staking
          • Delegated Staking
        • Governance
        • Endless Blockchain Deep Dive
          • Validator Nodes Overview
          • Fullnodes Overview
          • Node Networks and Synchronization
        • Move - A Web3 Language and Runtime
      • Explore Endless
      • Latest Endless Releases
      • Networks
    • Build
      • Tutorials
        • Your First Transaction
        • Your First Fungible Asset
        • Your First NFT
        • Your First Move Module
        • Your First Multisig
      • Learn the Move Language
        • The Move Book
          • Getting Started
            • Introduction
            • Modules and Scripts
          • Primitive Types
            • Move Tutorial
            • Integers
            • Bool
            • Address
            • Vector
            • Signer
            • References
            • Tuples and Unit
          • Basic Concepts
            • Local Variables and Scope
            • Equality
            • Abort and Assert
            • Conditionals
            • While, For, and Loop
            • Functions
            • Structs and Resources
            • Constants
            • Generics
            • Abilities
            • Uses and Aliases
            • Friends
            • Packages
            • Package Upgrades
            • Unit Tests
          • Global Storage
            • Global Storage - Structure
            • Global Storage - Operators
          • Reference
            • Libraries
            • Move Coding Conventions
        • Advanced Move Guides
          • Objects
            • Creating Objects
            • Configuring objects
            • Using objects
          • Move Scripts
            • Writing Move Scripts
            • Compiling Move Scripts
            • Running Move Scripts
            • Move Scripts Tutorial
          • Resource Accounts
          • Modules on Endless
          • Cryptography
          • Gas Profiling
          • Security
      • Endless Standards
        • Object
        • Endless Fungible Asset Standard
        • Endless Digital Asset Standard
        • Endless Wallet Standard
      • Endless APIs
        • Fullnode Rest API
        • Indexer Restful API
          • Indexer Installation
        • GRPC Transaction Stream
          • Running Locally
          • Custom Processors
            • End-to-End Tutorial
            • Parsing Transactions
          • Self-Hosted Transaction Stream Service
      • Endless SDKs
        • TypeScript SDK
          • Account
          • SDK Configuration
          • Fetch data from chain
          • Transaction Builder
          • HTTP Client
          • Move Types
          • Testing
          • Typescript
        • Rust SDK
        • Go SDK
      • Endless CLI
        • Install the Endless CLI
          • Install On Mac
          • Install On Alibaba Cloud
          • Install On Linux
          • Install On Windows
        • CLI Configuration
        • Use Endless CLI
          • Working With Move Contracts
            • Arguments in JSON Tutorial
          • Trying Things On-Chain
            • Look Up On-Chain Account Info
            • Create Test Accounts
          • Running A Local Network
            • Running a Public Network
          • Managing a Network Node
      • Integrate with Endless
        • Endless Token Overview
        • Application Integration Guide
      • Endless VSCode extension
      • Advanced Builder Guides
        • Develop Locally
          • Running a Local Network
          • Run a Localnet with Validator
    • Nodes
      • Learn about Nodes
      • Run a Validator and VFN
        • Node Requirements
        • Deploy Nodes
          • Using Docker
          • Using AWS
          • Using Azure
          • Using GCP
        • Connect Nodes
          • Connect to a Network
        • Verify Nodes
          • Node Health
          • Validator Leaderboard
      • Run a Public Fullnode
        • PFN Requirements
        • Deploy a PFN
          • Using Pre-compiled Binary
          • Using Docker
          • Using GCP 🚧 (under_construction)
        • Verify a PFN
        • Modify a PFN
          • Upgrade your PFN
          • Generate a PFN Identity
          • Customize PFN Networks
      • Bootstrap a Node
        • Bootstrap from a Snapshot
        • Bootstrap from a Backup
      • Configure a Node
        • State Synchronization
        • Data Pruning
        • Telemetry
        • Locating Node Files
          • Files For Mainnet
          • Files For Testnet
          • Files For Devnet
      • Monitor a Node
        • Node Inspection Service
        • Important Node Metrics
        • Node Health Checker
    • Reference
      • Endless Error Codes
      • Move Reference Documentation
      • Endless Glossary
    • FAQs
  • Endless Bridge
    • Intro to Endless Bridge
    • How to use bridge
    • Liquidity Management
    • Faucet
    • Developer Integration
      • Contract Integration
        • Message Contract
        • Execute Contract
      • Server-Side Integration
        • Message Sender
        • Example of Message Listener Service (Rust)
        • Example of Token Cross-Chain (JS)
  • Endless Wallet
    • User Guide
    • Basic Tutorial
    • FAQs
    • MultiAccount
    • SDK
      • Functions
      • Events
  • GameFi
    • Intro
    • GameFi & Endless
  • Endless Modules
    • Stacks
    • Storage
    • Module List
  • Endless Ecosystem
    • Intro
    • Show Cases
    • App Demo
  • Whitepaper
  • Endless SCAN
    • User Guide
  • MULTI-SIGNATURE
    • Multi-Signature User Guide
  • Regulations
    • Privacy Policy
    • Terms of Service
    • Funding Terms - Disclaimer
Powered by GitBook
On this page
  • Local Variables and Scope
  • Declaring Local Variables
  • Mutations
  • Scopes
  • Move and Copy
Export as PDF
  1. Endless Chain
  2. Build
  3. Learn the Move Language
  4. The Move Book
  5. Basic Concepts

Local Variables and Scope

Local Variables and Scope

Local variables in Move are lexically (statically) scoped. New variables are introduced with the keyword let, which will shadow any previous local with the same name. Locals are mutable and can be updated both directly and via a mutable reference.

Declaring Local Variables

let bindings

Move programs use let to bind variable names to values:

let x = 1;
let y = x + x:

let can also be used without binding a value to the local.

let x;

The local can then be assigned a value later.

let x;
if (cond) {
  x = 1
} else {
  x = 0
}

This can be very helpful when trying to extract a value from a loop when a default value cannot be provided.

let x;
let cond = true;
let i = 0;
loop {
    (x, cond) = foo(i);
    if (!cond) break;
    i = i + 1;
}

Variables must be assigned before use

Move's type system prevents a local variable from being used before it has been assigned.

let x;
x + x // ERROR!
let x;
if (cond) x = 0;
x + x // ERROR!
let x;
while (cond) x = 0;
x + x // ERROR!

Valid variable names

Variable names can contain underscores _, letters a to z, letters A to Z, and digits 0 to 9. Variable names must start with either an underscore _ or a letter a through z. They cannot start with uppercase letters.

// all valid
let x = e;
let _x = e;
let _A = e;
let x0 = e;
let xA = e;
let foobar_123 = e;

// all invalid
let X = e; // ERROR!
let Foo = e; // ERROR!

Type annotations

The type of local variable can almost always be inferred by Move's type system. However, Move allows explicit type annotations that can be useful for readability, clarity, or debuggability. The syntax for adding a type annotation is:

let x: T = e; // "Variable x of type T is initialized to expression e"

Some examples of explicit type annotations:

address 0x42 {
module example {

    struct S { f: u64, g: u64 }

    fun annotated() {
        let u: u8 = 0;
        let b: vector<u8> = b"hello";
        let a: address = @0x0;
        let (x, y): (&u64, &mut u64) = (&0, &mut 1);
        let S { f, g: f2 }: S = S { f: 0, g: 1 };
    }
}
}

Note that the type annotations must always be to the right of the pattern:

let (x: &u64, y: &mut u64) = (&0, &mut 1); // ERROR! should be let (x, y): ... =

When annotations are necessary

In some cases, a local type annotation is required if the type system cannot infer the type. This commonly occurs when the type argument for a generic type cannot be inferred. For example:

let _v1 = vector::empty(); // ERROR!
//        ^^^^^^^^^^^^^^^ Could not infer this type. Try adding an annotation
let v2: vector<u64> = vector::empty(); // no error

In a rarer case, the type system might not be able to infer a type for divergent code (where all the following code is unreachable). Both return and abort are expressions and can have any type. A loop has type () if it has a break, but if there is no break out of the loop, it could have any type. If these types cannot be inferred, a type annotation is required. For example, this code:

let a: u8 = return ();
let b: bool = abort 0;
let c: signer = loop ();

let x = return (); // ERROR!
//  ^ Could not infer this type. Try adding an annotation
let y = abort 0; // ERROR!
//  ^ Could not infer this type. Try adding an annotation
let z = loop (); // ERROR!
//  ^ Could not infer this type. Try adding an annotation

Adding type annotations to this code will expose other errors about dead code or unused local variables, but the example is still helpful for understanding this problem.

Multiple declarations with tuples

let can introduce more than one local at a time using tuples. The locals declared inside the parenthesis are initialized to the corresponding values from the tuple.

let () = ();
let (x0, x1) = (0, 1);
let (y0, y1, y2) = (0, 1, 2);
let (z0, z1, z2, z3) = (0, 1, 2, 3);

The type of the expression must match the arity of the tuple pattern exactly.

let (x, y) = (0, 1, 2); // ERROR!
let (x, y, z, q) = (0, 1, 2); // ERROR!

You cannot declare more than one local with the same name in a single let.

let (x, x) = 0; // ERROR!

Multiple declarations with structs

let can also introduce more than one local at a time when destructuring (or matching against) a struct. In this form, the let creates a set of local variables that are initialized to the values of the fields from a struct. The syntax looks like this:

struct T { f1: u64, f2: u64 }
let T { f1: local1, f2: local2 } = T { f1: 1, f2: 2 };
// local1: u64
// local2: u64

Here is a more complicated example:

address 0x42 {
module example {
    struct X { f: u64 }
    struct Y { x1: X, x2: X }

    fun new_x(): X {
        X { f: 1 }
    }

    fun example() {
        let Y { x1: X { f }, x2 } = Y { x1: new_x(), x2: new_x() };
        assert!(f + x2.f == 2, 42);

        let Y { x1: X { f: f1 }, x2: X { f: f2 } } = Y { x1: new_x(), x2: new_x() };
        assert!(f1 + f2 == 2, 42);
    }
}
}

Fields of structs can serve double duty, identifying the field to bind and the name of the variable. This is sometimes referred to as punning.

let X { f } = e;

is equivalent to:

let X { f: f } = e;

As shown with tuples, you cannot declare more than one local with the same name in a single let.

let Y { x1: x, x2: x } = e; // ERROR!

Destructuring against references

In the examples above for structs, the bound value in the let was moved, destroying the struct value and binding its fields.

struct T { f1: u64, f2: u64 }
let T { f1: local1, f2: local2 } = T { f1: 1, f2: 2 };
// local1: u64
// local2: u64

In this scenario the struct value T { f1: 1, f2: 2 } no longer exists after the let.

If you wish instead to not move and destroy the struct value, you can borrow each of its fields. For example:

let t = T { f1: 1, f2: 2 };
let T { f1: local1, f2: local2 } = &t;
// local1: &u64
// local2: &u64

And similarly with mutable references:

let t = T { f1: 1, f2: 2 };
let T { f1: local1, f2: local2 } = &mut t;
// local1: &mut u64
// local2: &mut u64

This behavior can also work with nested structs.

address 0x42 {
module example {
    struct X { f: u64 }
    struct Y { x1: X, x2: X }

    fun new_x(): X {
        X { f: 1 }
    }

    fun example() {
        let y = Y { x1: new_x(), x2: new_x() };

        let Y { x1: X { f }, x2 } = &y;
        assert!(*f + x2.f == 2, 42);

        let Y { x1: X { f: f1 }, x2: X { f: f2 } } = &mut y;
        *f1 = *f1 + 1;
        *f2 = *f2 + 1;
        assert!(*f1 + *f2 == 4, 42);
    }
}
}

Ignoring Values

In let bindings, it is often helpful to ignore some values. Local variables that start with _ will be ignored and not introduce a new variable

fun three(): (u64, u64, u64) {
    (0, 1, 2)
}
let (x1, _, z1) = three();
let (x2, _y, z2) = three();
assert!(x1 + z1 == x2 + z2, 42);

This can be necessary at times as the compiler will error on unused local variables

let (x1, y, z1) = three(); // ERROR!
//       ^ unused local 'y'

General let grammar

All the different structures in let can be combined! With that we arrive at this general grammar for let statements:

let-binding → let pattern-or-list type-annotationopt initializeropt

pattern-or-list → pattern | ( pattern-list )

pattern-list → pattern ,opt | pattern , pattern-list

type-annotation → : type

initializer → = expression

The general term for the item that introduces the bindings is a pattern. The pattern serves to both destructure data (possibly recursively) and introduce the bindings. The pattern grammar is as follows:

pattern → local-variable | struct-type { field-binding-list }

field-binding-list → field-binding ,opt | field-binding , field-binding-list

field-binding → field | field : pattern

A few concrete examples with this grammar applied:

    let (x, y): (u64, u64) = (0, 1);
//       ^                           local-variable
//       ^                           pattern
//          ^                        local-variable
//          ^                        pattern
//          ^                        pattern-list
//       ^^^^                        pattern-list
//      ^^^^^^                       pattern-or-list
//            ^^^^^^^^^^^^           type-annotation
//                         ^^^^^^^^  initializer
//  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ let-binding

    let Foo { f, g: x } = Foo { f: 0, g: 1 };
//      ^^^                                    struct-type
//            ^                                field
//            ^                                field-binding
//               ^                             field
//                  ^                          local-variable
//                  ^                          pattern
//               ^^^^                          field-binding
//            ^^^^^^^                          field-binding-list
//      ^^^^^^^^^^^^^^^                        pattern
//      ^^^^^^^^^^^^^^^                        pattern-or-list
//                      ^^^^^^^^^^^^^^^^^^^^   initializer
//  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ let-binding

Mutations

Assignments

After the local is introduced (either by let or as a function parameter), the local can be modified via an assignment:

x = e

Unlike let bindings, assignments are expressions. In some languages, assignments return the value that was assigned, but in Move, the type of any assignment is always ().

(x = e: ())

Practically, assignments being expressions means that they can be used without adding a new expression block with braces ({...}).

let x = 0;
if (cond) x = 1 else x = 2;

The assignment uses the same pattern syntax scheme as let bindings:

address 0x42 {
module example {
    struct X { f: u64 }

    fun new_x(): X {
        X { f: 1 }
    }

    // This example will complain about unused variables and assignments.
    fun example() {
       let (x, _, z) = (0, 1, 3);
       let (x, y, f, g);

       (X { f }, X { f: x }) = (new_x(), new_x());
       assert!(f + x == 2, 42);

       (x, y, z, f, _, g) = (0, 0, 0, 0, 0, 0);
    }
}
}

Note that a local variable can only have one type, so the type of the local cannot change between assignments.

let x;
x = 0;
x = false; // ERROR!

Mutating through a reference

In addition to directly modifying a local with assignment, a local can be modified via a mutable reference &mut.

let x = 0;
let r = &mut x;
*r = 1;
assert!(x == 1, 42);

This is particularly useful if either:

(1) You want to modify different variables depending on some condition.

let x = 0;
let y = 1;
let r = if (cond) {
  &mut x
} else {
  &mut y
};
*r = *r + 1;

(2) You want another function to modify your local value.

let x = 0;
modify_ref(&mut x);

This sort of modification is how you modify structs and vectors!

let v = vector::empty();
vector::push_back(&mut v, 100);
assert!(*vector::borrow(&v, 0) == 100, 42);

For more details, see Move references.

Scopes

Any local declared with let is available for any subsequent expression, within that scope. Scopes are declared with expression blocks, {...}.

Locals cannot be used outside the declared scope.

let x = 0;
{
    let y = 1;
};
x + y // ERROR!
//  ^ unbound local 'y'

But, locals from an outer scope can be used in a nested scope.

{
    let x = 0;
    {
        let y = x + 1; // valid
    }
}

Locals can be mutated in any scope where they are accessible. That mutation survives with the local, regardless of the scope that performed the mutation.

let x = 0;
x = x + 1;
assert!(x == 1, 42);
{
    x = x + 1;
    assert!(x == 2, 42);
};
assert!(x == 2, 42);

Expression Blocks

An expression block is a series of statements separated by semicolons (;). The resulting value of an expression block is the value of the last expression in the block.

{ let x = 1; let y = 1; x + y }

In this example, the result of the block is x + y.

A statement can be either a let declaration or an expression. Remember that assignments (x = e) are expressions of type ().

{ let x; let y = 1; x = 1; x + y }

Function calls are another common expression of type (). Function calls that modify data are commonly used as statements.

{ let v = vector::empty(); vector::push_back(&mut v, 1); v }

This is not just limited to () types---any expression can be used as a statement in a sequence!

{
    let x = 0;
    x + 1; // value is discarded
    x + 2; // value is discarded
    b"hello"; // value is discarded
}

But! If the expression contains a resource (a value without the drop ability), you will get an error. This is because Move's type system guarantees that any value that is dropped has the drop ability. (Ownership must be transferred or the value must be explicitly destroyed within its declaring module.)

{
    let x = 0;
    Coin { value: x }; // ERROR!
//  ^^^^^^^^^^^^^^^^^ unused value without the `drop` ability
    x
}
// Both are equivalent
{ x = x + 1; 1 / x; }
{ x = x + 1; 1 / x; () }
// Both are equivalent
{ }
{ () }

An expression block is itself an expression and can be used anyplace an expression is used. (Note: The body of a function is also an expression block, but the function body cannot be replaced by another expression.)

let my_vector: vector<vector<u8>> = {
    let v = vector::empty();
    vector::push_back(&mut v, b"hello");
    vector::push_back(&mut v, b"goodbye");
    v
};

(The type annotation is not needed in this example and only added for clarity.)

Shadowing

If a let introduces a local variable with a name already in scope, that previous variable can no longer be accessed for the rest of this scope. This is called shadowing.

let x = 0;
assert!(x == 0, 42);

let x = 1; // x is shadowed
assert!(x == 1, 42);

When a local is shadowed, it does not need to retain the same type as before.

let x = 0;
assert!(x == 0, 42);

let x = b"hello"; // x is shadowed
assert!(x == b"hello", 42);

After a local is shadowed, the value stored in the local still exists, but will no longer be accessible. This is important to keep in mind with values of types without the drop ability, as ownership of the value must be transferred by the end of the function.

address 0x42 {
    module example {
        struct Coin has store { value: u64 }

        fun unused_resource(): Coin {
            let x = Coin { value: 0 }; // ERROR!
//              ^ This local still contains a value without the `drop` ability
            x.value = 1;
            let x = Coin { value: 10 };
            x
//          ^ Invalid return
        }
    }
}

When a local is shadowed inside a scope, the shadowing only remains for that scope. The shadowing is gone once that scope ends.

let x = 0;
{
    let x = 1;
    assert!(x == 1, 42);
};
assert!(x == 0, 42);

Remember, locals can change type when they are shadowed.

let x = 0;
{
    let x = b"hello";
    assert!(x = b"hello", 42);
};
assert!(x == 0, 42);

Move and Copy

All local variables in Move can be used in two ways, either by move or copy. If one or the other is not specified, the Move compiler is able to infer whether a copy or a move should be used. This means that in all the examples above, a move or a copy would be inserted by the compiler. A local variable cannot be used without the use of move or copy.

copy will likely feel the most familiar coming from other programming languages, as it creates a new copy of the value inside the variable to use in that expression. With copy, the local variable can be used more than once.

let x = 0;
let y = copy x + 1;
let z = copy x + 2;

Any value with the copy ability can be copied in this way.

move takes the value out of the local variable without copying the data. After a move occurs, the local variable is unavailable.

let x = 1;
let y = move x + 1;
//      ------ Local was moved here
let z = move x + 2; // Error!
//      ^^^^^^ Invalid usage of local 'x'
y + z

Safety

Move's type system will prevent a value from being used after it is moved. This is the same safety check described in let declaration that prevents local variables from being used before it is assigned a value.

Inference

As mentioned above, the Move compiler will infer a copy or move if one is not indicated. The algorithm for doing so is quite simple:

  • Any value with the copy ability is given a copy.

  • Any reference (both mutable &mut and immutable &) is given a copy.

    • Except under special circumstances where it is made a move for predictable borrow checker errors.

  • Any other value is given a move.

  • If the compiler can prove that the source value with copy ability is not used after the assignment, then a move may be used instead of a copy for performance, but this will be invisible to the programmer (except in possible decreased time or gas cost).

For example:

struct Foo {
    f: u64
}

struct Coin has copy {
    value: u64
}

let s = b"hello";
let foo = Foo { f: 0 };
let coin = Coin { value: 0 };

let s2 = s; // copy
let foo2 = foo; // move
let coin2 = coin; // copy

let x = 0;
let b = false;
let addr = @0x42;
let x_ref = &x;
let coin_ref = &mut coin2;

let x2 = x; // copy
let b2 = b; // copy
let addr2 = @0x42; // copy
let x_ref2 = x_ref; // copy
let coin_ref2 = coin_ref; // copy
PreviousBasic ConceptsNextEquality

Last updated 7 months ago

If a final expression is not present in a block---that is, if there is a trailing semicolon ;, there is an implicit . Similarly, if the expression block is empty, there is an implicit unit () value.

unit () value