Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I really like the "delay" datatype as a way to implement general recursion (i.e. Turing-completeness) in a non-general/non-Turing-complete system like Coq or Agda.

We can think of 'Delay<T>' as being a bit like 'List<T>'. If we follow Lisp's encoding, there are two forms of list: the empty list 'Nil', which doesn't "contain" anything; and the pair 'Cons(x, y)' which contains a value 'x' (the "head" or "car" of the list) and another list 'y' (the "tail" or "cdr", which might be 'Nil').

'Delay' is the other way around: it is either 'Now(x)', which contains the value 'x', or it is 'Later(y)' which contains another delay 'y' (which might be a 'Now'). We can imagine 'Delay<T>' a bit like a list containing a single 'T' value at the end, preceded by an arbitrary amount of NULL elements. I've gone into a bit more depth on such constructions (AKA "initial algebras") in a blog post http://chriswarbo.net/blog/2014-12-04-Nat_like_types.html

This gets interesting if our language allows co-termination (like Agda, Coq, Haskell, etc.). This is where we're allowed to recurse 'inside' our return value. For example, the following function doesn't terminate, since it will always call itself recursively; yet it does co-terminate, since the recursion is happening "inside" the 'Cons'. We don't know what the full result will be, but at every stage we definitely know what the next part will be (a 'Cons' list). We can reach any finite "depth" of the result within a finite amount of time. This is also known as being "productive":

    function countUpFrom(x) {
      return new Cons(x, countUpFrom(x+1));
    }
The following function doesn't co-terminate (whether it always terminates is currently unknown), since (in the non-1 case) we don't 'narrow down' the return value before recursing. As far as we know, this could keep "passing the buck" forever, and we'd never get any closer to knowing what any part of the result looks like:

    function collatz(x) {
      return (x == 1)? 1
                     : collatz(even(x)? x / 2
                                      : 3 * x + 1);
    }
Implementing co-termination requires that the "contents" of a datastructure are calculated on-demand rather than straight away (AKA 'lazily'). We can actually do this in any language with first-class functions by creating (and subsequently forcing) "thunks", although it gets quite verbose, e.g.

    function countUpFrom(x) {
      return new Cons(x, function() { return countUpFrom(x+1); });
    }
I wrote a blog post which uses this to generate Fibonacci numbers in PHP: http://chriswarbo.net/blog/2014-07-23-fib.html

We can use co-termination to implement a Turing machine, or any other generally-recursive system, by simply making a list of each "step":

    function turingMachineList(tm) {
      return Cons(tm,
                  halted(tm)? Nil
                            : turingMachineList(step(tm)));
    }
This will produce a list of each machine state, which might stop (if the machine reaches a halted state) or go on forever. Likewise we can implement a list of Collatz values:

    function collatzList(x) {
      return Cons(x,
                  (x == 1)? Nil
                          : collatzList(even(x)? x / 2
                                               : x * 3 + 1));
    }
If we only care about the "result" then we can use 'Delay' instead of 'List':

    function turingMachineDelay(tm) {
      return halted(tm)? Now(tm)
                       : Later(turingMachineDelay(step(tm)));
    }

    function collatDelay(x) {
      return (x == 1)? Now(1)
                     : Later(collatzList(even(x)? x / 2
                                                : x * 3 + 1));
    }
These will return a value like 'Later(Later(Later(...)))' which will either keep nesting forever, or (if the calculation has an answer) will eventually contain 'Now(answer)'.

'Delay' is nice since we can write programs in pretty much the same way as normal. The only changes are mechanical and obvious:

- If a recursive call isn't "inside" the return value, we wrap it in 'Later(...)' so it is; and wrap any corresponding base-cases in 'Now(...)'.

- If we need to perform some calculation on the result of a 'Delay' (i.e. the value in its 'Now'), we can't do so directly, since there might not be a result (it could be 'Later(Later(...))' forever); and even if there is, we don't know how many 'Later' wrappers to dig through. Instead, we use these values indirectly, by mapping our calculation over the 'Delay' value. This isn't hard: it's exactly the same as mapping over the elements of a list, or over an optional value, etc. Here's what 'map' looks like for 'Delay':

    function mapDelay(f, x) {
      return case x {
          Now(y):   Now(f(y));
        Later(y): Later(mapDelay(f, y));
      };
    }
We can run such programs in two ways:

- We can remove (up to) some finite number of 'Later' wrappers, which is equivalent to running with a timeout. We can always do this inside the total (terminating/co-terminating) language. We might still end up with a 'Later(...)', unless we can prove that the computation will always finish before our timeout (i.e. if we have a termination proof).

- We can use an external program (in a Turing-complete language) as a clock to 'tick' through each step; essentially playing the role of OS or VM (similar to using a trampoline for recursion, in languages which don't eliminate tail-calls). In this sense, 'Delay' is like a language-enforced version of cooperative multitasking, since we must always eventually yield, at which point the OS can pick another delayed value to tick. The ticks are also a convenient place for the user to interrupt/cancel a program.

Of course, knowing that the next tick will be occur within a finite time doesn't guarantee that it will be happen before the heat-death of the universe!

Note that we can also use co-termination to build datatypes which have no base-case, and hence every value must be infinite. This would be pretty useless for 'Delay' (since there is only one infinite value, namely 'Later(Later(...))' forever). The always-infinite equivalent of lists are called "streams", and are defined as having a 'Cons' form but no 'Nil' form.

From the OS/VM perspective above, this lets us write programs which are guaranteed to run forever (e.g. servers which never crash), although they may run out of memory, and responses might take a while (bounded by the busy beaver number for the server's code size + the request size). The classic example is to represent input as an infinite stream, e.g. 'Stream<HTTPRequest>', have a function for constructing a response 'respond : HTTPRequest -> HTTPResponse', and construct a server via 'streamMap(respond, input) : Stream<HTTPResponse>'. If we want a stateful server we can include the new state with our reponse and have the server plumb it through:

    respond : (State, HTTPRequest) -> (State, HTTPResponse)

    server : (State, Stream<HTTPRequest>) -> Stream<HTTPResponse>

    function server(currentState, stream) {
      return case stream {
        StreamCons(request, rest): case respond(currentState, request) {
          (nextState, response): StreamCons(response,
                                            server(nextState, rest));
        };
      };
    }


Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: