OOP classes and Daml smart contract templates, an imperfect analogy
In high-level, object-oriented programming languages, instances of objects and handles to the objects are interchangeable. One can refer to an instance of an object and call its properties and methods using a variable that stores the handle to the object. In Daml, ContractId and contract data cannot be used interchangeably. In this article, I examine the limitations of the analogy between OOP classes and Daml templates and explain why Daml requires an explicit command to retrieve contract data from the ledger.
Like many people, when learning new concepts, I find drawing analogies to already familiar concepts makes understanding new concepts easier. Like most software developers, I have an object-oriented programming (OOP) background. As I was learning Daml, I couldn’t help drawing comparisons to OOP concepts. I found such comparisons—e.g., the analogy between Daml smart contract templates and OOP classes, Daml contracts and OOP objects, and Daml choices and OOP methods, mentioned in the Daml documentation—to be very helpful.
This said, most analogies are imprecise and have their limitations. While it may be helpful initially to reduce a new concept to an already familiar one, there comes a time in the learning journey when the analogy stops serving the purpose and becomes a detriment. I found myself at this point when I first needed to store information about Daml contracts in other Daml contracts. Following the analogy between Daml contracts and OOP objects, I didn’t pay much attention to the distinction between Daml contracts and their payload. This is because in my mental model I likened a Daml contract to an OOP object and a Daml ContractId to an OOP pointer or handle.
In high-level OOP languages with automatic memory management (like C#, Java, or Python), the distinction between the object with its payload and the handle to this object is quite obscure. When you assign an object to a variable, you know the variable stores the handle to the object. But at the same time, you can access the object’s payload using the variable name and the dot notation (e.g., variable1.property1 or variable1.method1), which creates a feeling that the variable stores the object and that the object and its handle are interchangeable. Applying the same thinking to Daml smart contracts leads to trouble.
In Daml, a contract (referred to by the ContractId) and its payload are not interchangeable. Daml has strict typecheck enforcement. You’ll never be able to use contract payload where ContractId is expected, and vice versa. Consider a trivial Daml template named Asset:
To create a Daml contract based on this template we can use the create function, which returns Update (ContractId Asset). The following line creates a new Asset contract on the ledger, lifts ContractId for the created Asset contract and assigns it to the variable named assetCid.
If we try to pass assetCid as an argument to a function that expects type Asset, we’ll get a typecheck compile error. Similarly, if we create a function that expects ContractId Asset, e.g.
then fetch the Asset contract from the ledger and pass it to the above function
we will get a typecheck error “Couldn't match expected type ‘ContractId Asset’ with actual type ‘Asset’”.
Why does Daml not allow interchangeable use of ContractId and contract payload?
One reason is that, unlike in OOP, where an object payload is stored in memory and is therefore accessible instantaneously, a Daml contract payload is stored on the ledger. Fetching a contract’s payload from the ledger is not instantaneous. To emphasize this, Daml requires an explicit use of fetch (or another function that has the effect of retrieving contract from the Active Contract Set) to access the contract payload, in preference over the variable1.property1 syntax.
A more important reason is to make explicit the distinction between what’s on the ledger and what’s off the ledger. A contract is an instance of a Daml template stored on the ledger and referenced by ContractId. Contract data is a Daml record, which is effectively a collection of fields. Two records are equivalent as long as the values of all the fields in these records are identical. The following function that uses the above Asset template always returns True.
Similarly, a Daml record created off the ledger and a contract payload retrieved from the ledger are equivalent as long as the record and the contract were made from the same Daml template and as long as the values of all the fields in these records are identical. To illustrate this, the assertion on the last line of the following code snippet always holds.
Since we cannot distinguish the records, as long as the values of all the fields in these records match, we couldn’t possibly use the contract payload as a reference to the contract. It’s perfectly possible to have multiple contracts with identical payload on the ledger. It’s also possible to create records off the ledger that are identical to the contract’s payload.
To summarize, perfect analogies are like perfect marriages. They are rare. Take the analogy between analogies and marriages as a hint.The analogy between OOP classes and Daml smart contract templates is useful to a point.
ContractId is used in Daml as a reference to an active contract on the ledger. Contract payload and ContractId are not interchangeable. This is where the analogy between Daml contracts and OOP objects falls short. One technique I found very useful to make the distinction between ContractIds and contract data very explicit in code is a variable naming convention, where “Cid” suffix is appended to any variable that holds a ContractId.