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:
- Why is the evaluation order of solution 1 and 2 different (general concepts)?
- 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:
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]
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(Disclaimer: I don't know PureScript very well, but it looks very much like Haskell and seems to work the same here.)