If you are not 100% sure about `do` blocks, `<-` notation and `return`, you should read this

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

The <- 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 IO ().

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 [a] and Maybe a (they all have a type parameter a).

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 [Int] monad.

Now back to DAML.

The 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.

The 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.”

The <- 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.

The 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 fail function:


> :t fail

fail :: Monad m => String -> m a

The most common Haskell monads like IO a, [a] and 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.”

4 Likes

Welcome to that exclusive club of people that have written a monad explainer @gyorgybalazsi. Well done!! λ

3 Likes

Well done @gyorgybalazsi, I enjoyed reading it :wave:

2 Likes