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
viewtype
keyword associates the view type with the interface. - This interface includes one function,
loanTo
, which will later be implemented on the templatesBook
andDisc
. - This interface includes one choice. This choice can be executed on
ContractId ILoanable
references, 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
with
block, acontroller
, and ado
block. - The choice has access to a value
this
which represents the contract of the implementing template. In this examplethis
would be either aBook
orDisc
instance. - 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 ofloanTo
in the choice body is not exactly the same as the signature of theloanTo
defined on the function. TheloanTo
function 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 instance
can be added to the template. The following appends aninterface instance
to the previously declaredDisc
template.
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 instance
for a template can alternatively be added to the interface. The following appends aninterface instance
to 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 ILoanable
s. 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 ()