Compile-time exhaustiveness checking in Common Lisp with Serapeum

vindarel

vindarel

Posted on March 19, 2023

Compile-time exhaustiveness checking in Common Lisp with Serapeum

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))
Enter fullscreen mode Exit fullscreen mode

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))))
Enter fullscreen mode Exit fullscreen mode

=> 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)
Enter fullscreen mode Exit fullscreen mode

And a nice message too.

The syntax is the following:

(ecase-of switch-state state 
  (:state (do-something))
  (:state (do-something)))
Enter fullscreen mode Exit fullscreen mode

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))))
Enter fullscreen mode Exit fullscreen mode

=>

; caught WARNING:
;   (MEMBER :NO) is not a subtype of SWITCH-STATE
Enter fullscreen mode Exit fullscreen mode

We see another possible syntax:

(ecase-of  
  ((:stuck :broken) (do-something)))
Enter fullscreen mode Exit fullscreen mode

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)))
Enter fullscreen mode Exit fullscreen mode

=> Warning

; caught WARNING:
;   Can't check exhaustiveness: cannot determine if (OR (NOT INTEGER)
;                                                       (INTEGER * -1)
;                                                       (INTEGER 1 *)) is the same as T
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
vindarel
vindarel

Posted on March 19, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

OOP Bootcamp 2: The Why of OOP
oop OOP Bootcamp 2: The Why of OOP

November 29, 2024

Mastering Golang Debugging in Emacs
debug Mastering Golang Debugging in Emacs

November 29, 2024

Remaking a rule-engine DSL
lisp Remaking a rule-engine DSL

November 18, 2024

JavaScript tarixi
undefined JavaScript tarixi

November 20, 2024