if-let Macro in Common Lisp

I recently came across someone using the macro if-let. I was guessing its purpose is similar to the if let construct in Rust:

let a: Option<_>;
if let Some(x) = a {
    ...
}

I actually really need something like this. Especially when working on this year’s Advent of Code (AOC), where you often encounter situations like this:

(if (gethash a table) 
    (setf something (gethash a table)) 
    (print b))

Here, I kind of need to introduce a let to avoid calling gethash twice:

(let ((v (gethash a table)))
  (if v
      (setf something v)
      (print b)))

I did a bit of searching and found thatone of the most popular Common Lisp libraries, alexandria, already has this feature. So I decided to run some tests with this macro and its sibling, when-let*.


Playing Around

Basic example:

(ql:quickload "alexandria")

(defun provide-value ()
  (if (zerop (mod (random 2) 2))
      t
      nil))

(defun test0 ()
  (alexandria:if-let (x (provide-value))
    (format t "x has value")
    (format t "x has no value")))

Multiple bindings:

(defun test1 ()
  (alexandria:if-let
      ((x (provide-value))
       (y (provide-value)))
    (format t "x and y have values")
    (format t "x = ~a, y = ~a" x y)))

This requires all bindings to have values; otherwise, it won’t execute the then statement.


How About when-let?

(defun test2 ()
  (alexandria:when-let (x (provide-value))
    (format t "x has a value~%")
    (format t "Running code with x having a value")))

What About let*?

Common Lisp already has let*, which allows you to define values that depend on previously defined ones. I couldn’t findif-let*, but I did discover when-let*.

;; (defun test3 ()
;;   (alexandria:if-let* ;; This won't work; there is no `if-let*`
;;       ((x (provide-value))
;;        (y (provide-value))
;;        (yy y))
;;     (format t "x and y have values")
;;     (format t "x = ~a, y = ~a" x y)))

(defun test4 ()
  (alexandria:when-let*
      ((x (provide-value))
       (y (provide-value))
       (yy y))
    (format t "x has value ~a~%" x)
    (format t "y has value ~a~%" y)
    (format t "Running with all values present, yy = ~a~%" yy)))

Wrapping Up

Back to the initial example, I can now write:

(if-let ((v (gethash a table)))
  (setf something v)
  (print b))

Pretty nice, right?

Written on January 8, 2025