--- title: "Time Traveling In Haskell: How It Works And How To Use It" date: 2020-07-30T00:58:10-07:00 tags: ["Haskell"] --- I recently got to use a very curious Haskell technique {{< sidenote "right" "production-note" "in production:" >}} As production as research code gets, anyway! {{< /sidenote >}} time traveling. I say this with the utmost seriousness. This technique worked like magic for the problem I was trying to solve, and so I thought I'd share what I learned. In addition to the technique and its workings, I will also explain how time traveling can be misused, yielding computations that never terminate. ### Time Traveling Some time ago, I read [this post](https://kcsongor.github.io/time-travel-in-haskell-for-dummies/) by Csongor Kiss about time traveling in Haskell. It's really cool, and makes a lot of sense if you have wrapped your head around lazy evaluation. I'm going to present my take on it here, but please check out Csongor's original post if you are interested. Say that you have a list of integers, like `[3,2,6]`. Next, suppose that you want to find the maximum value in the list. You can implement such behavior quite simply with pattern matching: ```Haskell myMax :: [Int] -> Int myMax [] = error "Being total sucks" myMax (x:xs) = max x $ myMax xs ``` You could even get fancy with a `fold`: ```Haskell myMax :: [Int] -> Int myMax = foldr1 max ``` All is well, and this is rather elementary Haskell. But now let's look at something that Csongor calls the `repMax` problem: > Imagine you had a list, and you wanted to replace all the elements of the > list with the largest element, by only passing the list once. How can we possibly do this in one pass? First, we need to find the maximum element, and only then can we have something to replace the other numbers with! It turns out, though, that we can just expect to have the future value, and all will be well. Csongor provides the following example: ```Haskell repMax :: [Int] -> Int -> (Int, [Int]) repMax [] rep = (rep, []) repMax [x] rep = (x, [rep]) repMax (l : ls) rep = (m', rep : ls') where (m, ls') = repMax ls rep m' = max m l ``` In this example, `repMax` takes the list of integers, each of which it must replace with their maximum element. It also takes __as an argument__ the maximum element, as if it had already been computed. It does, however, still compute the intermediate maximum element, in the form of `m'`. Otherwise, where would the future value even come from? Thus far, nothing too magical has happened. It's a little strange to expect the result of the computation to be given to us; it just looks like wishful thinking. The real magic happens in Csongor's `doRepMax` function: ```Haskell doRepMax :: [Int] -> [Int] doRepMax xs = xs' where (largest, xs') = repMax xs largest ``` Look, in particular, on the line with the `where` clause. We see that `repMax` returns the maximum element of the list, `largest`, and the resulting list `xs'` consisting only of `largest` repeated as many times as `xs` had elements. But what's curious is the call to `repMax` itself. It takes as input `xs`, the list we're supposed to process... and `largest`, the value that _it itself returns_. This works because Haskell's evaluation model is, effectively, [lazy graph reduction](https://en.wikipedia.org/wiki/Graph_reduction). That is, you can think of Haskell as manipulating your code as {{< sidenote "right" "tree-note" "a syntax tree," >}} Why is it called graph reduction, you may be wondering, if the runtime is manipulating syntax trees? To save on work, if a program refers to the same value twice, Haskell has both of those references point to the exact same graph. This violates the tree's property of having only one path from the root to any node, and makes our program a DAG (at least). Graph nodes that refer to themselves (which are also possible in the model) also violate the properties of a a DAG, and thus, in general, we are working with graphs. {{< /sidenote >}} performing substitutions and simplifications as necessary until it reaches a final answer. What the lazy part means is that parts of the syntax tree that are not yet needed to compute the final answer can exist, unsimplified, in the tree. Why don't we draw a few graphs to get familiar with the idea? ### Visualizing Graphs and Their Reduction __A word of caution__: the steps presented below may significantly differ from the actual graph reduction algorithms used by modern compilers. In particular, this section draws a lot of ideas from Simon Peyton Jones' book, [_Implementing functional languages: a tutorial_](https://www.microsoft.com/en-us/research/publication/implementing-functional-languages-a-tutorial/). However, modern functional compilers (i.e. GHC) use a much more complicated abstract machine for evaluating graph-based code, based on -- from what I know -- the [spineless tagless G-machine](https://www.microsoft.com/en-us/research/wp-content/uploads/1992/04/spineless-tagless-gmachine.pdf). In short, this section, in order to build intuition, walks through how a functional program evaluated using graph reduction _may_ behave; the actual details depend on the compiler. Let's start with something that doesn't have anything fancy. We can take a look at the graph of the expression: ```Haskell length [1] ``` Stripping away Haskell's syntax sugar for lists, we can write this expression as: ```Haskell length (1:[]) ``` Then, recalling that `(:)`, or 'cons', is just a binary function, we rewrite: ```Haskell length ((:) 1 []) ``` We're now ready to draw the graph; in this case, it's pretty much identical to the syntax tree of the last form of our expression: {{< figure src="length_1.png" caption="The initial graph of `length [1]`." class="small" >}} In this image, the `@` nodes represent function application. The root node is an application of the function `length` to the graph that represents the list `[1]`. The list itself is represented using two application nodes: `(:)` takes two arguments, the head and tail of the list, and function applications in Haskell are [curried](https://en.wikipedia.org/wiki/Currying). Eventually, in the process of evaluation, the body of `length` will be reached, and leave us with the following graph: {{< figure src="length_2.png" caption="The graph of `length [1]` after the body of `length` is expanded." class="small" >}} Conceptually, we only took one reduction step, and thus, we haven't yet gotten to evaluating the recursive call to `length`. Since `(+)` is also a binary function, `1+length xs` is represented in this new graph as two applications of `(+)`, first to `1`, and then to `length []`. But what is that box at the root? This box _used to be_ the root of the first graph, which was an application node. However, it is now a an _indirection_. Conceptually, reducing this indirection amounts to reducing the graph it points to. But why have we {{< sidenote "right" "altered-note" "altered the graph" >}} This is a key aspect of implementing functional languages. The language itself may be pure, while the runtime can be, and usually is, impure and stateful. After all, computers are impure and stateful, too! {{< /sidenote >}} in this manner? Because Haskell is a pure language, of course! If we know that a particular graph reduces to some value, there's no reason to reduce it again. However, as we will soon see, it may be _used_ again, so we want to preserve its value. Thus, when we're done reducing a graph, we replace its root node with an indirection that points to its result. When can a graph be used more than once? Well, how about this: ```Haskell let x = square 5 in x + x ``` Here, the initial graph looks as follows: {{< figure src="square_1.png" caption="The initial graph of `let x = square 5 in x + x`." class="small" >}} As you can see, this _is_ a graph, but not a tree! Since both variables `x` refer to the same expression, `square 5`, they are represented by the same subgraph. Then, when we evaluate `square 5` for the first time, and replace its root node with an indirection, we end up with the following: {{< figure src="square_2.png" caption="The graph of `let x = square 5 in x + x` after `square 5` is reduced." class="small" >}} There are two `25`s in the graph, and no more `square`s! We only had to evaluate `square 5` exactly once, even though `(+)` will use it twice (once for the left argument, and once for the right). Our graphs can also include cycles. A simple, perhaps _the most_ simple example of this in practice is Haskell's `fix` function. It computes a function's fixed point, {{< sidenote "right" "fixpoint-note" "and can be used to write recursive functions." >}} In fact, in the lambda calculus, fix is pretty much the only way to write recursive functions. In the untyped lambda calculus, it can be written as: $$\lambda f . (\lambda x . f (x \ x)) \ (\lambda x . f (x \ x))$$ In the simply typed lambda calculus, it cannot be written in any way, and needs to be added as an extension, typically written as \(\textbf{fix}\). {{< /sidenote >}} It's implemented as follows: ```Haskell fix f = let x = f x in x ``` See how the definition of `x` refers to itself? This is what it looks like in graph form: {{< figure src="fixpoint_1.png" caption="The initial graph of `let x = f x in x`." class="tiny" >}} I think it's useful to take a look at how this graph is processed. Let's pick `f = (1:)`. That is, `f` is a function that takes a list, and prepends `1` to it. Then, after constructing the graph of `f x`, we end up with the following: {{< figure src="fixpoint_2.png" caption="The graph of `fix (1:)` after it's been reduced." class="small" >}} We see the body of `f`, which is the application of `(:)` first to the constant `1`, and then to `f`'s argument (`x`, in this case). As before, once we evaluated `f x`, we replaced the application with an indirection; in the image, this indirection is the top box. But the argument, `x`, is itself an indirection which points to the root of `f x`, thereby creating a cycle in our graph. Traversing this graph looks like traversing an infinite list of `1`s. Almost there! A node can refer to itself, and, when evaluated, it is replaced with its own value. Thus, a node can effectively reference its own value! The last piece of the puzzle is how a node can access _parts_ of its own value: recall that `doRepMax` calls `repMax` with only `largest`, while `repMax` returns `(largest, xs')`. I have to admit, I don't know the internals of GHC, but I suspect this is done by translating the code into something like: ```Haskell doRepMax :: [Int] -> [Int] doRepMax xs = snd t where t = repMax xs (fst t) ``` #### Detailed Example: Reducing `doRepMax` If the above examples haven't elucidated how `doRepMax` works, stick around in this section and we will go through it step-by-step. This is a rather long and detailed example, so feel free to skip this section to read more about actually using time traveling. If you're sticking around, why don't we watch how the graph of `doRepMax [1, 2]` unfolds. This example will be more complex than the ones we've seen so far; to avoid overwhelming ourselves with notation, let's adopt a different convention of writing functions. Instead of using application nodes `@`, let's draw an application of a function `f` to arguments `x1` through `xn` as a subgraph with root `f` and children `x`s. The below figure demonstrates what I mean: {{< figure src="notation.png" caption="The new visual notation used in this section." class="small" >}} Now, let's write the initial graph for `doRepMax [1,2]`: {{< figure src="repmax_1.png" caption="The initial graph of `doRepMax [1,2]`." class="small" >}} Other than our new notation, there's nothing too surprising here. The first step of our hypothetical reduction would replace the application of `doRepMax` with its body, and create our graph's first cycle. At a high level, all we want is the second element of the tuple returned by `repMax`, which contains the output list. To get the tuple, we apply `repMax` to the list `[1,2]` and the first element of its result. The list `[1,2]` itself consists of two uses of the `(:)` function. {{< figure src="repmax_2.png" caption="The first step of reducing `doRepMax [1,2]`." class="small" >}} Next, we would also expand the body of `repMax`. In the following diagram, to avoid drawing a noisy amount of crossing lines, I marked the application of `fst` with a star, and replaced the two edges to `fst` with edges to similar looking stars. This is merely a visual trick; an edge leading to a little star is actually an edge leading to `fst`. Take a look: {{< figure src="repmax_3.png" caption="The second step of reducing `doRepMax [1,2]`." class="medium" >}} Since `(,)` is a constructor, let's say that it doesn't need to be evaluated, and that its {{< sidenote "right" "normal-note" "graph cannot be reduced further" >}} A graph that can't be reduced further is said to be in normal form, by the way. {{< /sidenote >}} (in practice, other things like packing may occur here, but they are irrelevant to us). If `(,)` can't be reduced, we can move on to evaluating `snd`. Given a pair, `snd` simply returns the second element, which in our case is the subgraph starting at `(:)`. We thus replace the application of `snd` with an indirection to this subgraph. This leaves us with the following: {{< figure src="repmax_4.png" caption="The third step of reducing `doRepMax [1,2]`." class="medium" >}} Since it's becoming hard to keep track of what part of the graph is actually being evaluated, I marked the former root of `doRepMax [1,2]` with a blue star. If our original expression occured at the top level, the graph reduction would probably stop here. After all, we're evaluating our graphs using call-by-need, and there doesn't seem to be a need for knowing the what the arguments of `(:)` are. However, stopping at `(:)` wouldn't be very interesting, and we wouldn't learn much from doing so. So instead, let's assume that _something_ is trying to read the elements of our list; perhaps we are trying to print this list to the screen in GHCi. In this case, our mysterious external force starts unpacking and inspecting the arguments to `(:)`. The first argument to `(:)` is the list's head, which is the subgraph starting with the starred application of `fst`. We evaluate it in a similar manner to `snd`. That is, we replace this `fst` with an indirection to the first element of the argument tuple, which happens to be the subgraph starting with `max`: {{< figure src="repmax_5.png" caption="The fourth step of reducing `doRepMax [1,2]`." class="medium" >}} Phew! Next, we need to evaluate the body of `max`. Let's make one more simplification here: rather than substitututing `max` for its body here, let's just reason about what evaluating `max` would entail. We would need to evaluate its two arguments, compare them, and return the larger one. The argument `1` can't be reduced any more (it's just a number!), but the second argument, a call to `fst`, needs to be processed. To do so, we need to evaluate the call to `repMax`. We thus replace `repMax` with its body: {{< figure src="repmax_6.png" caption="The fifth step of reducing `doRepMax [1,2]`." class="medium" >}} We've reached one of the base cases here, and there are no more calls to `max` or `repMax`. The whole reason we're here is to evaluate the call to `fst` that's one of the arguments to `max`. Given this graph, doing so is easy. We can clearly see that `2` is the first element of the tuple returned by `repMax [2]`. We thus replace `fst` with an indirection to this node: {{< figure src="repmax_7.png" caption="The sixth step of reducing `doRepMax [1,2]`." class="medium" >}} This concludes our task of evaluating the arguments to `max`. Comparing them, we see that `2` is greater than `1`, and thus, we replace `max` with an indirection to `2`: {{< figure src="repmax_8.png" caption="The seventh step of reducing `doRepMax [1,2]`." class="medium" >}} The node that we starred in our graph is now an indirection (the one that used to be the call to `fst`) which points to another indirection (formerly the call to `max`), which points to `2`. Thus, any edge pointing to a star now points to the value 2. By finding the value of the starred node, we have found the first argument of `(:)`, and returned it to our mysterious external force. If we were printing to GHCi, the number `2` would appear on the screen right about now. The force then moves on to the second argument of `(:)`, which is the call to `snd`. This `snd` is applied to an instance of `(,)`, which can't be reduced any further. Thus, all we have to do is take the second element of the tuple, and replace `snd` with an indirection to it: {{< figure src="repmax_9.png" caption="The eighth step of reducing `doRepMax [1,2]`." class="medium" >}} The second element of the tuple was a call to `(:)`, and that's what the mysterious force is processing now. Just like it did before, it starts by looking at the first argument of this list, which is the list's head. This argument is a reference to the starred node, which, as we've established, eventually points to `2`. Another `2` pops up on the console. Finally, the mysterious force reaches the second argument of the `(:)`, which is the empty list. The empty list also cannot be evaluated any further, so that's what the mysterious force receives. Just like that, there's nothing left to print to the console. The mysterious force ceases. After removing the unused nodes, we are left with the following graph: {{< figure src="repmax_10.png" caption="The result of reducing `doRepMax [1,2]`." class="small" >}} As we would have expected, two `2`s were printed to the console, and our final graph represents the list `[2,2]`. ### Using Time Traveling Is time tarveling even useful? I would argue yes, especially in cases where Haskell's purity can make certain things difficult. As a first example, Csongor provides an assembler that works in a single pass. The challenge in this case is to resolve jumps to code segments occuring _after_ the jump itself; in essence, the address of the target code segment needs to be known before the segment itself is processed. Csongor's code uses the [Tardis monad](https://hackage.haskell.org/package/tardis-0.4.1.0/docs/Control-Monad-Tardis.html), which combines regular state, to which you can write and then later read from, and future state, from which you can read values before your write them. Check out [his complete example](https://kcsongor.github.io/time-travel-in-haskell-for-dummies/#a-single-pass-assembler-an-example) here. Alternatively, here's an example from my research, which my coworker and coauthor Kai helped me formulate. I'll be fairly vague, since all of this is still in progress. The gist is that we have some kind of data structure (say, a list or a tree), and we want to associate with each element in this data structure a 'score' of how useful it is. There are many possible heuristics of picking 'scores'; a very simple one is to make it inversely propertional to the number of times an element occurs. To be more concrete, suppose we have some element type `Element`: {{< codelines "Haskell" "time-traveling/ValueScore.hs" 5 6 >}} Suppose also that our data structure is a binary tree: {{< codelines "Haskell" "time-traveling/ValueScore.hs" 14 16 >}} We then want to transform an input `ElementTree`, such as: ```Haskell Node A (Node A Empty Empty) Empty ``` Into a scored tree, like: ```Haskell Node (A,0.5) (Node (A,0.5) Empty Empty) Empty ``` Since `A` occured twice, its score is `1/2 = 0.5`. Let's define some utility functions before we get to the meat of the implementation: {{< codelines "Haskell" "time-traveling/ValueScore.hs" 8 12 >}} The `addElement` function simply increments the counter for a particular element in the map, adding the number `1` if it doesn't exist. The `getScore` function computes the score of a particular element, defaulting to `1.0` if it's not found in the map. Just as before -- noticing that passing around the future values is getting awfully bothersome -- we write our scoring function as though we have a 'future value'. {{< codelines "Haskell" "time-traveling/ValueScore.hs" 18 24 >}} The actual `doAssignScores` function is pretty much identical to `doRepMax`: {{< codelines "Haskell" "time-traveling/ValueScore.hs" 26 28 >}} There's quite a bit of repetition here, especially in the handling of future values - all of our functions now accept an extra future argument, and return a work-in-progress future value. This is what the `Tardis` monad, and its corresponding `TardisT` monad transformer, aim to address. Just like the `State` monad helps us avoid writing plumbing code for forward-traveling values, `Tardis` helps us do the same for backward-traveling ones. #### Cycles in Monadic Bind We've seen that we're able to write code like the following: ```Haskell (a, b) = f a c ``` That is, we were able to write function calls that referenced their own return values. What if we try doing this inside a `do` block? Say, for example, we want to sprinkle some time traveling into our program, but don't want to add a whole new transformer into our monad stack. We could write code as follows: ```Haskell do (a, b) <- f a c return b ``` Unfortunately, this doesn't work. However, it's entirely possible to enable this using the `RecursiveDo` language extension: ```Haskell {-# LANGUAGE RecursiveDo #-} ``` Then, we can write the above as follows: ```Haskell do rec (a, b) <- f a c return b ``` This power, however, comes at a price. It's not as straightforward to build graphs from recursive monadic computations; in fact, it's not possible in general. The translation of the above code uses `MonadFix`. A monad that satisfies `MonadFix` has an operation `mfix`, which is the monadic version of the `fix` function we saw earlier: ```Haskell mfix :: Monad m => (a -> m a) -> m a -- Regular fix, for comparison fix :: (a -> a) -> a ``` To really understand how the translation works, check out the [paper on recursive do notation](http://leventerkok.github.io/papers/recdo.pdf). ### Beware The Strictness Though Csongor points out other problems with the time traveling approach, I think he doesn't mention an important idea: you have to be _very_ careful about introducing strictness into your programs when running time-traveling code. For example, suppose we wanted to write a function, `takeUntilMax`, which would return the input list, cut off after the first occurence of the maximum number. Following the same strategy, we come up with: {{< codelines "Haskell" "time-traveling/TakeMax.hs" 1 12 >}} In short, if we encounter our maximum number, we just return a list of that maximum number, since we do not want to recurse further. On the other hand, if we encounter a number that's _not_ the maximum, we continue our recursion. Unfortunately, this doesn't work; our program never terminates. You may be thinking: > Well, obviously this doesn't work! We didn't actually compute the maximum number properly, since we stopped recursing too early. We need to traverse the whole list, and not just the part before the maximum number. To address this, we can reformulate our `takeUntilMax` function as follows: {{< codelines "Haskell" "time-traveling/TakeMax.hs" 14 21 >}} Now we definitely compute the maximum correctly! Alas, this doesn't work either. The issue lies on lines 5 and 18, more specifically in the comparison `x == m`. Here, we are trying to base the decision of what branch to take on a future value. This is simply impossible; to compute the value, we need to know the value! This is no 'silly mistake', either! In complicated programs that use time traveling, strictness lurks behind every corner. In my research work, I was at one point inserting a data structure into a set; however, deep in the structure was a data type containing a 'future' value, and using the default `Eq` instance! Adding the data structure to a set ended up invoking `(==)` (or perhaps some function from the `Ord` typeclass), which, in turn, tried to compare the lazily evaluated values. My code therefore didn't terminate, much like `takeUntilMax`. Debugging time traveling code is, in general, a pain. This is especially true since future values don't look any different from regular values. You can see it in the type signatures of `repMax` and `takeUntilMax`: the maximum number is just an `Int`! And yet, trying to see what its value is will kill the entire program. As always, remember Brian W. Kernighan's wise words: > Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it. ### Conclusion This is about it! In a way, time traveling can make code performing certain operations more expressive. Furthermore, even if it's not groundbreaking, thinking about time traveling is a good exercise to get familiar with lazy evaluation in general. I hope you found this useful!