Motto: if you cannot avoid using boilerplate code, it’s better to generate it than writing it.
Inspired by @RyanMedlen, @Gary_Verhaegen and Rust macros used for implementing traits.
The challenge: I want to migrate Daml contracts from one Daml model into an upgraded Daml model. The double dot syntax for copying the unchanged fields only works if we don’t have custom types in the contract payload to be migrated, which is unlikely for a real-life business use case. We can reduce the amount of boilerplate by keeping the old model as a dependency, but this is suboptimal. See more details in this earlier forum question.
It turns out though that the boilerplate code we need is pretty boring - which is good because it can be generated.
Let’s see a little bit more fancy version of the Asset template from the skeleton project template:
data AssetCode = AssetCode with
name: Text
version: Int
deriving (Eq, Show)
data Redeemable
= No
| Yes with until:Date
deriving(Eq, Show)
type AssetId = ContractId Asset
template Asset
with
issuer : Party
owner : Party
assetCode : AssetCode
redeemable : Redeemable
where
ensure assetCode.name /= ""
signatory issuer
observer owner
choice Give : AssetId
with
newOwner : Party
controller owner
do create this with
owner = newOwner
In the upgraded Daml model I’ve added a dummy text field, and I want to write a migrate
function which converts an old Asset contract into a new Asset contract.
I need the following type class and instance declarations. The majority of the code can be generated, I only need to add the extra dummy field manually:
module Migrate where
import Package1.Main qualified
import Package2.Main qualified
class HasMigrate old new where
migrate : old -> new
instance HasMigrate Party Party where
migrate = identity
instance HasMigrate Date Date where
migrate = identity
instance HasMigrate Text Text where
migrate = identity
instance HasMigrate Int Int where
migrate = identity
instance HasMigrate Package1.Main.AssetCode Package2.Main.AssetCode where
migrate (Package1.Main.AssetCode field0 field1) = Package2.Main.AssetCode (migrate field0) (migrate field1)
-- Extra field added manually
instance HasMigrate Package1.Main.Asset Package2.Main.Asset where
migrate (Package1.Main.Asset field0 field1 field2 field3) = Package2.Main.Asset (migrate field0) (migrate field1) (migrate field2) (migrate field3) "Dummy field"
instance HasMigrate Package1.Main.Redeemable Package2.Main.Redeemable where
migrate (Package1.Main.Yes field0) = Package2.Main.Yes (migrate field0)
migrate (Package1.Main.No ) = Package2.Main.No
A few lines of code makes the trick. I used for this Python but it can be actually any programming language:
#! python3
import os.path
module = "Migrate"
filename = f"upgrade/daml/{module}.daml"
old_module = "Package1.Main"
new_module = "Package2.Main"
standard_types = ["Party", "Date", "Text", "Int"]
# [(<Record name>, <# of fields>)]
record_type_shapes = [("AssetCode", 2), ("Asset", 4)]
# [(<Variant name), [<List of variant shapes, see record shape])]
variant_type_shapes = [("Redeemable", [("Yes", 1), ("No", 0)])]
header_lines = [f"module {module} where", f"import {old_module} qualified", f"import {new_module} qualified", "\n"]
header = "\n".join(header_lines)
class_declaration = "class HasMigrate old new where" + "\n migrate : old -> new" + "\n\n"
def standard_instance(type_):
return f"instance HasMigrate {type_} {type_} where" + "\n " + "migrate = identity" + "\n\n"
def instance_header(type_):
return f"instance HasMigrate {old_module}.{type_} {new_module}.{type_} where"
def migrate_product_type(type_, fields):
return f"migrate ({old_module}.{type_} {' '.join([f'field{idx}' for idx in range(fields)])}) = {new_module}.{type_} {' '.join([f'(migrate field{idx})' for idx in range(fields)])}"
def record_instance(type_, fields):
return instance_header(type_) + '\n ' + migrate_product_type(type_, fields) + '\n\n'
def variant_instance(type_, option_shapes):
options = [f"{migrate_product_type(type_, fields)}" for type_,fields in option_shapes]
lines = [instance_header(type_)] + options
return '\n '.join(lines) + '\n\n'
def codegen():
with open(filename, 'w') as f:
f.write(header)
f.write(class_declaration)
for type_ in standard_types:
f.write(standard_instance(type_))
for type_,fields in record_type_shapes:
f.write(record_instance(type_, fields))
for type_,option_shapes in variant_type_shapes:
f.write(variant_instance(type_, option_shapes))
print(f"{filename} created")
def remove_if_exists(path):
if os.path.exists(path):
os.remove(path)
print(f"{path} removed")
if __name__ == "__main__":
remove_if_exists(filename)
codegen()
Having all this in place, we only need an upgrade contract for authorization, on which a choice will use the migrate
function to convert an old Asset instance into a new asset creation input:
module Main where
import DA.Date
import Daml.Script
import Package1.Main qualified
import Package2.Main qualified
import Migrate
template Upgrade
with
issuer : Party
owner : Party
where
signatory issuer
observer owner
choice Perform : ContractId Package2.Main.Asset
with
cid : ContractId Package1.Main.Asset
controller owner
do
oldAsset <- fetch cid
let newAsset = migrate oldAsset
archive cid
create newAsset
test: Script (ContractId Package2.Main.Asset)
test = do
alice <- allocatePartyWithHint "Alice" (PartyIdHint "Alice")
bob <- allocatePartyWithHint "Bob" (PartyIdHint "Bob")
assetCid <- submit alice do
createCmd Package1.Main.Asset with
issuer = alice
owner = bob
assetCode = Package1.Main.AssetCode with
name = "TV"
version = 1
redeemable = Package1.Main.Yes (date 2023 Dec 31)
upgradeCid <- submit alice do
createCmd Upgrade with
issuer = alice
owner = bob
submit bob do
exerciseCmd upgradeCid Perform with cid = assetCid
For contracts with multiple signatories, we need a multistep process for the migration, in order to get authorization from all signatories.
Needless to say that for such a small Daml model the Daml codegen doesn’t make a big difference, but for a really complex model it can spare us a lot of typing.
After having tackled the codegen challenge, we need a way to collect all the types from the Daml model, for which we need to declare a HasMigrate
instance. I’m contemplating several approaches for this, and I haven’t found a fully automated method yet.