I had a requirement change come in the other day. It was for a
golang program that made some REST calls to an API. The change was
that if a user did not supply credentials, the program should still
be able to call those parts of the REST API that could handle
anonymous requests. If the user supplied credentials, then all
calls should use them. If the user did not supply credentials, but
attempted to call a part of the API that required them, the program
would panic with missing credentials and exit.
In Lisp, this is good use case for continuation-passing-style. The
routine that fetches the credentials could take two continuations,
one to call with the user-supplied credentials and one to call if no
credentials are found. The second continuation could take a
condition object that indicates why the credentials were not
found.
A caller requesting credentials could call this routine with two
continuations. The first would be a lexical closure that accepted
the credentials as an argument and invoked the relevant API call.
The second could either be a first-class function that accepted the
condition and signalled it (and so the credentials would be
required) or it could be a lexical closure that just invoked the API
without credentials (and so make an anonymous request).
This separates the concerns nicely. The code that fetches the
credentials doesn't have to know whether the credentials are
ultimately required or optional. The code that uses the credentials
doesn't have to know how they were fetched or where they are fetched
from.
But I was working in Golang, not Lisp. Nevertheless, you can write
a limited form of continuation-passing-style in Golang. Golang
supports first-class functions and lexical closures, so you can pass
these down as a continuations. But there are a couple of
problems.
First, there is no tail recursion in Golang. This means that a
stack frame is pushed on each call regardless of whether it is a
continuation or not. You will run out of stack space if you call
several continuation-passing-style routines in a row, especially if
they end up looping. Each subroutine call pushes an "identity"
stack frame that just returns what is returned to it, and these pile
up. When you write continuation-passing-style code in Golang, you
have to be careful to return to direct style often enough to pop
these accumulating identity frames off the stack.
Second, Golang distinguishes between expressions that return a
value and statements that do not. The function definition syntax is
used for both, the difference being whether you declare a return type
or not. A continuation-passing-style function returns a value of
the same type as the continuation returns. For this we use a
generic function that takes a type parameter for the return
type:
func foo[T any](cont func(int) T) T {
... do something ...
return cont(...some integer value...)
}
But what if the continuation is a statement that does not return a
value? In this case, we don't want a return type at all.
func fooVoid (cont func(int)) {
... do something ...
cont(...some integer value...)
return
}
The problem here is that we now need two versions of every
continuation-passing-style routine, one that returns a value and one
that does not, and we have to manually choose which version to use
at each call site. This is tedious and error-prone. It would be
nice to have a "void" type that acts as a placeholder to indicate
that no actual return value is expected.