Compile-time exhaustiveness checking in Common Lisp with Serapeum
vindarel
Posted on March 19, 2023
Serapeum is an excellent CL library, with lots of utilities. You should check it out. It provides a case
-like macro, to use on enums, that warns you at compile-time if you handle all the states of that enum.
Example with ecase-of
from its README. First we define the enum:
(deftype switch-state ()
'(member :on :off :stuck :broken))
We write a function that does something depending on the state of a switch object. We use ecase-of
against this enum type to not miss any state.
Below we only check two states, so we'll get a warning (at compile-time, with a C-c C-c
on Slime: the feedback is… instantaneous). Imagine that (state switch)
is a function call that gets the current state of the "switch" variable passed to the function:
(defun flick (switch)
(ecase-of switch-state (state switch)
(:on (switch-off switch))
(:off (switch-on switch))))
=> the warning:
; caught WARNING:
; Non-exhaustive match: (MEMBER :ON :OFF) is a proper subtype of SWITCH-STATE.
; There are missing types: ((EQL :STUCK) (EQL :BROKEN))
; in: DEFUN FLICK
; (CL-USER::STATE CL-USER::SWITCH)
And a nice message too.
The syntax is the following:
(ecase-of switch-state state
(:state (do-something))
(:state (do-something)))
where switch-state
is the enum-type defined above, state
is the current value we are testing.
Another example: you think you fixed the previous example with the following version, but it has a bug. Serapeum catches it:
(defun flick (switch)
(ecase-of switch-state (state switch)
(:no (switch-off switch)) ;; <---- typo!
(:off (switch-on switch))
((:stuck :broken) (error "Sorry, can't flick ~a" switch))))
=>
; caught WARNING:
; (MEMBER :NO) is not a subtype of SWITCH-STATE
We see another possible syntax:
(ecase-of … …
((:stuck :broken) (do-something)))
we can group and handle states together.
Union-types
And we can do the same with union-types, using etypecase-of
:
(defun negative-integer? (n)
(etypecase-of t n
((not integer) nil)
((integer * -1) t)
((integer 1 *) nil)))
=> Warning
; caught WARNING:
; Can't check exhaustiveness: cannot determine if (OR (NOT INTEGER)
; (INTEGER * -1)
; (INTEGER 1 *)) is the same as T
and indeed, we are handling ]∞, -1] U [1, ∞[
(defun negative-integer? (n)
(etypecase-of t n
((not integer) nil)
((integer * -1) t)
((integer 1 *) nil)
((integer 0) nil)))
=> No warning: handle 0.
This all happens at compile time, when we define functions, and the feedback is immediate, thanks to the live Lisp image.
Go play with it!
(ql:quickload "serapeum")
Posted on March 19, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.