Scoping of variables

How does variable scoping work in Daml? Getting “ambiguous occurrence” errors trying to pass values into an exerciseCmd in Script.

For example:

File:     daml/Workflow.daml
Hidden:   no
Range:    33:120-33:127
Source:   typecheck
Severity: DsError
Message:
  daml/Workflow.daml:33:120: error:
  Ambiguous occurrence ‘painter’
  It could refer to
  either the field ‘painter’,
  imported from ‘Paint’ at daml/Workflow.daml:6:1-12
  (and originally defined at daml/Paint.daml:13:5-11)
  or the field ‘painter’,
  imported from ‘Paint’ at daml/Workflow.daml:6:1-12
  (and originally defined at daml/Paint.daml:23:5-11)
  or the field ‘painter’,
  imported from ‘Paint’ at daml/Workflow.daml:6:1-12
  (and originally defined at daml/Paint.daml:44:5-11)
ERROR: Creation of DAR file failed.

with the Script code being:

acceptOffer : Parties -> Script (ContractId AcceptByOwner)
acceptOffer parties = do
  iouContracts <- fetch @Iou alice
  let iouId = head ious
  offerContracts <- fetch @OfferToPaintHouseByPainter alice
  let offerContract = head offerContracts
  acceptId <- submit parties.alice do
    exerciseCmd offerContract AcceptByOwner with iouId = iouContract; bank = parties.bank; houseOwner = parties.alice; painter = parties.bob
  pure (acceptId)

In this context, this usually means that AcceptByOwner doesn’t have a field painter. Otherwise, can you share enough to reproduce the error?

1 Like

OK. That helped fixed that. Was trying to pass too many parameters to choice execution.

I’m trying to perform the sequence in the standard Canton Examples (Iou and Paint) where it performs the same sequence as:

painterOffersAndHouseOwnerAccepts painter houseOwner bank =
  script do
    painter <- allocateParty painter
    houseOwner <- allocateParty houseOwner
    bank <- allocateParty bank
    let price = Amount {value = 100.0; currency = "USD"}
    iouId <- submit bank do
        createCmd $ Iou with payer = bank; owner = houseOwner; amount = price; viewers = []
    offerId <- submit painter do
        createCmd $ OfferToPaintHouseByPainter with
          painter = painter; houseOwner = houseOwner; bank = bank; amount = price
    painterIouId <- submit houseOwner do
      exerciseCmd offerId AcceptByOwner with iouId
    submit painter do 
      exerciseCmd painterIouId Call

But in this case it is being done through Scripts being called with User context against separate participant nodes (So I have to look up contracts before being able to operate on them).

My equivalent of

    painterIouId <- submit houseOwner do
      exerciseCmd offerId AcceptByOwner with iouId

is now:

acceptOffer : Parties -> Script (ContractId AcceptByOwner)
acceptOffer parties = do
  iouContracts <- query @Iou parties.alice
  let iou = head iouContracts
  let iouId = iou._1
  offerContracts <- query @OfferToPaintHouseByPainter parties.alice
  let offer = head offerContracts
  let offerId = offer._1
  acceptId <- submit parties.alice do
    exerciseCmd offerId AcceptByOwner with iouId = iouId
  pure (acceptId)

This is in the (soon to be public) repo DACH-NY/ex-secure-canton-infra

Your code states that, essentially, acceptId has type ContractId AcceptByOwner (see the type declaration of acceptOffer). However, AcceptByOwner is not a template type, and therefore cannot have a contract ID; it is the type of a choice argument.

By contrast, see in the original example function where AcceptByOwner is a contract ID on which Call is exercised, and the template on which Call is available? Iou. Which should be reflected in the return type of the AcceptByOwner choice being defined to be ContractId Iou.

OK. So the issue was the return type of the Script. I needed to return an Iou.

Thank you.

I guess there’s kind of a philosophical question here: do you need to return that Iou? (This comment is about broader design ideas; feel free to ignore if you’re happy to just have solved the problem.)

Scripts are composable, in exactly the same way you can compose Updates. You can split them into functions and have them return results; the core Script functions like query and submit are themselves whole Scripts that you are just composing into your own bigger Script. For example, I can factor your “query, take the head, take the contract ID” pattern into a script

assumeFirst: forall tpl p. (Template tpl, IsParties p) => p -> Script (ContractId tpl)
assumeFirst party = do
  (headId, _) :: _ <- query @tpl party
  pure headId

-- then you can write in acceptOffer
  iouId <- assumeFirst @Iou parties.alice
  offerId <- assumeFirst @OfferToPaintHouseByPainter parties.alice

-- then you can go back and add better error reporting
-- to assumeFirst if that seems useful

It’s obvious why query and submit are declared to have results; you want to put them on the right side of an <- and use those results for something. It’s also obvious why assumeFirst should have a result; you actually want the contract ID for something.

Is there such a use for acceptOffer, i.e. is the contract ID it returns useful? If not, it’s probably best just to finish the script with pure () and declare the type as Script ().

I agree that no doubt the code could be refactored better. So far this was just to get the based secure deployment (PKI, JWT on a Canton domain with separate participants ) to work. I make no pretence that the Daml side of this is perfect (I am still learning).

I like the proposed function.

The code is in transition and I hadn’t decided whether returning the result and passing into the next function was preferrble to performing the lookup in the next function. So the return value still probably redundant, It does raise questions about how to best pass in parameters to Scripts - do you need a customer data structure or other ways to import required data set.

I don’t know about best, but right now if you want to pass a parameter to a [function that returns a] Script, you have two options:

  1. Your [function that returns a] Script is invoked in Daml code: it behaves just like any other function, you can pass Daml values to it normally.
  2. You want your [function that returns a] Script to be an entry point for daml script commands. In that case, it has to take exactly one argument, and that argument can be provided through --input-file using its JSON representation (complete example here).