DAML borrows a lot of concepts from Haskell. To these borrowed Haskell assets belong
do blocks, the
<- notation, and
return in DAML template choice bodies.
If you somehow understand choice body syntax, but still, you are not 100 % sure (as I was for a long time), maybe reviewing the analogous Haskell syntax in a fairly simple case might help.
Both DAML and Haskell have the concept of
Action. Broadly speaking, an action is something which either succeeds or not, and if it succeeds, it can have some effect, and return some value to the program.
Most importantly, an action is not a function, which means it doesn’t have any input parameters. In this sense, it’s more akin to a value like 3 or “hello, world” – but it’s different from these because of the possibility of failure.
In Haskell, actions are e.g. IO operations, like reading from standard input, writing to standard output, or file reads and writes.
The Introduction to Haskell IO/Actions HaskellWiki article eg. explains the basics of reading from standard input and writing to standard output.
An action which writes the string “hello” to the standard output eg. can be expressed like this:
module Main where main :: IO () main = putStrLn "hello"
Some important things:
As its type signature reveals,
main is an expression. The type signature doesn’t contain any arrows which would indicate that we are seeing a function.
The type signature of
main contains a type parameter, namely
() (also called unit, and can be interpreted as an empty tuple). In itself,
IO is not a type, it always needs to have a type parameter like this, indicating which type of data gets returned to the function by running an IO action. This particular IO action doesn’t return anything to the program.
The expression is built from a function, namely
putStrLn. The type signature of
putStrLn (which you can print out in the GHCi IDE by the
:t putStrLn command) indicates that it takes a string input, and returns an IO action without a return value:
> :t putStrLn putStrLn :: String -> IO ()
As the article puts it, actions are just possibilities: “Actions are like directions. They specify something that can be done. They are not active in and of themselves. They need to be “run” to make something happen. Simply having an action lying around doesn’t make anything happen.”
How do actions are triggered to run? The thing is, that in Haskell only the
main action is run when the program is run, and the
main action needs to have the
IO () type.
For IO actions, the
do notation is used to build up a sequence of actions from individual actions.
Another kind of IO action is the
getLine action, which has a different type from
putStrLn, because its specific trick is to return the string read from standard input to the program:
> :t getLine getLine :: IO String
<- notation is used the get this returned string for further manipulations in a composite action (note that the returned two strings are manipulated using the normal
++ string concatenation operator to create a third string):
main :: IO () main = do putStrLn "Enter two lines" line1 <- getLine -- line1 :: String line2 <- getLine -- line2 :: String putStrLn ("you said: " ++ line1 ++ " and " ++ line2)
The action above prompts the user to enter two lines, gets both lines, combines them, and writes out the result to the standard output. It is run when we run the program, because it is called
main, and has the appropriate type signature, namely
The fact that the
main action cannot have a return value, doesn’t mean that composite action expressions cannot have a return value either. In fact, a
do block always returns its last return value. (That’s why you cannot have a
<- expression as the last line of a
do block. You know this from the DAML Studio as well, where you have “The last statement in a ‘do’ block must be an expression” error message if you violate this rule.)
If we want to use some other return value, we can use the
return statement to create a different one. The IO action cited above, modified in a somewhat contrived way, demonstrates this:
promptTwoLines :: String -> String -> IO String promptTwoLines prompt1 prompt2 = do line1 <- promptLine prompt1 -- line1 :: String line2 <- promptLine prompt2 -- line2 :: String return (line1 ++ " and " ++ line2) main :: IO () main = do both <- promptTwoLines "First line: " "Second line: " putStrLn ("you said " ++ both)
Summarizing the article:
- IO actions are used to affect the world outside of the program.
- Actions take no arguments but have a result value.
- Actions are inert until run. Only one IO action in a Haskell program is run (main).
- Do-blocks combine multiple actions together into a single action.
- Combined IO actions are executed sequentially with observable side-effects.
- Arrows are used to bind action results in a do-block.
- Return is a function that builds actions. It is not a form of control flow!
These are just snippets from the above HaskellWiki article, you should read through it to fully understand the concept.
Note that in Haskell you not only can use
do blocks with IO actions, but with other kinds of monads as well - which is a broader concept, and contains IO actions as a special case. The most common monads besides
IO a are
Maybe a (they all have a type parameter
Just to mention a still quite simple example for a
do block with the list monad in Haskell, the following expression can be used to filter a list and map a function to the rest. Note that for mapping and filtering there are other more convenient tools in Haskell, so you won’t see this often:
doubleEvens :: [Int] -> [Int] doubleEvens xs = do -- the effect is as if you were looping over a list y <- xs guard $ even y let result = 2*y return result > doubleEvens [1..10] [4,8,12,16,20]
In this case the
do block describes an
Now back to DAML.
do blocks in contract templates, just like in Haskell, are there to build a sequence of actions. These are other kinds of actions, not IO actions, but something which is called
Update in DAML. (There is some remote similarity though because ledger updates are also something which “read” and “write” things form and to the ledger, and also can succeed or fail.)
Similarly to IO in Haskell,
Update in DAML is a parametric type class, which needs a type parameter telling which type of data gets returned to the program when the update is run.
create function eg. returns an update, which returns a contract id of some template:
create :: (HasCreate t) => t -> Update (ContractId t) Create a contract based on a template `t`.
You can see this return type as the type annotation of template choices. There is a catch though. You may wonder, if the
create function returns an
Update (ContractId t), why we don’t see the
Update part in the template, eg. in the following choice of the social media messaging app introduced in the introductory part of the documentation:
nonconsuming choice Follow: ContractId User with userToFollow: Party controller username do assertMsg "You cannot follow yourself" (userToFollow /= username) assertMsg "You cannot follow the same user twice" (userToFollow `notElem` following) archive self create this with following = userToFollow :: following
The reason for this is explained here in the documentation:
“If you paid a lot of attention in 3 Data types, you may have noticed that the create statement returns an Update (ContractId Contact), not a ContractId Contact. As a do block always returns the value of the last statement within it, the whole do block returns an Update, but the return type on the choice is just a ContractId Contact. This is a convenience. Choices always return an Update so for readability it’s omitted on the type declaration of a choice.”
<- notation is used in DAML
do blocks in a similar way to Haskell IO actions: we can “unpack” the return value of the individual actions, use them in operations, before wrapping up the result as a return value.
return statement is quite useful when you want to perform a ledger update with more than one result, eg. when splitting an asset, as it is demonstrated in the Composing choices part of the DAML documentation.
On failure and atomic transaction handling:
Haskell monads usually handle failures by implementing the
> :t fail fail :: Monad m => String -> m a
The most common Haskell monads like
Maybe a implement
fail in the following way:
f1 :: IO () f1 = fail "Fail" f2 :: [Int] f2 = fail "Fail" f3 :: Maybe Int f3 = fail "Fail"
> f1 *** Exception: user error (Fail) > f2  > f3 Nothing
For DAML actions, the synonym of the
fail function is the
abort function, which guarantees that DAML transactions, which are composed of actions, always succeed or fail in an atomic way. As explained in the Failing actions part of the docs:
“Not only are Update and Scenario examples of Action, they are both examples of actions that can fail, e.g. because a transaction is illegal or the party retrieved via getParty doesn’t exist on the ledger.
Each has a special action
abort txt that represents failure, and that takes on type Update () or Scenario () depending on context .
Transactions and scenarios succeed or fail atomically as a whole. So an occurrence of an abort action will always fail the entire evaluation of the current Scenario or Update.”