Am I right to think that Script actually shouldn't be Action?

It seems to me that the Action (aka monad) type class is not really suitable to mimic the behavior of client applications using the ledger API.

Of course, I can be wrong, but here is my thought process.

Client applications can submit transactions to the ledger API so that the failure of any action contained in the transaction aborts the whole transaction, expressed by the submit function call. The limitation is that the actions within a transaction cannot pass any return value to each other.

This behavior is mimicked by the Commands a type and the way how it can be built using “applicative do” notation, where the “do” block can contain several createCmd and exerciseCmd function calls.

On the other hand, composing several submit function calls within one Script “do” block is not correct, because the Script “do” block builds a monadic composition, which suggests that we can build “supertransactions” from simple transactions from client application API calls which is not the case.

Put differently, a Script do block only mimics correctly the behavior of a client application if it contains one single submit function call.

What does this mean from a testing perspective?

Putting several submit function calls into a Script do block is stricter than the ledger API. If a Script “do” block contains any submit function calls which would be rejected by the ledger, the whole Script will be rejected, although the ledger might accept some transactions.

I don’t think that’s quite right. Script does not bulid up a “supertransaction”. Each submit call is atomic but the whole script is not. You can see thta by having two submits where the second one fails, the first one will still get committed.

So script really just works like any other ledger client which is evidenced by the fact that it can run over the grpc api.

I think you might be getting at the fact that in Daml Studio each script runs in a new ledger. I wouldn’t call that a super transaction though. It is really just completely separate ledgers. You could in theory do that for your regular tests as well and spin up a new ledger each time, it’s just not practical because that’s usually too slow and you’re better off getting isolation from using different parties.

What about this formulation?:

Script tries to be a monad and not a monad at the same time (which is, I think, strange).

Having the Asset template from the skeleton model where the Asset name cannot be an empty string:

type AssetId = ContractId Asset

template Asset
  with
    issuer : Party
    owner  : Party
    name   : Text
  where
    ensure name /= ""
    signatory issuer
    observer owner
    choice Give : AssetId
      with
        newOwner : Party
      controller owner
      do create this with
           owner = newOwner

This script fails, as it is expected from a monad, but the error message states that there would be a committed transaction:

setup : Script AssetId
setup = script do

  alice <- allocatePartyWithHint "Alice" (PartyIdHint "Alice")
  bob <- allocatePartyWithHint "Bob" (PartyIdHint "Bob")

  aliceTV1 <- submit alice do
    createCmd Asset with
      issuer = alice
      owner = alice
      name = "TV1"

  aliceTV2 <- submit alice do
    createCmd Asset with
      issuer = alice
      owner = alice
      name = ""

  bobTV <- submit alice do
    exerciseCmd aliceTV1 Give with newOwner = bob

  submit bob do
    exerciseCmd bobTV Give with newOwner = alice

The error message is after the daml test command:

  Script execution failed on commit at Main:36:15:
  Unhandled exception:
  DA.Exception.PreconditionFailed:PreconditionFailed@f20de1e4e37b92280264c08bf15eca0be0bc5babd7a7b5e574997f154c00cb78
  with
  message =
  "Template precondition violated: Asset {issuer = 'Alice', owner = 'Alice', name = ""}"

  Ledger time: 1970-01-01T00:00:00Z

  Partial transaction:

  Committed transactions:
  TX 0 1970-01-01T00:00:00Z (Main:30:15)
  #0:0
  │ disclosed to (since): 'Alice' (0)
  └─> 'Alice' creates Main:Asset
  with
  issuer = 'Alice'; owner = 'Alice'; name = "TV1"

If I try to start Sandbox with this script as init script, it fails:

Running the initialization script.
Exception in thread "main" com.daml.lf.engine.script.ScriptF$FailedCmd: Command submit failed: FAILED_PRECONDITION: DAML_INTERPRETATION_ERROR(9,9d8c720c): Interpretation error: Error: Unhandled Daml exception: DA.Exception.PreconditionFailed:PreconditionFailed@f20de1e4{ message = "Template precondition violated: Asset {issuer = 'Alice::122063a3c7ed4471794618e76ff673577b89bd00350b90ab9044b404705c3ab5a8d9', owner = 'Alice::122063a3c7ed4471794618e76ff673577b89bd00350b90ab9044b404705c3ab5a8d9', name = ""}" }. Details: Last location: [unknown source], partial transaction: <empty transaction>

I think you’re interpreting things into the word monad here that don’t actually apply. Compare it to something like IO in Haskell. Any operation there can fail and abort your program. Just like submit can fail at any point and abort your script.

If you run your script via daml script against a ledger you will see one transaction committed to your ledger exactly is it should be. If you look at the output of Daml Studio, you will also see that the first transaction got committed. It is just not “persisted” becasue each script run runs with a fresh ledger but that has nothing to do with it being a Monad or not.

1 Like

Ok, that’s true.

If I run the script separately, not as an init script but after Sandbox already runs, I get an error message on the console, but the valid transaction is committed.

Thanks for helping to clarify this.

My takeaway is this:

Monadic composition is not necessarily atomic composition.

One difference between the Update a and the Script a type in Daml is that in Update monadic composition is atomic, in Script it’s not.

Is that correct?

1 Like

Yes that’s correct, monadic composition is definitely not equivalent to atomic composition. It just happens to be the case for Update.

1 Like

Thank you!

For what it’s worth, the only things required for something to be an Action (or Monad for that matter) are

  1. You can implement an instance Action for it that type-checks
  2. for all a, k, pure a >>= k = k a (here = means “program yields same results”, not “compares for equality or identity”)
  3. for all m, m >>= pure = m
  4. given a >=> b = \z -> a z >>= b, for all m, k, h, (m >=> k) >=> h = m >=> (k >=> h)
  5. (some extra, very similar rules for Functor, Applicative that aren’t too interesting for this discussion)

Anything that satisfies all of these rules is an Action, no matter how prosaic or exotic; there is no further meaning to the idea of Action. Anything that falls short is not an Action, but there are no further rules than these.

1 Like

Yes, thank you, Stephen!