Book
Structs and Messages

Structs and Messages

Tact supports a number of primitive data types that are tailored for smart contract use. However, using individual means of storage often becomes cumbersome, so there are Structs and Messages which allow combining types together.

⚠️

Warning: Currently circular types are not possible. This means that Struct/Message A can't have a field of a Struct/Message B that has a field of the Struct/Message A.

Therefore, the following code won't compile:

struct A {
    circularFieldA: B;
}
 
struct B {
    impossibleFieldB: A;
}

Structs

Structs can define complex data types that contain multiple fields of different types. They can also be nested.

struct Point {
    x: Int as int64;
    y: Int as int64;
}
 
struct Line {
    start: Point;
    end: Point;
}

Structs can also contain default fields and define fields of optional types. This can be useful if you have a lot of fields, but don't want to keep having to specify common values for them in new instances.

struct Params {
    name: String = "Satoshi"; // default value
 
    age: Int?; // field with an optional type Int?
               // and default value of null
 
    point: Point; // nested Structs
}

Structs are also useful as return values from getters or other internal functions. They effectively allow a single getter to return multiple return values.

contract StructsShowcase {
    params: Params; // Struct as a contract's persistent state variable
 
    init() {
        self.params = Params{
            point: Point{
                x: 4,
                y: 2,
            },
        };
    }
 
    get fun params(): Params {
        return self.params;
    }
}

Note, that the last semicolon ; in Struct declaration is optional and may be omitted:

struct Mad { ness: Bool }
 
struct MoviesToWatch {
    wolverine: String;
    redFunnyGuy: String
}

The order of fields matters, as it corresponds to the resulting memory layout in TL-B schemas (opens in a new tab). However, unlike some languages with manual memory management, Tact does not have any padding between fields.

Messages

Messages can hold Structs in them:

struct Point {
    x: Int;
    y: Int;
}
 
message Add {
    point: Point; // holds a struct Point
}

Messages are almost the same thing as Structs with the only difference that Messages have a 32-bit integer header in their serialization containing their unique numeric id. This allows Messages to be used with receivers since the contract can tell different types of messages apart based on this id.

Tact automatically generates those unique ids for every received Message, but this can be manually overwritten:

// This Message overwrites its unique id with 0x7362d09c
message(0x7362d09c) TokenNotification {
    forwardPayload: Slice as remaining;
}

This is useful for cases where you want to handle certain opcodes (operation codes) of a given smart contract, such as Jetton standard (opens in a new tab). The short-list of opcodes this contract is able to process is given here in FunC (opens in a new tab). They serve as an interface to the smart contract.

Operations

Instantiate

Creation of Struct and Message instances resembles function calls, but instead of paretheses () one needs to specify arguments in braces {} (curly brackets):

struct StA {
    field1: Int;
    field2: Int;
}
 
message MsgB {
    field1: String;
    field2: String;
}
 
fun example() {
    // Instance of a Struct StA
    StA{
        field1: 42,
        field2: 68 + 1, // trailing comma is allowed
    };
 
    // Instance of a Message MsgB
    MsgB{
        field1: "May the 4th",
        field2: "be with you!", // trailing comma is allowed
    };
}

When the name of a variable or constant assigned to a field coincides with the name of such field, Tact provides a handy syntactic shortcut sometimes called field punning. With it, you don't have to type more than it's necessary:

struct PopQuiz {
    vogonsCount: Int;
    nicestNumber: Int;
}
 
fun example() {
    // Let's introduce a couple of variables
    let vogonsCount: Int = 42;
    let nicestNumber: Int = 68 + 1;
 
    // You may instantiate the Struct as usual and assign variables to fields,
    // but that is a bit repetitive and tedious at times
    PopQuiz{ vogonsCount: vogonsCount, nicestNumber: nicestNumber };
 
    // Let's use field punning and type less,
    // because our variable names happen to be the same as field names
    PopQuiz{
        vogonsCount,
        nicestNumber, // trailing comma is allowed here too!
    };
}
💡

Because instantiation is an expression in Tact, it's also described on the related page: Instantiation expression.

Convert to a Cell, .toCell()

It's possible to convert an arbitrary Struct or Message to the Cell type by using the .toCell() extension function:

struct Big {
    f1: Int;
    f2: Int;
    f3: Int;
    f4: Int;
    f5: Int;
    f6: Int;
}
 
fun conversionFun() {
    dump(Big{
        f1: 10000000000, f2: 10000000000, f3: 10000000000,
        f4: 10000000000, f5: 10000000000, f6: 10000000000,
    }.toCell()); // x{...cell with references...}
}
💡

See those extension functions in the Reference:
Struct.toCell()
Message.toCell()

Obtain from a Cell or Slice, .fromCell() and .fromSlice()

Instead of manually parsing a Cell or Slice via a series of relevant .loadSomething() function calls, one can use .fromCell() and .fromSlice() extension functions for converting the provided Cell or Slice into the needed Struct or Message.

Those extension functions only attempt to parse a Cell or Slice according to the structure of your Struct or Message. In case layouts don't match, various exceptions may be thrown — make sure to wrap your code in try...catch blocks to prevent unexpected results.

struct Fizz { foo: Int }
message(100) Buzz { bar: Int }
 
fun constructThenParse() {
    let fizzCell = Fizz{foo: 42}.toCell();
    let buzzCell = Buzz{bar: 27}.toCell();
 
    let parsedFizz: Fizz = Fizz.fromCell(fizzCell);
    let parsedBuzz: Buzz = Buzz.fromCell(buzzCell);
}
💡

See those extension functions in the Reference:
Struct.fromCell()
Struct.fromSlice()
Message.fromCell()
Message.fromSlice()

Conversion laws

Whenever one converts between Cell/Slice and Struct/Message via .toCell() and .fromCell() functions, the following laws hold:

  • For any instance of type Struct/Message, calling .toCell() on it, then applying Struct.fromCell() (or Message.fromCell()) to the result gives back the copy of the original instance:
struct ArbitraryStruct {}
message(0x2A) ArbitraryMessage {}
 
fun lawOne() {
    let structInst = ArbitraryStruct{};
    let messageInst = ArbitraryMessage{};
 
    ArbitraryStruct.fromCell(structInst.toCell());   // = structInst
    ArbitraryMessage.fromCell(messageInst.toCell()); // = messageInst
 
    // Same goes for Slices, with .toCell().asSlice() and .fromSlice()
 
    ArbitraryStruct.fromSlice(structInst.toCell().asSlice());   // = structInst
    ArbitraryMessage.fromSlice(messageInst.toCell().asSlice()); // = messageInst
}
struct ArbitraryStruct { val: Int as uint32 }
message(0x2A) ArbitraryMessage {}
 
fun lawTwo() {
    // Using 32 bits to store 42 just so this cellInst can be
    // re-used for working with both ArbitraryStruct and ArbitraryMessage
    let cellInst = beginCell().storeUint(42, 32).endCell();
 
    ArbitraryStruct.fromCell(cellInst).toCell();  // = cellInst
    ArbitraryMessage.fromCell(cellInst).toCell(); // = cellInst
 
    // Same goes for Slices, with .fromSlice() and .toCell().asSlice()
    let sliceInst = cellInst.asSlice();
 
    ArbitraryStruct.fromSlice(sliceInst).toCell().asSlice();  // = sliceInst
    ArbitraryMessage.fromSlice(sliceInst).toCell().asSlice(); // = sliceInst
}