A variant is a type whose value is exactly one of several named cases, each
with its own data. A payment is cash, a card with its last digits, or a bank
transfer with an IBAN and a reference. Daml calls these sum types or
algebraic data types. In Python you’d model them with a class hierarchy or a
tagged union.
Language Fundamentals shows the basic
declaration. This page covers the rest: payload styles, constructing and
comparing values, using variants in templates, recursive and generic variants,
testing with Daml Script, and what isn’t supported yet.
The examples are in main/daml/Order.vy and test/daml/OrderTest.daml, and
every test passes.
from forge.models import ContractId, Decimal, Party, choice, template, variant, variant_record
A variant is a class marked @variant. Each method declares one constructor:
its name is the case, its parameters are the case’s data, and its body is
pass.
@variant
classPaymentMethod:
defCash(): pass
defCard(last_digits: str): pass
@variant_record
defBankTransfer(iban: str, reference: str): pass
dataPaymentMethod
=Cash
|CardText
|BankTransferwith
iban:Text
reference:Text
deriving (Eq, Show)
There are three kinds of constructor:
Constructor
.vy
Daml
No data
def Cash(): pass
Cash
Positional, one value
def Card(last_digits: str): pass
Card Text
Record, named fields
@variant_record on def BankTransfer(iban: str, reference: str): pass
BankTransfer with iban : Text; reference : Text
Positional constructors keep only the type of their value. The parameter
name (last_digits) documents the .vy source but isn’t part of the Daml
type.
Record constructors keep their field names. Use @variant_record when a
constructor holds more than one value, or when the field names matter to
clients, such as JSON API users.
Every parameter needs a type annotation.
The class may contain only constructors (DF1203). Put helper functions at
the top level.
Variants derive Eq and Show, so values can be compared and printed.
Warning: not available yet. DamlForge doesn’t check the two declarations
below yet. Both compile without a DF error, then damlc rejects the
generated Daml. Errors for both are planned for a future release.
1. A positional constructor with two or more values.
@variant
classShape:
defRect(width: int, height: int): pass
defDot(): pass
This generates Rect Int Int. Daml requires a constructor with more than one
value to name its fields, so damlc reports Constructors with multiple fields must give explicit field names.
How to avoid it: mark every constructor with two or more values as
@variant_record, and construct it with keyword arguments:
@variant
classShape:
@variant_record
defRect(width: int, height: int): pass
defDot(): pass
defsquare(side: int) -> Shape:
returnRect(width=side,height=side)
dataShape
=Rectwith
width:Int
height:Int
|Dot
deriving (Eq, Show)
square :Int->Shape
square side =
Rect with width = side, height = side
Alternatively, group the values in a struct and keep one positional value:
def Rect(size: Size): pass.
2. A variant with only one constructor, marked @variant_record.
@variant
classPayment:
@variant_record
defBankTransfer(iban: str, reference: str): pass
This generates data Payment = BankTransfer with .... Daml reads a type with a
single named-field constructor as a record, and a record’s constructor must
have the type’s name, so damlc reports Record type Payment has constructor BankTransfer with different name.
How to avoid it: a variant with one case is really a record, so declare it
as a struct:
struct Payment:
iban: str
reference: str
defwire(iban: str, reference: str) -> Payment:
returnPayment(iban=iban,reference=reference)
wire :Text->Text->Payment
wire iban reference =
Payment with iban = iban, reference = reference
If more cases are coming later, declare them now: with two or more
constructors, as in PaymentMethod above, @variant_record constructors work
as expected.
Call a constructor by its bare name, like a function. Don’t prefix it with the
variant name:
.vy
Daml
Cash()
Cash
Card("4242")
Card "4242"
BankTransfer(iban=iban, reference=ref)
BankTransfer with iban = iban, reference = ref
BankTransfer(iban, ref)
BankTransfer iban ref
PaymentMethod.Cash() fails with DF1219, because PaymentMethod is a type,
not a namespace. A constructor without data still needs () in .vy; the
parentheses disappear in Daml.
Constructors are ordinary expressions, so a function can build a variant value:
assertself.status ==Placed(), "the order is already paid"
ifself.amount >Decimal(1000):
assert method !=Cash(), "cash is accepted up to 1000"
returncreate(with_status(self,Paid(method)))
choice Pay:ContractIdOrder
with
method :PaymentMethod
controller customer
do
let self = this
assertMsg "the order is already paid" (self.status ==Placed)
if self.amount > (1000.0:Decimal)
thendo
assertMsg "cash is accepted up to 1000" (method /=Cash)
pure ()
elsedo
pure ()
create (with_status self (Paid method))
method != Cash() is a reliable test, because Cash has no data. A
comparison with a case that has data matches only that exact value:
method == Card("4242") is False for Card("1111").
To test for “any case except these”, list the cases without data:
A variant can take type parameters with Generic, like a generic Python class:
T =TypeVar("T")
@variant
classOutcome(Generic[T]):
defDone(value: T): pass
defRejected(reason: str): pass
@template
classJob:
owner: Party
outcome: Outcome[int]
signatory: Party = owner
dataOutcomet
=Donet
|RejectedText
deriving (Eq, Show)
Outcome[int] becomes Outcome Int. The standard library’s Either is
declared the same way, in forge/builtins/DA/Types.vy.
Warning: not available yet. Generic variants can be declared and used as
types, but .vy code can’t construct their values yet. Done(42) in a
function returning Outcome[int] fails with DF1230 and DF1226. Values
created by clients or Daml Script, such as Job above, work. Support is
planned for a future release.
Warning: not available yet..vy has no way to check which case a value
is and read its data. Daml does this with case ... of, but the Python
equivalents don’t compile:
Compiles to method.last_digits, which damlc rejects: a variant has no fields
Support for match is planned for a future release.
Until then:
Compare with == and in when the cases you test have no data, as Pay
and Ship do.
Store the data you need to read in its own field. If a choice must read
the card digits, keep them in a template field or choice argument, and use
the variant to record which case applies.
Use an Enum for the case and a separate field for its data when every
choice needs to branch on the case.
Read the data off the ledger. Daml Script and clients reading the ledger
through the JSON API can inspect variant data in full.