my blog lives here now
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

155 lines
6.6 KiB

6 years ago
  1. ---
  2. title: Delimited Continuations, Urn and Lua
  3. date: August 1, 2017
  4. ---
  5. As some of you might know, [Urn](https://squiddev.github.io/urn) is my
  6. current pet project. This means that any potential upcoming blag posts
  7. are going to involve it in some way or another, and that includes this
  8. one. For the uninitiated, Urn is a programming language which compiles
  9. to Lua[^1], in the Lisp tradition, with no clear ascendance: We take
  10. inspiration from several Lisps, most notably Common Lisp and Scheme.
  11. As functional programmers at heart, we claim to be minimalists: Urn is
  12. reduced to 12 core forms before compilation, with some of those being
  13. redundant (e.g. having separate `define` and `define-macro` builtins
  14. instead of indicating that the definition is a macro through
  15. a parameter). On top of these primitives we build all our abstraction,
  16. and one such abstraction is what this post is about: _Delimited
  17. continuations_.
  18. Delimited continuations are a powerful control abstraction first
  19. introduced by Matthias Felleisein[^2], initially meant as
  20. a generalisation of several other control primitives such as
  21. `call-with-current-continuation`{.scheme} from Scheme among others.
  22. However, whereas `call-with-current-continuation`{.scheme} captures
  23. a continuation representing the state of the entire program after that
  24. point, delimited continuations only reify a slice of program state. In
  25. this, they are cheaper to build and invoke, and as such may be used to
  26. implement e.g. lightweight threading primitives.
  27. While this may sound rather limiting, there are very few constructs that
  28. can simultaneously be implemented with
  29. `call-with-current-continuation`{.scheme} without also being expressible
  30. in terms of delimited continuations. The converse, however, is untrue.
  31. While `call/cc`{.scheme} be used to implement any control abstraction,
  32. it can't implement any _two_ control abstractions: the continuations it
  33. reifies are uncomposable[^3].
  34. ### Delimited Continuations in Urn
  35. Our implementation of delimited continuations follows the Guile Scheme
  36. tradition of two functions `call-with-prompt` and `abort-to-prompt`,
  37. which are semantically equivalent to the more traditional
  38. `shift`/`reset`. This is, however, merely an implementation detail, as
  39. both schemes are available.
  40. We have decided to base our implementation on Lua's existing coroutine
  41. machinery instead of implementing an ad-hoc solution especially for Urn.
  42. This lets us reuse and integrate with existing Lua code, which is one of
  43. the goals for the language.
  44. `call-with-prompt` is used to introduce a _prompt_ into scope, which
  45. delimits a frame execution and sets up an abort handler with the
  46. specified tag. Later on, calls to `abort-to-prompt` reify the rest of
  47. the program slice's state and jump into the handler set up.
  48. ```lisp
  49. (call/p 'a-prompt-tag
  50. (lambda ()
  51. ; code to run with the prompt
  52. )
  53. (lambda (k)
  54. ; abort handler
  55. ))
  56. ```
  57. One limitation of the current implementation is that the continuation,
  58. when invoked, will no longer have the prompt in scope. A simple way to
  59. get around this is to store the prompt tag and handler in values and use
  60. `call/p`[^4] again instead of directly calling the continuation.
  61. Unfortunately, being implemented on top of Lua coroutines does bring one
  62. significant disadvantage: The reified continuations are single-use.
  63. After a continuation has reached the end of its control frame, there's
  64. no way to make it go back, and there's no way to copy continuations
  65. either (while we have a wrapper around coroutines, the coroutines
  66. themselves are opaque objects, and there's no equivalent of
  67. `string.dump`{.lua} to, for instance, decompile and recompile them)
  68. ### Why?
  69. In my opinion (which, like it or not, is the opinion of the Urn team),
  70. Guile-style delimited continuations provide a much better abstraction
  71. than operating with Lua coroutines directly, which may be error prone
  72. and feels out of place in a functional-first programming language.
  73. As a final motivating example, below is an in-depth explanation of
  74. a tiny cooperative task scheduler.
  75. ```lisp
  76. (defun run-tasks (&tasks) ; 1
  77. (loop [(queue tasks)] ; 2
  78. [(empty? queue)] ; 2
  79. (call/p 'task (car queue)
  80. (lambda (k)
  81. (when (alive? k)
  82. (push-cdr! queue k)))) ; 3
  83. (recur (cdr queue)))) ; 4
  84. ```
  85. 1. We begin, of course, by defining our function. As inputs, we take
  86. a list of tasks to run, which are generally functions, but may be Lua
  87. coroutines (`threads`) or existing continuations, too. As a sidenote,
  88. in Urn, variadic arguments have `&` prepended to them, instead of
  89. having symbols beginning of `&` acting as modifiers in a lambda-list.
  90. For clarity, that is wholly equivalent to `(defun run-tasks (&rest
  91. tasks)`{.lisp}.
  92. 2. Then, we take the first element of the queue as the current task to
  93. run, and set up a prompt of execution. The task will run until it
  94. hits an `abort-to-prompt`, at which point it will be interrupted and
  95. the handler will be invoked.
  96. 3. The handler inspects the reified continuation to see if it is
  97. suitable for being scheduled again, and if so, pushes it to the end
  98. of the queue. This means it'll be the first task to execute again
  99. when the scheduler is done with the current set of working tasks.
  100. 4. We loop back to the start with the first element (the task we just
  101. executed) removed.
  102. Believe it or not, the above is a fully functioning cooperative
  103. scheduler that can execute any number of tasks.[^5]
  104. ### Conclusion
  105. I think that the addition of delimited continuations to Urn brings
  106. a much needer change in the direction of the project: Moving away from
  107. ad-hoc abstraction to structured, proven abstraction. Hopefully this is
  108. the first of many to come.
  109. [^1]: Though this might come off as a weird decision to some, there is
  110. a logical reason behind it: Urn was initially meant to be used in the
  111. [ComputerCraft](https://computercraft.info) mod for Minecraft, which
  112. uses the Lua programming language, though the language has outgrown it
  113. by now. For example, the experimental `readline` support is being
  114. implemented with the LuaJIT foreign function interface.
  115. [^2]: [The Theory and Practice of First-Class
  116. Prompts](http://www.cs.tufts.edu/~nr/cs257/archive/matthias-felleisen/prompts.pdf).
  117. [^3]: Oleg Kiselyov demonstrates
  118. [here](http://okmij.org/ftp/continuations/against-callcc.html#traps)
  119. that abstractions built on `call/cc`{.scheme} do not compose.
  120. [^4]: `call-with-prompt` is a bit of a mouthful, so the alias `call/p`
  121. is blessed.
  122. [^5]: There's a working example [here](/static/tasks.lisp) ([with syntax
  123. highlighting](/static/tasks.lisp.html)) as runnable Urn. Clone the
  124. compiler then execute `lua bin/urn.lua --run tasks.lisp`.
  125. <!-- vim: tw=72
  126. -->