In this code, they key piece is the delay 0
arrow in the rec
block. To see how it works, it helps to think of values as varying over time and time as chopped into slices. I think of the slices as ‘days’. The rec
block explains how each day’s computation works. It’s organised by value, rather than by causal order, but we can still track causality if we’re careful. Crucially, we must make sure (without any help from the types) that each day’s work relies on the past but not the future. The one-day delay 0
buys us time in that respect: it shifts its input signal one day later, taking care of the first day by giving the value 0. The delay’s input signal is ‘tomorrow’s next
’.
rec output <- returnA -< if reset then 0 else next
next <- delay 0 -< output+1
So, looking at the arrows and their outputs, we’re delivering today’s output
but tomorrow’s next
. Looking at the inputs, we’re relying on today’s reset
and next
values. It’s clear that we can deliver those outputs from those inputs without time travel. The output
is today’s next
number unless we reset
to 0; tomorrow, the next
number is the successor of today’s output
. Today’s next
value thus comes from yesterday, unless there was no yesterday, in which case it’s 0.
At a lower level, this whole setup works because of Haskell’s laziness. Haskell computes by a demand-driven strategy, so if there is a sequential order of tasks which respects causality, Haskell will find it. Here, the delay
establishes such an order.
Be aware, though, that Haskell’s type system gives you very little help in ensuring that such an order exists. You’re free to use loops for utter nonsense! So your question is far from trivial. Each time you read or write such a program, you do need to think ‘how can this possibly work?’. You need to check that delay
(or similar) is used appropriately to ensure that information is demanded only when it can be computed. Note that constructors, especially (:)
can act like delays, too: it’s not unusual to compute the tail of a list, apparently given the whole list (but being careful only to inspect the head). Unlike imperative programming, the lazy functional style allows you to organize your code around concepts other than the sequence of events, but it’s a freedom that demands a more subtle awareness of time.