Search code examples
functional-programmingapplicativepurescriptlifting

How to rewrite ado notation as general Applicative lifting, respecting evaluation order?


There seems to be a difference in the evaluation order of applicative do notation / ado vs. applicative lifting via <$>/map on the first argument, and <*>/apply for remaining arguments.

At least, this is what I have read so far and what is reflected in the course of the exercise shown below. Questions:

  1. Why is the evaluation order of solution 1 and 2 different (general concepts)?
  2. How can I rewrite solution 2 (without ado), respecting the preorder assertion from the test?

Given

Exercise from the PureScript by Example book (Chapter 7) can be found here:

3.(Medium) Write a function traversePreOrder :: forall a m b. Applicative m => (a -> m b) -> Tree a -> m (Tree b) that performs a pre-order traversal of the tree. [...] Applicative do notation (ado) is the easiest way to write this function.

Algebraic data type Tree:

data Tree a
  = Leaf
  | Branch (Tree a) a (Tree a)

Test expecting the traverse order [1,2,3,4,5,6,7]:

Assert.equal (1 .. 7)
          $ snd
          $ runWriter
          $ traversePreOrder (\x -> tell [ x ])
          $ Branch (Branch (leaf 3) 2 (leaf 4)) 1 (Branch (leaf 6) 5 (leaf 7))

Note: I am not sure, what tell and runWriter exactly do - this is a copied code block from the exercise.

For illustration - the example tree looks like this:

enter image description here

What I tried

Solution 1: ado (works)

traversePreOrder :: forall a m b. Applicative m => (a -> m b) -> Tree a -> m (Tree b)
traversePreOrder f Leaf = pure Leaf
traversePreOrder f (Branch tl v tr) = ado
  ev <- f v
  etl <- traversePreOrder f tl
  etr <- traversePreOrder f tr
  in Branch etl ev etr

Solution 2: conventional lifting (does not work)

traversePreOrder :: forall a m b. Applicative m => (a -> m b) -> Tree a -> m (Tree b)
traversePreOrder f Leaf = pure Leaf
traversePreOrder f (Branch tl v tr) =
  let
    ev = f v -- I consciously tried to place this evaluation first, does not work
    etl = traversePreOrder f tl
    etr = traversePreOrder f tr
  in
    Branch <$> etl <*> ev <*> etr

This triggers the error:

expected [1,2,3,4,5,6,7], got [3,2,4,1,6,5,7]


Solution

  • let
      ev = f v -- I consciously tried to place this evaluation first, does not work
      etl = traversePreOrder f tl
      etr = traversePreOrder f tr
    in
      Branch <$> etl <*> ev <*> etr
    

    Source order does not matter in functional programming. You could place these let declarations in any order, it would work the same - they would create the same values, and these values would describe the same computation, and will form the same programs when used in the same expressions.

    The "evaluation order" that actually matters here is a property of the applicative functor you're using - the order in which applicative effects are applied. The order is governed by the operators from the Applicative typeclass which you are using here, namely <*>: it is documented to first apply the effects from the left hand side, then the effects from the right hand side. To implement pre-order traversal, you will therefore have to write

    traversePreOrder :: forall a m b. Applicative m => (a -> m b) -> Tree a -> m (Tree b)
        traversePreOrder f Leaf = pure Leaf
        traversePreOrder f (Branch tl v tr) =
          (\ev etl etr -> Branch etl ev etr) <$> f v <*> traversePreOrder f tl <*> traversePreOrder f tr
    

    (Disclaimer: I don't know PureScript very well, but it looks very much like Haskell and seems to work the same here.)