4

This has been discussed to death here on SO, but my specific example is escaping me because I feel that my fold shouldn't care whether it's composed right-to-left or left-to-right. This is a solution to day 1 of 2016's Advent of Code which boils down to taking a list of instructions (turn right/left, walk x steps forward), applying them, and giving the taxicab-geometry distance between where you end up and where you started.

I wrote an apply function to handle one step of this journey, which has signature:

data Direction   = North | East | South | West deriving (Enum, Show)
type Location    = (Int, Int)
type Instruction = String

apply :: Direction -> Location -> Instruction -> (Direction, Location)

Assume for the moment that it's implemented correctly (because I tested it and it is. I'll include a runnable example below the fold). I noticed that I can use a fold to apply this to the whole list of instructions, and did

(_, finalLocation) = foldr f (North, (0, 0)) instructions  -- note the foldr.
  where f = (\ins (d, loc) -> apply d loc ins)

Using the right-associated fold here worked, but gave me the wrong answer. When I re-ran it with foldl (and flip f) I got a totally different answer which adventofcode accepted, so I acknowledge that the fold direction is definitely the difference, I just don't know why it's a difference since it seems to me that my code shouldn't care which way the fold happens.

Why am I wrong?


module AdventOfCode where

-- split
import Data.List.Split (splitOn)

day1input :: String
day1input = "L4, R2, R4, L5, L3, L1, R4, R5, R1, R3, L3, L2, L2, R5, R1, L1, L2, \
            \R2, R2, L5, R5, R5, L2, R1, R2, L2, L4, L1, R5, R2, R1, R1, L2, L3, \
            \R2, L5, L186, L5, L3, R3, L5, R4, R2, L5, R1, R4, L1, L3, R3, R1, L1, \
            \R4, R2, L1, L4, R5, L1, R50, L4, R3, R78, R4, R2, L4, R3, L4, R4, L1, \
            \R5, L4, R1, L2, R3, L2, R5, R5, L4, L1, L2, R185, L5, R2, R1, L3, R4, \
            \L5, R2, R4, L3, R4, L2, L5, R1, R2, L2, L1, L2, R2, L2, R1, L5, L3, L4, \
            \L3, L4, L2, L5, L5, R2, L3, L4, R4, R4, R5, L4, L2, R4, L5, R3, R1, L1, \
            \R3, L2, R2, R1, R5, L4, R5, L3, R2, R3, R1, R4, L4, R1, R3, L5, L1, L3, \
            \R2, R1, R4, L4, R3, L3, R3, R2, L3, L3, R4, L2, R4, L3, L4, R5, R1, L1, \
            \R5, R3, R1, R3, R4, L1, R4, R3, R1, L5, L5, L4, R4, R3, L2, R1, R5, L3, \
            \R4, R5, L4, L5, R2"

day1Processed :: [String]
day1Processed = splitOn ", " day1input

data Direction = North | East | South | West deriving (Enum, Show)
type Location = (Int, Int)

-- |'apply' takes your current 'Direction' and 'Location', applies the instruction
-- and gives back a tuple of (newDirection, (new, location))
apply :: Direction -> Location -> String -> (Direction, Location)
apply d' loc (t:num') = (d, step d loc numsteps)
  where d        = turn d' t
        numsteps = read num' :: Int

-- |'distanceBetween' returns the taxicab geometric distance between two 'Location's
distanceBetween :: Location -> Location -> Int
distanceBetween (x1, y1) (x2, y2) = (abs $ x1-x2) + (abs $ y1-y2)


-- |'turn' changes direction based on the received Char
turn :: Direction -> Char -> Direction
turn West  'R' = North
turn North 'L' = West
turn d     'R' = succ d
turn d     'L' = pred d
turn d      _  = d

-- |'step' moves location based on current direction and number of steps
step :: Direction -> Location -> Int -> Location
step North (x, y) s = (x  , y+s)
step East  (x, y) s = (x+s, y)
step South (x, y) s = (x  , y-s)
step West  (x, y) s = (x-s, y)

wrongLocation :: Location
rightLocation :: Location
(_, wrongLocation) = foldr (\x (d, loc) -> apply d loc x) (North, (0, 0)) day1Processed
(_, rightLocation) = foldl (\(d, loc) x -> apply d loc x) (North, (0, 0)) day1Processed

wrongAnswer :: Int
rightAnswer :: Int
wrongAnswer = distanceBetween (0, 0) wrongLocation
rightAnswer = distanceBetween (0, 0) rightLocation
Adam Smith
  • 52,157
  • 12
  • 73
  • 112
  • Not sure why the close vote, since I did provide an MCVE. At the very least, the close reason chosen is incorrect. (Too broad, maybe? "Why do things fold the way they fold" is pretty broad) – Adam Smith Nov 10 '17 at 00:45
  • You seem to have made no effort debugging this yourself. Your code is far from minimal and you give no justification for why different code should produce the same output, just "*it seems to me that my code shouldn't care which way the fold happens*". This leaves us with having to reverse engineer not just your code, but also your thought process to figure out where you went wrong (i.e. *why* you think `foldl` should produce the same result). – melpomene Nov 10 '17 at 00:46
  • @melpomene interesting to hear, since I walk through my debug process in the question and arrived at the correct solution independently and before I posted the question. I'm confused about how the right vs left fold affects my function, which appears (to me) to work independent of fold direction. I'm obviously *wrong* about that, which is why I'm asking the specific, well-defined question. – Adam Smith Nov 10 '17 at 00:49
  • Why do you *think* the fold order shouldn't matter? It certainly matters if the operation is `(-)`, or `div`, or `\x y -> y * 5000`. Fold order doesn't matter if the binary operation is both associative and commutative, while there is absolutely zero reason to assume your function is either of those things. – Silvio Mayolo Nov 10 '17 at 00:50
  • Explain why you think it should work the same with `foldl` and `foldr`. – melpomene Nov 10 '17 at 00:51
  • 1
    By "debugging" I don't mean the solution to the Advent of Code exercise, I mean the problem you're asking about here. I classify this as debugging because it falls under the general "my code does *X* where I expected *Y*" umbrella. – melpomene Nov 10 '17 at 00:53
  • @SilvioMayolo isn't it, though? Maybe that's the point I'm not quite grasping. – Adam Smith Nov 10 '17 at 00:54
  • @melpomene Hmm because I thought that `foldr f == foldl (flip f)`, which I just went to sarcastically respond to you with and found that that's wrong. Maybe I don't understand folding as well as I thought I did. Does `foldr` then pull from the end of the foldable first? So it's applying my instructions in reverse order? – Adam Smith Nov 10 '17 at 00:59
  • I don't know what you mean by "pull". To me, `foldl` and `foldr` are completely different functions. Just look at their source code. – melpomene Nov 10 '17 at 01:01
  • 1
    Just compare two examples of `foldr` and `foldl` on a piece of paper, you'll find that they behave very differently, and `foldr f == foldl (flip f)` is not the way to look at it. – RoadRunner Nov 10 '17 at 02:55
  • 1
    Another thing to think about: if it really were the case that `foldr f` is the same as `foldl (flip f)`, then would both have been included in the language? Probably not, I think: it would be so easy to adapt your code to always use `foldr` whichever direction you really want to fold. Therefore, you should suspect there might be some deeper difference between them causing both to be in the language. – amalloy Nov 10 '17 at 03:09
  • @amalloy, there are plenty of functions in the standard library that differ by less. `($>)` vs `(<$)`, `($)` vs `(&)`, `all` vs `and`, `concat . map` vs `concatMap` ... – luqui Nov 10 '17 at 04:32

1 Answers1

7

Based on the comments, I'd say you have some confusion about the difference between foldl and foldr. I'll try to distinguish those here. Let's look at a minimal example.

foldr f x [a, b, c] = a `f` (b `f` (c `f` x))
foldl g x [a, b, c] = ((x `g` a) `g` b) `g` c

That's the way these functions would expand on a somewhat small list containing three elements. Now, let's suppose g = flip f and see what foldl does then.

foldr f x [a, b, c] = a `f` (b `f` (c `f` x))
foldl (flip f) x [a, b, c] = c `f` (b `f` (a `f` x))

So the order of the list, in a sense, ends up being reversed when you do foldl (flip f) as opposed to foldr f.

Thus, your initial assertion that foldl (flip f) === foldr f is false in general, but we do end up with a rather interesting property in its place. Assuming the list we're working with is finite, it seems that foldl (flip f) x === foldr f x . reverse

Silvio Mayolo
  • 62,821
  • 6
  • 74
  • 116
  • 1
    For fun, [here's a proof](https://github.com/luqui/experiments/blob/master/foldlr.agda#L17) of your "it seems" theorem – luqui Nov 10 '17 at 20:02
  • @luqui I literally spent several hours this morning trying to come up with that (in Agda as well, no less), and I kept hitting roadblocks. Thank you! – Silvio Mayolo Nov 10 '17 at 20:50