Example of a Daml interface, step-by-step

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.

  1. The viewtype keyword associates the view type with the interface.
  2. This interface includes one function, loanTo, which will later be implemented on the templates Book and Disc.
  3. 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.

  1. The choice appears similar to the choices you might see on a template. It has a with block, a controller, and a do block.
  2. The choice has access to a value this which represents the contract of the implementing template. In this example this would be either a Book or Disc instance.
  3. The choice has access to a function named view, which returns an instance of the view type. In this example, the view type is a LoanableView. It can be used to get access to the values projected from the implementing type.
  4. 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 of loanTo in the choice body is not exactly the same as the signature of the loanTo defined on the function. The loanTo function available in the choice body includes an additional parameter, for which we pass this.

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.

  1. The interface instance can be added to the template. The following appends an interface instance to the previously declared Disc 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.

  1. The interface instance for a template can alternatively be added to the interface. The following appends an interface 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 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 ()
7 Likes

Need to find a way to add this into the official documentation, or at least have a link that points to this. Thanks for this wonderful write up!

2 Likes