A Conditional Situations Function API Pattern

I haven't written for a long time, and before Zach is going to weep my occasional mumblings from Planet Lisp, I thought I better sit down and share some API pattern that I have grown fond of.

The pattern addresses conditional code paths in a function's execution, in particular exceptional situations. Typically, there are the following ways you want to deal with a conditional situation:
  • return a caller-specific value,
  • signal an error as a structured way to transfer control,
  • signal a warning or log a message and continue execution,
  • signal a warning or log a message and return from execution,
  • ignore the conditional situation and just continue execution,
  • or perform some arbitrary caller-specific action.
Using the pattern, we will be able to express all those ways conveniently and concisely in the call to the function.

As illustrative example throughout this posting, we will use JOIN-THREAD. (It was the recent update of SBCL's JOIN-THREAD that reminded me on this pattern.)

JOIN-THREAD waits until the passed thread finishes its execution and returns the values of evaluating the last form in the thread. There are two exceptional situations involved:
  1. as one does not necessarily want to wait forever, a caller can specify a timeout that might expire;
  2. the thread might not have finished gracefully.
The pattern I want to introduce would make it have the following function interface:
(join-thread thread &key timeout on-timeout on-failure)
Where ON-TIMEOUT and ON-FAILURE can be either functions taking a condition object, or a non-function value that would essentially be interpreted as if (CONSTANTLY <the-value>) was passed.

The code invoking the ON-TIMEOUT callback might be written as follows:
(if (functionp on-timeout)
(return-from join-thread (funcall on-timeout <timeout condition>))
(return-from join-thread on-timeout))
Notice that the callback is invoked with a Condition object. Notice further that it's invoked in a tail position of the function being defined. This is true for exceptional situations; in the broader case of conditional situations, the function will usually continue further after having invoked the callback. (Passing a non-function value should result in returning that value even in the case of a mere conditional situations; it makes writing tests much easier that purport to exercise exactly that code path.)

Despite being an exceptional situation (i.e. just continuing is usually impossible), we might still want to provide the caller the ability to proceed further by using Common Lisp's restart system. E.g. to allow a caller to just plainly continue and hang on waiting on the thread forever, we could have written:
(if (functionp on-timeout)
(with-simple-restart (continue "Ignore timeout, hang on thread ~S." thread)
(return-from join-thread (funcall on-timeout <timeout condition>)))
(return-from join-thread on-timeout))
And we can already express a variety of different behaviours. To simply return :TIMEOUT when the timeout expires, we call JOIN-THREAD as follows:
(join-thread <thread> ... :on-timeout ':timeout)
To signal an error on timeout, we call JOIN-THREAD as follows:
(join-thread <thread> ... :on-timeout #'error) ; this should probably be the default
To turn the timeout into a mere warning, it is unfortunately not enough to simply pass #'WARN because WARN is specified to require a condition of subtype Warning.
(join-thread <thread> ... :on-timeout #'warn)  ; caveat: won't do
This restriction on WARN is superfluous but it's there, and we can always define our own variant:
(declaim (inline alert))
(defun alert (datum &rest args)
(let ((condition (apply #'coerce-to-condition datum args)))
(if (not (typep condition 'warning))
(cl:warn "~A" condition)
(cl:warn condition))))

(join-thread <thread> ... :on-timeout #'alert)
Notice that the above call will display a message on timeout and return NIL. To display a message (or log it using some arbitrary log mechanism) and go on waiting, we could use the following functions:
(declaim (inline invoke-and-continue))
(defun invoke-and-continue (function condition)
(funcall function condition)
(continue condition)
(error "BUG: could not find a CONTINUE restart for condition ~S." condition))

(defun alert+continue (condition)
(invoke-and-continue #'alert condition))

(defun logg+continue (condition)
(invoke-and-continue #'logg condition))

(join-thread <thread> ... :on-timeout #'alert+continue)
(join-thread <thread> ... :on-timeout #'logg+continue)
Another example would be ALERT+RESIGNAL which will resignal the condition. This can be useful during debugging when you want to display a condition's underlying message stemming from a certain function call that a handler higher up might catch -- just add :ON-TIMEOUT #'ALERT+RESIGNAL to the call.

If desired, one could add
(defvar *timeout-behaviour* #'error)
(defvar *failure-behaviour* #'error)
and make :ON-TIMEOUT and :ON-FAILURE default to these variables.

So what's nice about this pattern?

After all the CL equivalent would be
(handler-bind ((timeout #'alert+continue))
(join-thread <thread> ...))
(handler-case (join-thread <thread> ...)
(timeout () :timeout))
which may not be as concise but surely is not overly verbose either.

A couple of things.

For one, it's essentially Continuation Passing Style for exceptional situations which can be exactly what you want occasionally. Also the return-a-value case does not involve signaling a condition or an extra function call - which can make a difference. Likewise, handling an exceptional situation does not involve a non-local exit; just a function call and a local exit.

And another thing, it serves as a window of opportunity to document the exceptional situations in the function's docstring, and of course, it helps remembering these situations during automatic lambda list display.

And it's easier to change between different behaviours as it does not require changing a HANDLER-BIND to a HANDLER-CASE or vice versa, or having to add a BLOCK for explicit transfer of control out of a handler.

And, as mentioned before, the conciseness of the return-a-value case is very handy when writing tests.

It also makes code read more like prose. (He said, handwavingly^Wsternly.)


tuscland said...


Thank you for this interesting post.
I was wondering how the "on-failure" condition was catched from the thread in the function JOIN-THREAD. Is this a state variable that is set when the thread exits? Could you please share some details?

All the best,

trittweiler said...

Yeah, in the special case of JOIN-THREAD, each thread would have a slot that contains either a list of the thread's return values or a condition object. JOIN-THREAD would then either use VALUES-LIST on that list, or invoke the ON-FAILURE callback on the condition object.

Sonal Sharma said...

Welcome to top rated Delhi escort service website. We showcase some of the most beautiful models, young college girls for escorts in Delhi

Chang bui thi said...

Any way I'll be subscribing to your feed and I hope you post again soon.
i like play games friv4 online and play games girls Download baixar facebook movel

Frozen Juegos said...

Very helpful advice in this particular post! It’s the little changes that make the largest changes. Thanks for sharing!
Jugar juegos de frozen en lĂ­nea gratis, los nuevos de princesa de Disney juegos frozen - la princesa encantadora y linda. Divertirse frozen!

Facebook Descargar said...

Thanks for sharing this quality information with us. I really enjoyed reading.
download descargar facebook gratis para Android celular and download free descargar facebook apk and descargar facebook gratis , descarga facebook

Online Game said...

You need to kill time, you need entertainment. Refer to our website. hope you get the most comfort.
Thanks you for sharing!
Friv Games

Hieu Nguyen said...

you'd have time to look these kids active. Please visit our website and let us play the game interesting.
Thanks for sharing !
Friv 10
Kizi 10
Yepi 2

Hieu Nguyen said...

You need to kill time, you need entertainment. Refer to our website. hope you get the most comfort.
Thanks for sharing !
Friv 5
Kizi 1
Yepi 3

Seo Expert said...

I’ll bookmark your weblog and check again here SEO Services

Hieu Nguyen said...

When you're tired, you want to relax after a stressful working hours, you need to have time to take care of the kids active.
Please visit our website and play exciting flash games.Thanks you for sharing!
Friv 4

Kizi200 Games said...

Play the Best Free Games! We've picked out the racing games, cooking games, candy crush, games shooting, fashion games, ...
Thanks for sharing !
Kizi 200
Friv 1

Kizi200 Games said...

Play the Best Free Games! We've picked out the racing games, cooking games, candy crush, games shooting, fashion games, ...
Thanks for sharing !
Kizi 200
Friv 1

Jenny Daring said...

Fireboy and Watergirl arrived again to the temple in the forest. 2 players together can help them to find their way out.
happy wheels | strike force heroes | tank trouble 2 | fireboy and watergirl |fireboyandwatergirl | fireboy and watergirl 2 | goodgame empire | slitherio | Tank trouble | happy wheels | Strike Force Heroes
Log in to your account or sign up to create a new account
gmail sign in | create a Gmail account

Hieu Nguyen said...

You want to relax after a stressful working hours. Refer to our website. hope you get the most comfort.
Thanks for sharing !
Friv 100
Friv 8
Friv 2

Online Friv said...

You want to relax after a stressful working hours. Refer to our website. Hope you get the most comfort.
Thanks for sharing !
Kizi 100