Jan 30th, 2019 - written by Kimserey with .
Last week a colleague of mine introduced me to the concept of continuation in Racket (the best of Scheme and Lisp - at least that is what racket-lang.org states). I knew about the existence of Lisp but I never really paid attention to what it provided as language features. So I took the bite and started to read the post shared to me on continuations, written by Matt Might and oh boy… was I confused. Everything about the code confused me, the notation, the syntax, and of course the flow of the program itself. What I understood was that this piece was implementing Exceptions by using continuations which was enough to make me want to understand it.
So today, what I am sharing with you is my understanding of this implementation of Exceptions that Matt Might added at the end of his post, using Racket, coming from someone who had zero knowledge in Lisp and Racket. This post isn’t a guide on Racket, but rather an attempt to understand Matt’s post without prior knowledge in Racket.
Here is the implementation of Exceptions in Racket language that we will try to understand:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
; current-continuation: -> continuation
(define (current-continuation) (let/cc k k))
; exception-stack: list[continuation]
(define exception-stack '())
; throw: exception -> unit
(define (throw exception-value)
(define cc (car exception-stack))
(cc (list 'exception exception-value)))
; (try EXP ... catch catch-procedure)
; Runs EXP ... and invokes catch-procedure with
; the value passed to throw.
(define-syntax try
(syntax-rules (catch)
[(_ EXP ... catch catch-procedure)
(let ([cc (current-continuation)])
(cond
[(procedure? cc) (dynamic-wind
(lambda () (set! exception-stack (cons cc exception-stack)))
(lambda () EXP ...)
(lambda () (set! exception-stack (cdr exception-stack))))]
[(pair? cc) (catch-procedure (cadr cc))]))]))
; Example:
(try (try (throw 'foo)
catch
(lambda (exn)
(display "got inner exception: ")
(display exn)
(newline)
(throw 'bar)))
catch
(lambda (exn)
(display "got outer exception: ")
(display exn)
(newline)))
As someone who never seen Lisp or Racket before, it is hard to understand the notation as the term used aren’t familiar terms with more widespread/general languages. Therefore in this first part we will go through the notation and explain some aspects of it.
The first apparent difference with other languages is the extensive usage of parentheses. This notation is named S-expression
for symbolic expression. Everything in parenthesis represents an expression which is executed.
For example when I do (hello)
, if hello
is a procedure, it will be executed. If hello
accepts arguments, we can call it with the arguments with (hello 1 2 3)
.
Also another important point is that ()
and []
are equivalent. They can be used interchangeably to ease reading.
define
and let
The second keyword used is define
. In the code that we’re trying to understand, we use define
in two ways.
The first one:
1
2
; (define id expr)
(define my-value 1)
Binds the result of the expression body to the id my-value
.
While the second usage:
1
2
; (define (head args) body ...+)
(define (f) 1)
Binds a procedure to the id f
. Which can then be executed with (f)
.
We also see let
being used, which creates a local binding while executing the content of the body.
1
2
3
; (let ([id val-expr] ...) body ...+)
(let ([x 1] [y 2])
(printf "~a ~a\n" x y))
x
and y
are initiated with a default value 1
and 2
specified in []
and the body is then evaluated with the value of x
and y
.
list
procedure is used to create a list with initial elements:
1
2
> (list 1 2 3)
'(1 2 3)
We can also use quote
or '
to create a list:
1
2
> '(1 2 3)
'(1 2 3)
We can then add an element an existing list with cons
:
1
2
> (const 10 '(1 2 3))
'(10 1 2 3)
With a list we have few operations procedures available:
car
,cdr
,cadr
(which is basically combining (cdr (car ...))
).1
2
3
4
5
6
7
8
> (car '(1 2 3))
1
> (cdr '(1 2 3))
'(2 3)
> (cadr '(1 2 3))
2
quote
or '
We just saw that quotes could be used to create list. What actually happens is that a quote creates a datum of a single value or a list of datum for an S-expression where a datum is a literal version of the value or s-expression.
1
2
3
4
5
> 'hello
'hello
> '(+ 1 1)
'(+ 1 1)
Another syntax that we see is cond
for condition/clause.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
; (cond cond-clause ...)
; cond-clause = [test-expr then-body ...+]
; | [else then-body ...+]
; | [test-expr => proc-expr]
; | [test-expr]
; procedure
(define (do-something v)
(cond
[(procedure? v) (v)]
[(pair? v) (displayln (cdr v))]))
; results in "print from first clause" as the lambda provided
; is a procedure therefore match the first clause
(do-something (lambda () (displayln "Print from first clause")))
; results in "(2 3 4 5)" printed
(do-something '(1 2 3 4 5))
We start by definining a procedure do-something
which takes a single argument v
. We then create a cond-clause with two clauses:
1
2
3
(cond
[(procedure? v) (v)]
[(pair? v) (displayln (cdr v))])
Both clauses match on the datatype of v
, the first one check if v
is a procedure, and executes (v)
. The second one check if v
is a pair.
Now if we call do-something
with a procedure, the procedure will be executed as it matches the first clause:
1
2
> (do-something (lambda () (displayln "Print from first clause")))
print from first clause
Whereas if we passed a list of values, we would have the second clause matched:
1
2
> (do-something '(1 2 3 4 5))
(2 3 4 5)
The second clause matching on pairs considers '(1 2 3 4 5)
as a pair and executes displayln (cdr v)
as outcome.
If you are thinking why would a list ‘(1 2 3) be considered as a pair?. The answer is that a pair is whatever list where cdr returns a value
. Therefore '()
or '(1)
aren’t pairs while '(1 2)
and '(1 2 3)
are pairs where the car
returns 1
and repectively cdr
returns '(2)
and '(2 3)
.
dynamic-wind
Another important syntax used in the example is dynamic-wind
. dynamic-wind
applies a pre-thunk
and post-thunk
operations and returns the value of the value-thunk
.
1
2
3
4
5
6
7
8
; (dynamic-wind
; pre-thunk
; value-thunk
; post-thunk)
(dynamic-wind
(lambda () (displayln "pre"))
(lambda () (displayln "Hello world"))
(lambda () (displayln "post")))
Executing this construct will yield:
1
2
3
pre
Hello world
post
The pre-thunk
is invoked, then the value-thunk
and lastly the post-thunk
.
Now what is interesting for dynamic-wind
is that pre
and post
are always executed even when we jump stack with continuations.
In this example of Exceptions implementation, dynamic-wind
is used to push an element to a list on pre
, and pop out an element from the same list on post
using set!
which sets a previously defined value.
define-syntax
While define
is used to define a binding of a value or procedure, define-syntax
is used to define a syntax transformer. A syntax transformer expands the id to the expression provided. For example, we could do the following:
1
2
; (define-syntax id expr)
(define-syntax say-hello (lambda (stx) (syntax "Hello")))
This creates a binding to a say-hello
transformer. syntax
can also be written with #'
therefore say-hello
can be written:
1
(define-syntax (say-hello stx) #'"Hello")
Then when we use (say-hello)
, we get "Hello"
.
syntax-rules
define-syntax
can be used with syntax-rules
to transform syntax based on pattern matching rules. For example here we create a syntax hello
with rules allowing the literal keyword in
:
1
2
3
4
5
6
7
8
9
10
11
12
13
; (define-syntax id
; (syntax-rules (literal-id ...)
; [pattern template]
; ...))
(define-syntax hello
(syntax-rules (in)
[(_ name in time)
(cond
[ (equal? "morning" time) (printf "Good Morning, ~a\n" name)]
[ (equal? "evening" time) (printf "Good Evening, ~a\n" name)]
[ else (printf "Time not recognized\n")])]
[(_ name) (printf "Hello ~a\n" name)]
[(_ EXP ...) (printf "~a\n" '(EXP ...))]))
The first pattern matches on (_ name in time)
and checks if time
is either morning
or evening
resulting in the propert print of Good Morning
or Good Evening
.
1
2
3
4
5
6
7
8
> (hello "Kimserey" in "morning")
Good Morning, Kimserey
> (hello "Kimserey" in "evening")
Good Evening, Kimserey
> (hello "Kim" in "")
Time not recognized
The second pattern (_ name)
matches a single name
.
1
2
> (hello "Kim")
Hello Kim
And the last pattern ellipsis (_ EXP ...)
matching the whole expression.
1
2
> (hello "a" 1 2 3 "b")
(a 1 2 3 b)
The last important syntax used in the code is let/cc
. let/cc
creates a binding by providing a continuation as argument. /cc
is a short form for with current-continutation
so let/cc
means let binding with current continuation
.
1
2
> (+ 1 (+ 2 (let/cc k (+ 10 1))))
14
k
being the continuation, if we invoke it, the let
binding will be short circuited.
1
2
> (+ 1 (+ 2 (let/cc k (k 5) (+ 10 1))))
8
Continuations are the main topic of Matt’s post, it is the construct that allows implementation of Exceptions in his example.
A continuation captures the rest of the computation and makes it available as a procedure. (This is greatly simplified as more than the rest of the execution is captured and I can’t proclaim that I understand it fully - but seeing it as a procedure was the best interpretation which allowed me to understand the implementation so I’ll go with it).
So here (+ 1 (+ 2 (let/cc k (+ 10 1))))
, k
is not used therefore the let
binding results in 11
then gets +2
and +1
resulting in 14
.
In the second case, (+ 1 (+ 2 (let/cc k (k 5) (+ 10 1))))
, k
is called with 5
which then short circuits the normal result (+ 10 1)
and result in the let
binding being equal to 5
which then results in 8
.
The continuation changed the control flow by jumping back to where it was defined.
The other important aspect is that the continuation can also be called from outside the let binding
.
1
2
3
4
5
6
7
8
9
10
(define (current-continuation) (let/cc k k))
(define (call) #f)
(let ([cc (current-continuation)])
(cond
[(procedure? cc) (set! call cc)]
[(string? cc) (displayln cc)]))
(call "hello")
Here we define a call
procedure initialized with false
. We then define a let
which sets the current continuation to the beginning of let
as a initial value for cc
as argument.
We then check with a cond-clause whether it was entered with a procedure
as cc
, and if it is, we set call
to the continuation.
If cc
is a string
, we know that call
was invoked with a string as argument, therefore we display the content of cc
.
This allows us to use call
anywhere and therefore rewind & replay the content of let
.
Looking back at the Exception implementation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
; current-continuation: -> continuation
(define (current-continuation) (let/cc k k))
; exception-stack: list[continuation]
(define exception-stack '())
; throw: exception -> unit
(define (throw exception-value)
(define cc (car exception-stack))
(cc (list 'exception exception-value)))
; (try EXP ... catch catch-procedure)
; Runs EXP ... and invokes catch-procedure with
; the value passed to throw.
(define-syntax try
(syntax-rules (catch)
[(_ EXP ... catch catch-procedure)
(let ([cc (current-continuation)])
(cond
[(procedure? cc) (dynamic-wind
(lambda () (set! exception-stack (cons cc exception-stack)))
(lambda () EXP ...)
(lambda () (set! exception-stack (cdr exception-stack))))]
[(pair? cc) (catch-procedure (cadr cc))]))]))
We now know all the syntax used in this code and we also know the concept behind the flow. The two main aspects of Racket allowing us to implement Exceptions are:
Going line by line, we start by defining a procedure which will capture the current continuation when called:
1
(define (current-continuation) (let/cc k k))
Then we define an empty list which will be used as a exception handling stack as try/catch
constructs can be embedded, we can try
and within the try
, try
another time, etc…
1
(define exception-stack '())
Our exception handlers will be current continuations from the beginning of the try/catch
. We will see how we set them later.
Then we define a throw
procedure which takes an exception as argument:
1
2
3
(define (throw exception-value)
(define cc (car exception-stack))
(cc (list 'exception exception-value)))
Our throw
takes the first continuation from the stack with car
and calls it passing in a pair of 'exception
, quoted exception literal, and the exception-value
.
Lastly, we define a syntax try
with a literal catch
:
1
2
3
4
5
6
7
8
9
10
(define-syntax try
(syntax-rules (catch)
[(_ EXP ... catch catch-procedure)
(let ([cc (current-continuation)])
(cond
[(procedure? cc) (dynamic-wind
(lambda () (set! exception-stack (cons cc exception-stack)))
(lambda () EXP ...)
(lambda () (set! exception-stack (cdr exception-stack))))]
[(pair? cc) (catch-procedure (cadr cc))]))]))
The pattern (_ EXP ... catch catch-procedure)
will match a try try-body catch catch-body
construct where EXP ...
will by the try-body
and catch-procedure
will be the catch-body
.
The expression for the syntax-rule is a let
which instantiates a current continuation as argument and checks with a condition whether the argument cc
is a procedure or a pair similarly as we did in our hello
example. (This is actually the idiom for continuation explained by Matt Might in his post)
1
2
3
4
5
6
7
(let ([cc (current-continuation)])
(cond
[(procedure? cc) (dynamic-wind
(lambda () (set! exception-stack (cons cc exception-stack)))
(lambda () EXP ...)
(lambda () (set! exception-stack (cdr exception-stack))))]
[(pair? cc) (catch-procedure (cadr cc))]))
When we enter the condition, if cc
is a procedure, we know that it is the first time the try/catch
is invoked therefore we use dynamic-wind
to add the continuation to the stack, then we return the body of the try
with EXP ...
. Lastly if either the body is executed properly or we jumped out of the execution with a continuation, the unwinding removes the continuation from the stack.
On the other hand if cc
is a pair, it means that the continuation was invoked from throw
by passing in the (list 'exception exception-value)
argument which we then take with cadr
and in turn invoke the catch
handler by doing (catch-procedure (cadr cc))
.
This construct allows throw
to short circuit the try
execution and invoke catch
instead.
When throw
occurs as part of EXP ...
, the continuation which was set during winding of dynamic-wind
is called, the cond-clause is then re-evaluated with cc
being a pair of exception with an exception value which then matches the second clause resulting in the call of the catch-procedure
.
The try
syntax allows developers to write the following try/catch
logic:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(try
(try
(throw 'foo)
catch
(lambda (exn)
(display "got inner exception: ")
(display exn)
(newline)
(throw 'bar)))
catch
(lambda (exn)
(display "got outer exception: ")
(display exn)
(newline)))
; got inner exception: foo
; got outer exception: bar
The logic is executed as followed:
try/catch
continuation is pushed to the stack as part of the winding,try
expression body of the outer try/catch
, containing the inner try/catch
, is executed,try/catch
continuation is pushed to the stack as part of the winding,try
expression body is executed,throw
occurs in the body of the inner try
, which calls the continuation stacked by the inner body,try/catch
removes the continuation from the stack,catch
procedure of the inner try/catch
is executed,throw
occurs in the catch
of the inner body, which calls the continuation stacked by the outer body,try/catch
removes the continuation from the stack,catch
procedure of the outer try/catch
is executed.The result is the following:
1
2
got inner exception: foo
got outer exception: bar
And that concludes today’s post!
Today we explored a blog post written by Matt Might on Continuations. The goal of this post was to explain how Exceptions could be implemented in a language supporting syntax definition and continuations. We started by looking at the syntax of Racket, explaining each aspect of the notation used in the example. We then moved on to see how we could define our own syntax, following with a brief example on how continuations worked. Finally we completed the post by looking at an implementation of Exceptions looking step by step at how the flow jumped when exceptions were raised. I hope you liked this post! See you on the next one!