Search code examples
typeclasspurescriptdo-notation

Can I always replace do ... pure ... with ado ... in ... in Purescript?


Can I always replace a do-block like this:

do
  ...
  pure ...

where the last line is "pure" something, with an ado-block like this:

ado
  ...
  in ...

?

I know do only requires Bind and not Monad (i.e. Applicative), but we're already using pure so we're already Applicative.


Solution

  • The long answer

    The difference between Applicative (and therefore applicative do - ado) and Monad (bind or do) is that when combining two applicative computations, the second one cannot depend on the result of the first computation. This means:

    do
      x <- getX
      y <- getY
      pure { x, y }
    -- Can be turned into
    ado
      x <- getX
      y <- getY
      in { x, y }
    

    But if y depends on x we cannot do that:

    do
      x <- getX
      y <- getY x
      pure { x, y }
    -- This won't compile
    ado
      x <- getX
      y <- getY x
      in { x, y }
    

    This means the answer to your question is "no".

    Examples

    Let's think about an intuitive example. The Aff Monad allows us to run asynchronous effects, like fetching data from an API. If the requests are independent, we can run them in parallel.

    ado
      user1 <- get "/user/1"
      user2 <- get "/user/2"
      in [user1, user2]
    

    If they depend on each other, we need to run one request after another:

    do
      user1 <- get "/user/1"
      bestFriend <- get ("/user/" <> user1.bestFriendId)
      pure { user1, bestFriend }
    

    Please note, that Aff always runs things sequentially no matter if you use bind or apply, unless we use parallel which turns an Aff into a ParAff. Notice the lack of a Bind/Monad instance for ParAff because two parallel computations cannot depend on each others results! https://pursuit.purescript.org/packages/purescript-aff/7.1.0/docs/Effect.Aff#t:ParAff

    Meaning for the first example to really run in parallel, we have to actually

    ado
      user1 <- parallel $ get "/user/1"
      user2 <- parallel $ get "/user/2"
      in [user1, user2]
    

    Another example I like is parsing/validation. Imagine you are parsing two fields of user input: A date string and an age. We can check both properties an parallel and return two errors if both properties are invalid. But to validate a date, we might first have to check if an input value is a string and then check if the value is a date string. For an example library, check out purescript-foreign

    validated = ado
      date <- do
        str <- isString dateInput
        isDate date
      age <- do
        int <- isInt ageInput
        isPositive int
      in { date, age }
    

    We see that Applicative is much weaker than Monad. But there are intances, where forgoing in the power of Monad can give us other interesting possibilities like parallelisation or multiple errors.