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
For what it’s worth, the only things required for something to be an Action
(or Monad
for that matter) are
- You can implement an
instance Action
for it that type-checks
- for all a, k,
pure a >>= k
= k a
(here =
means “program yields same results”, not “compares for equality or identity”)
- for all m,
m >>= pure
= m
- given
a >=> b = \z -> a z >>= b
, for all m, k, h, (m >=> k) >=> h
= m >=> (k >=> h)
- (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