Unions
Unions allow you to define a type by enumerating its possible variants. Where structs and tuples
give you a way of grouping together related fields and data, like a User
with its status
,
address
, and balance
, unions give you a way of saying a value is one of possible set of values.
For example, we may want to say that User
is one of a set of possible account types that also
includes Contract
. To do this, Pint allows us to encode these possibilities as a union
.
Let’s look at a situation we might want to express in code and see why unions are useful and more appropriate that structs in this case. Say we need to describe token ownership of one of three available tokens on an exchange: DAI, USDC, and USDT. Because these are the only tokens we can work with, we can enumerate them all as different variants:
union Token = DAI | USDC | USDT;
Note how the possible variants of Token
are separated by a |
. Token
is now a custom data type
that we can use elsewhere in our code. Any Token
can now be DAI
, USDC
, or USDT
but never 2
or more of these at the same time. That property of Token
makes the union data structure
appropriate because a union variant can only be one of its variants. All three tokens are still
fundamentally tokens, so they should be treated as the same type when the code is handling
situations that apply to any of these tokens.
Union Values
We can now create an instance of each of the three variants of Token
like this:
let dai: Token = Token::DAI;
let usdc: Token = Token::USDC;
let usdt: Token = Token::USDT;
Note that the variants of the union are namespaced under its identifier, and we use a double colon
to separate the two. This is useful because now all three values Token::DAI
, Token::USDC
, and
Token::USDT
are of the same type: Token
. We can then, for instance, declare a variable called
token_type
to be of type Token
and assign it to either variants depending some token ID:
let token_type: Token = cond {
token_id == 0 => Token::DAI,
token_id == 1 => Token::USDC,
else => Token::USDT,
};
Using unions has even more advantages. Thinking more about our Token
type, at the moment we don’t
have a way to store the actual token amount; we only know what kind it is. Given what you know about
structs and tuples, you might be tempted to tackle this problem with structs as follows:
type Balance = {
token: Token,
balance: int,
};
predicate balances_1() {
let alice_bal: Balance = {
token: Token::DAI,
balance: 42,
};
let bob_bal: Balance = {
token: Token::USDC,
balance: 96,
};
}
Here, we've defined a new type called Balance
that has two fields: a token
field that is of type
Token
(the union we defined previously) and a balance
field of type int
that represents the
balance. We have two instances of this type. The first is alice_bal
, and it has the value
Token::DAI
as its token
with associated balance
of 42
. The second instance is bob_bal
. It
has another variant of Token
as its kind
value, Token::USDC
, and has a balance
of 96
associated with it. We've used a struct to bundle the token
and balance
values together, so now
the variant is associated with the value.
However, representing the same concept using just a union is more concise: rather than a union
inside a struct, we can put data directly into each union variant. This new definition of the
Token
union says that the variants DAI
, USDC
, and USDT
will have associated int
values:
union TokenBalance = DAI(int) | USDC(int) | USDT(int);
predicate balances_2() {
let alice_bal: TokenBalance = TokenBalance::DAI(42);
let bob_bal: TokenBalance = TokenBalance::USDC(96);
}
We attach data to each variant of the union directly, so there is no need for an extra struct. Also notice the syntax for constructing an instance of the union where the value held by a variant is added between parentheses after the variant name.
There's another advantage to using a union rather than a struct: each variant can have different types and amounts of associated data. Let's look at another example of a union that has a wide variety of types embedded in its variants:
union Action = Quit // Quit the application
| Buy (TokenBalance) // Buy some amount of a token
| Sell({TokenBalance, price: int}) // sell some amount of a token at some price
| Swap({TokenBalance, TokenBalance}); // Swap some amount of a token for another amount of some other token
This union has four variants with different types:
Quit
has no data associated with it at all.Buy
includes aTokenBalance
.Sell
andSwap
includes tuples.
Defining a union with variants such as the ones above is similar to defining different kinds of
struct definitions, except the union variants are grouped together under the Action
type which
makes it easier to reason about them as part of a single type.