Interface Syntax
This is a step-by-step example of the syntax for Daml interfaces. For a more complete discussion of Daml interfaces, see the docs.
This example uses the theme of a community library with a collection of books and discs. The books and discs can be loaned out to borrowers. Daml interfaces are used in this example as a way to reference both books and discs with a single type. While books and discs have unique fields (e.g, books have authors, discs have formats), both are types of things that can be borrowed.
This article builds up the example incrementally. The full source code is listed at the bottom of the article.
This example was built using Daml SDK 2.5.0.
1. Setup
Create a new Daml project. Be sure that you are using a Daml SDK 2.5.0 or later. You may also need to manually edit the daml.yaml file, setting the target in the build-options.
Here is an example daml.yaml file.
sdk-version: 2.5.0-snapshot.20221201.11065.0.caac1d10
name: interface-testing
source: daml
init-script: Setup:setup
navigator-options:
- --feature-user-management=false
version: 0.0.1
dependencies:
- daml-prim
- daml-stdlib
- daml-script
build-options: [--target=1.15]
Next, define the basic templates for the example. There is probably no surprises or anything new here. (We will start defining an interface in the next step.)
import Daml.Script
data DiscFormat = DVD | BluRay
deriving (Eq, Show)
-- a book can be loaned
template Book
with
owner : Party
title : Text
loanedTo : Optional Party
author : Text -- only on Book
where
signatory owner
observer loanedTo
-- a disc can be loaned
template Disc
with
owner : Party
title : Text
loanedTo : Optional Party
format : DiscFormat -- only on Disc
where
signatory owner
observer loanedTo
demo : Script ()
demo = script do
-- setup
owner <- allocateParty "owner"
borrower <- allocateParty "borrower"
let title = "The Hobbit"
loanedTo = None
book <-
submit owner do
createCmd Book with
author = "Tolkien",..
disc <-
submit owner do
createCmd Disc with
format = BluRay,..
return ()
Notice that books and discs have some things in common. They are both loanable items in our application. Additionally, on the templates that represent those items, some of the fields are identical – owner, title, and loanedTo. The two templates also have unique fields, specifically author and format.
2. Define the interface
Let’s define an interface so that we can write common code that works with both books and discs.
The first step to defining the interface is to create a Daml record to serve as the “interface view” (or simply “view”.) The view includes fields for values that could be considered common to both books and discs. In this example, the view will be named LoanableView.
-- data that can be projected (read-only) from both Book and Disc
data LoanableView = LoanableView
with
owner : Party
media : Text
title : Text
loanedTo : Optional Party
Notice that the view includes fields that are common (owner, title, and loanedTo) and one additional field (media) which could be projected from both books and discs. When we later query the ledger for loanable items, these are the data that will be retrieved for both books and discs.
The next step is to define the actual interface.
-- the interface for things that can be loaned
interface ILoanable where
viewtype LoanableView
-- the functions that must be implemented for all ILoanables
-- (similar to OOP abstract methods)
loanTo : Party -> Update (ContractId ILoanable)
-- a choice that can be exercised on ContractId ILoanable
-- (similar to OOP base class method, calling abstract methods)
choice LoanTo : ContractId ILoanable
with
borrower : Party
controller (view this).owner
do
loanTo this borrower
There are three things to notice about the interface above.
- The
viewtypekeyword associates the view type with the interface. - This interface includes one function,
loanTo, which will later be implemented on the templatesBookandDisc. - This interface includes one choice. This choice can be executed on
ContractId ILoanablereferences, as we will see.
There are four things to notice about the choice LoanTo above.
- The choice appears similar to the choices you might see on a template. It has a
withblock, acontroller, and adoblock. - The choice has access to a value
thiswhich represents the contract of the implementing template. In this examplethiswould be either aBookorDiscinstance. - The choice has access to a function named
view, which returns an instance of the view type. In this example, the view type is aLoanableView. It can be used to get access to the values projected from the implementing type. - The choice is able to call functions defined by the interface. In this example, the choice can call a function named
loanTo. Notice that the signature ofloanToin the choice body is not exactly the same as the signature of theloanTodefined on the function. TheloanTofunction available in the choice body includes an additional parameter, for which we passthis.
3. Use the interface
The power of interfaces is that one can write code that works with multiple types – as long as those types implement the same interface.
The following is the implementation of a Library that has a list of ContractId ILoanable items.
import DA.List (replace)
:
template Library
with
owner : Party
items : [ContractId ILoanable] -- both books and discs
where
signatory owner
choice Loan : ContractId Library
with
borrower : Party
item : ContractId ILoanable -- a book or disc
controller owner
do
assert (item `elem` items)
loanedItem <- exercise item LoanTo with ..
let newItems = replace [item] [loanedItem] this.items
create this with items = newItems
Notice that the Library above works exclusively with ILoanable references. It even exercises the choice LoanTo which is implemented on the ILoanable interface. The Library needs no code specific to Book or specific to Disc. This makes Library extensible. If some day the library allows patrons to checkout games, instances of a Game template implementing the ILoanable interface can be added to the list of items.
However, at this point, there is still no connection between the Book and Disc templates and the ILoanable interface. Let’s fix that.
4. Implement the interface
There are two ways templates like Book and Disc can implement interfaces like ILoanable.
- The
interface instancecan be added to the template. The following appends aninterface instanceto the previously declaredDisctemplate.
template Disc
with
owner : Party
title : Text
loanedTo : Optional Party
format : DiscFormat -- only on Disc
where
signatory owner
observer loanedTo
-- an interface instance defined on an implementing template
-- (how the interface is "implemented" for the Disc template)
interface instance ILoanable for Disc where
view = LoanableView owner (show format) title loanedTo
loanTo borrower = do
loanedDisc <- create this with loanedTo = Some borrower
return (toInterfaceContractId loanedDisc)
Notice how the view projection and the loanTo functions are defined for the Disc. Also notice that the toInterfaceContractId function is used to convert the ContractId Disc to the expected ContractId ILoanable.
- The
interface instancefor a template can alternatively be added to the interface. The following appends aninterface instanceto the previously declared interface.
-- the interface for things that can be loaned
interface ILoanable where
viewtype LoanableView
-- the functions that must be implemented for all ILoanables
-- (similar to OOP abstract methods)
loanTo : Party -> Update (ContractId ILoanable)
-- a choice that can be exercised on ContractId ILoanable
-- (similar to OOP base class method, calling abstract methods)
choice LoanTo : ContractId ILoanable
with
borrower : Party
controller (view this).owner
do
loanTo this borrower
-- an interface instance defined on the interface itself
-- (how the interface is "implemented" for the Book template)
interface instance ILoanable for Book where
view = LoanableView owner "Book" title loanedTo
loanTo borrower = do
loanedBook <- create this with loanedTo = Some borrower
return (toInterfaceContractId loanedBook)
Notice that the keywords and syntax for the interface instance are identical whether it appears on the template or the interface.
The Daml model is defined. We can now turn our attention to how this model is used.
5. Script the interface-based model
In this section, we add to the demo = script do block.
Previously, we showed the script creating instances of a Book and Disc. If we try to add those directly to the library, it will not compile.
Warning: Does not yet compile
library1 <- submit owner do
createCmd Library with
items = [book, disc],..
The library is expecting ContractId ILoanables. We can use the toInterfaceContractId function. The following compiles:
book <- toInterfaceContractId @ILoanable <$>
submit owner do
createCmd Book with
author = "Tolkien",..
disc <- toInterfaceContractId @ILoanable <$>
submit owner do
createCmd Disc with
format = BluRay,..
library1 <- submit owner do
createCmd Library with
items = [book, disc],..
We can further exercise the library as follows:
-- happy path
library2 <- submit owner do
exerciseCmd library1 Loan with
item = book,..
library3 <- submit owner do
exerciseCmd library2 Loan with
item = disc,..
-- must fail
someone_else <- allocateParty "somone_else"
submitMustFail owner do
exerciseCmd library3 Loan with
item = book
borrower = someone_else
Finally, to add to the example, the ledger can be queried for contracts that implement the ILoanable interface.
import DA.Foldable (forA_)
:
-- build receipt using LoanableViews
-- requires SDK 2.5.0
items <- queryInterface @ILoanable borrower
forA_ items
(\(_, Some i) -> do
let lineItem = i.media <> ": " <> i.title
debug lineItem)
Here are the script results:
Trace:
"BluRay: The Hobbit"
"Book: The Hobbit"
Full Source Code
module Main where
import Daml.Script
import DA.Foldable (forA_)
import DA.List (replace)
data DiscFormat = DVD | BluRay
deriving (Eq, Show)
-- a book can be loaned
template Book
with
owner : Party
title : Text
loanedTo : Optional Party
author : Text -- only on Book
where
signatory owner
observer loanedTo
-- a disc can be loaned
template Disc
with
owner : Party
title : Text
loanedTo : Optional Party
format : DiscFormat -- only on Disc
where
signatory owner
observer loanedTo
-- an interface instance defined on an implementing template
-- (how the interface is "implemented" for the Disc template)
interface instance ILoanable for Disc where
view = LoanableView owner (show format) title loanedTo
loanTo borrower = do
loanedDisc <- create this with loanedTo = Some borrower
return (toInterfaceContractId loanedDisc)
-- data that can be projected (read-only) from both Book and Disc
data LoanableView = LoanableView
with
owner : Party
media : Text
title : Text
loanedTo : Optional Party
-- the interface for things that can be loaned
interface ILoanable where
viewtype LoanableView
-- the functions that must be implemented for all ILoanables
-- (similar to OOP abstract methods)
loanTo : Party -> Update (ContractId ILoanable)
-- a choice that can be exercised on ContractId ILoanable
-- (similar to OOP base class method, calling abstract methods)
choice LoanTo : ContractId ILoanable
with
borrower : Party
controller (view this).owner
do
loanTo this borrower
-- an interface instance defined on the interface itself
-- (how the interface is "implemented" for the Book template)
interface instance ILoanable for Book where
view = LoanableView owner "Book" title loanedTo
loanTo borrower = do
loanedBook <- create this with loanedTo = Some borrower
return (toInterfaceContractId loanedBook)
template Library
with
owner : Party
items : [ContractId ILoanable] -- both books and discs
where
signatory owner
choice Loan : ContractId Library
with
borrower : Party
item : ContractId ILoanable -- a book or disc
controller owner
do
assert (item `elem` items)
loanedItem <- exercise item LoanTo with ..
let newItems = replace [item] [loanedItem] this.items
create this with items = newItems
demo = script do
-- setup
owner <- allocateParty "owner"
borrower <- allocateParty "borrower"
let title = "The Hobbit"
loanedTo = None
book <- toInterfaceContractId @ILoanable <$>
submit owner do
createCmd Book with
author = "Tolkien",..
disc <- toInterfaceContractId @ILoanable <$>
submit owner do
createCmd Disc with
format = BluRay,..
library1 <- submit owner do
createCmd Library with
items = [book, disc],..
-- happy path
library2 <- submit owner do
exerciseCmd library1 Loan with
item = book,..
library3 <- submit owner do
exerciseCmd library2 Loan with
item = disc,..
-- must fail
someone_else <- allocateParty "somone_else"
submitMustFail owner do
exerciseCmd library3 Loan with
item = book
borrower = someone_else
-- build receipt using LoanableViews
-- requires SDK 2.5.0
items <- queryInterface @ILoanable borrower
forA_ items
(\(_, Some i) -> do
let lineItem = i.media <> ": " <> i.title
debug lineItem)
# daml.yaml
#
# sdk-version: 2.4.0
# the following is required for `queryInterface`
sdk-version: 2.5.0-snapshot.20221201.11065.0.caac1d10
name: interface-testing
source: daml
init-script: Setup:setup
navigator-options:
- --feature-user-management=false
version: 0.0.1
dependencies:
- daml-prim
- daml-stdlib
- daml-script
build-options: [--target=1.15]
module Setup where
import Daml.Script
setup = script do
allocatePartyWithHint "owner" (PartyIdHint "owner")
allocatePartyWithHint "borrower" (PartyIdHint "borrower")
return ()
