576 lines
25 KiB
Markdown
576 lines
25 KiB
Markdown
---
|
|
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, <code>fix</code> is pretty much <em>the only</em>
|
|
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 <em>normal form</em>,
|
|
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!
|