Skip to content
LogoLogo

Variants

A variant type lists every case a value can take, and each case may carry a payload. Variants shape entrypoints, where an external function that takes a variant becomes one ABI function per case, and they hold state, where case makes every reader handle every state.

variants.lithe
(* A variant type is a closed set of cases. A case may carry a payload.
   An external function that takes a variant becomes one ABI function per case:
   this contract exposes `deposit(uint256)`, `send(address,uint256)`, and `reset()`. *)
 
import "std/evm";
 
type SendParams is record {
  to : address;
  amount : uint;
}
 
type Action is
  | Deposit external deposit of uint
  | Send external send of SendParams
  | Reset external reset
 
error Insufficient of uint;
 
storage is record {
  balances : map(address, uint);
}
 
(* `case` must handle every case; leaving one out is a compile error.
   Each arm binds the case's payload. *)
external function main(const action : Action)
  raises Insufficient
is
  case action of {
  | Deposit(amount) -> {
      storage.balances[Evm.sender()] := storage.balances[Evm.sender()] + amount;
    }
  | Send(p) -> {
      const have : uint := storage.balances[Evm.sender()];
      assert(have >= p.amount, Insufficient(have));
      storage.balances[Evm.sender()] := have - p.amount;
      storage.balances[p.to] := storage.balances[p.to] + p.amount;
    }
  | Reset -> {
      storage.balances[Evm.sender()] := 0;
    }
  }
 
external view function balanceOf(const who : address) : uint is
  storage.balances[who]
  • case must cover every case. Delete the Reset arm and the compiler reports missing case arm `Reset` .
  • A record payload becomes named ABI parameters, so Send is send(address to, uint256 amount).
  • See Dispatch for nesting and guards.

Stored state

A variant value is written by naming its case. A variant with no payloads is one byte in storage and a uint8 in the ABI, like a Solidity enum:

market.lithe
import "std/evm";
 
type Phase is
  | Open
  | Paused
  | Settled of uint
 
error NotOpen;
error NotSettled;
 
storage is record {
  phase : Phase;
  bids : map(address, uint);
}
 
external function bid(const amount : uint) raises NotOpen is {
  assert(storage.phase = Open, NotOpen);
  storage.bids[Evm.sender()] := storage.bids[Evm.sender()] + amount;
}
 
external function pause() is {
  storage.phase := Paused;
}
 
external function settle(const price : uint) is {
  storage.phase := Settled(price);
}
 
(* `| Open | Paused ->` is one arm for both, and still exhaustive: a new case
   is a compile error here until it is handled. *)
external view function price() : uint raises NotSettled is
  case storage.phase of {
  | Settled(p) -> p
  | Open | Paused -> fail NotSettled
  }
  • Settled carries a price, so phase is a tag slot and a payload slot. Without it, phase would take one byte.
  • A new contract reads phase as Open, the first case.
  • The Order executor keeps each order's status as a variant.

Payloads may hold strings, bytes, and lists. One arm can bind the payload of several cases when they carry the same type:

review.lithe
type Review is
  | Draft
  | Rejected of string
  | Withdrawn of string
  | Approved of list(address)
 
storage is record {
  reviews : map(uint, Review);
}
 
external function reject(const id : uint; const why : string) is {
  storage.reviews[id] := Rejected(why);
}
 
external function withdraw(const id : uint; const why : string) is {
  storage.reviews[id] := Withdrawn(why);
}
 
external function approve(const id : uint; const by : list(address)) is {
  storage.reviews[id] := Approved(by);
}
 
external view function reason(const id : uint) : string is
  case storage.reviews[id] of {
  | Rejected(why) | Withdrawn(why) -> why
  | Draft | Approved -> ""
  }
 
external view function approvals(const id : uint) : uint is
  case storage.reviews[id] of {
  | Approved(by) -> by.length
  | Draft | Rejected | Withdrawn -> 0
  }
  • Rejected and Withdrawn keep their reason in the same slot, so either case reads it the same way.
  • Writing Approved clears a stored reason, and writing Draft clears both.