# HG changeset patch # User Steve Losh # Date 1531080701 0 # Node ID 0ad7006a154be0dde614f68a15f45470a65c414b # Parent 08265e9dcdd3b5cb722b4cc80b171bcde69bc489 if-let post diff -r 08265e9dcdd3 -r 0ad7006a154b content/blog/2018/07/fun-with-macros-if-let.markdown --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/content/blog/2018/07/fun-with-macros-if-let.markdown Sun Jul 08 20:11:41 2018 +0000 @@ -0,0 +1,607 @@ ++++ +title = "Fun with Macros: If-Let and When-Let" +snip = "Part 2 in a series of short posts about fun Common Lisp Macros." +date = 2018-07-09T16:05:00Z +draft = false + ++++ + +I haven't been writing much lately because I've been in the process of switching +my life over to Linux from OS X. I finally managed to get my blog +infrastructure running again, so let's take a look at some deceptively-tricky +Common Lisp macros: `when-let` and `if-let` (and their `let*` counterparts). + +
+ +## Introduction + +I first heard of the [`when-let`][clojure-when-let] and +[`if-let`][clojure-if-let] macros when I learned Clojure a few years ago. +They're used like `let` to bind values to variables, but if a value is `nil` +then `when-let` will return `nil` without running the body, and `if-let` will +execute its alternative branch. + +Let's implement them in Common Lisp. Once we're done writing them we'll use +them like this: + +[clojure-if-let]: https://clojuredocs.org/clojure.core/if-let +[clojure-when-let]: https://clojuredocs.org/clojure.core/when-let + +```lisp +(when-let ((name 'steve)) + (format t "Hello, ~A" name)) + +; => Hello, STEVE + +(when-let ((name nil)) + (format t "Hello, ~A" name)) + +; => Nothing printed, nil returned + +(if-let ((name nil)) + (format t "Hello, ~A" name) + (format t "Hello, unnamed person!")) + +; => Hello, unnamed person! +``` + +## A First Attempt + +A simple first attempt at writing `when-let` might look something like this: + +```lisp +(defmacro when-let (binding &body body) + "Bind `binding` and execute `body`, short-circuiting on `nil`. + + This macro combines `when` and `let`. It takes a binding and binds + it like `let` before executing `body`, but if the binding's value + evaluates to `nil`, then `nil` is returned. + + Examples: + + (when-let ((a 1)) + (list a)) + ; => + (1) + + (when-let ((a nil)) + (list a)) + ; => + NIL + + " + (destructuring-bind ((symbol value)) binding + `(let ((,symbol ,value)) + (when ,symbol + ,@body)))) +``` + +A first attempt at `if-let` looks similar: + +```lisp +(defmacro if-let (binding then else) + "Bind `binding` and execute `then` if true, or `else` otherwise. + + This macro combines `if` and `let`. It takes a binding and binds + it like `let` before executing `then`, but if the binding's value + evaluates to `nil` the `else` branch is executed (with no binding + in effect). + + Examples: + + (if-let ((a 1)) + (list a) + 'nope) + ; => + (1) + + (if-let ((a nil)) + (list a) + 'nope) + ; => + NOPE + + " + (destructuring-bind ((symbol value)) binding + `(let ((,symbol ,value)) + (if ,symbol + ,then + ,else)))) +``` + +This is a decent attempt at a first pass, but already there are a couple of +things to note. + +First: we have documentation, with examples! The docstrings are longer than the +macros themselves, and *this is fine*! I always prefer to err on the side of +being more clear than saving space. If you're a little more verbose than +necessary some experts might have to flick their scroll wheels, but if you're +too terse you can leave someone wallowing in confusion. Experts should know how +to skim documentation quickly if they're really experts, so help out the newer +people and be clear! + +I didn't make the `else` branch optional like it is in Common Lisp's `if` +because one-armed `if`s are a stylistic abomination. + +We could have added some `check-type` statements to make sure the `symbol` is +*actually* a symbol, but since it's getting compiled to a `let` the error will +be caught there immediately anyway. + +## Multiple Bindings + +Our first attempt works, but only supports a single binding. Clojure's versions +of these macros quit here, but we can do better. Let's make our macros support +multiple bindings. First we'll upgrade `when-let`: + +```lisp +(defmacro when-let (bindings &body body) + "Bind `bindings` and execute `body`, short-circuiting on `nil`. + + This macro combines `when` and `let`. It takes a list of bindings + and binds them like `let` before executing `body`, but if any + binding's value evaluates to `nil`, then `nil` is returned. + + Examples: + + (when-let ((a 1) + (b 2)) + (list a b)) + ; => + (1 2) + + (when-let ((a nil) + (b 2)) + (list a b)) + ; => + NIL + + " + (let ((symbols (mapcar #'first bindings))) + `(let ,bindings + (when (and ,@symbols) + ,@body)))) +``` + +And now `if-let`: + +```lisp +(defmacro if-let (bindings then else) + "Bind `bindings` and execute `then` if all are true, or `else` otherwise. + + This macro combines `if` and `let`. It takes a list of bindings and + binds them like `let` before executing `then`, but if any binding's + value evaluates to `nil` the `else` branch is executed (with no + bindings in effect). + + Examples: + + (if-let ((a 1) + (b 2)) + (list a b) + 'nope) + ; => + (1 2) + + (if-let ((a nil) + (b 2)) + (list a b) + 'nope) + ; => + NOPE + + " + (let ((symbols (mapcar #'first bindings))) + `(let ,bindings + (if (and ,@symbols) + ,then + ,else)))) +``` + +Note how we've updated the docstrings to be clear about the new behavior: if +*any* binding is `nil`, the alternate case takes over. + +We've also updated the parameter names to be `bindings` (plural). One thing +I love about Common Lisp is that it's a Lisp 2, so you can almost always use +nice names for function parameters instead of mangling them to avoid shadowing +functions (e.g. `(defun filter (function list) ...)` instead of `(defun filter +(fn lst) ...)`). Take advantage of this and give your parameters descriptive, +pronounceable names. You'll thank yourself every time your editor shows you the +arglist. + +If you've read other blog posts about implementing these macros, this is where +they probably stopped. But let's keep going, there's still much more to dig +into! + +## Adding Some Stars + +Now that we've got `if-let` and `when-let`, the obvious next step is to add +`if-let*` and `when-let*`. We could do this by changing the `let` each macro +emits to a `let*`, but before we rush ahead let's think about how people will +use these macros to see if that change would make sense. + +The point of using a `let*` instead of a `let` is so that later variables can +refer back to earlier ones: + +```lisp +(let* ((name (read-string)) + (length (length name))) + ; ... + ) +``` + +The point of using our `when-` and `if-` variants is to short-circuit and escape +on `nil`. With the way our macros are currently written, *all* the variables +get bound before they *all* get checked for `nil` in the `and`. This works for +the `-let` variants but isn't ideal for the new `-let*` variants. If we're +using `when-let*` it would be nice if the later variables could assume the +earlier ones are non-`nil`. + +This means we'll want to bail out *immediately* after the first `nil` value is +detected. This is a little bit trickier than what we've currently got. There +are a number of ways we could do it, but I'll save us from hitting a dead-end +rabbit hole later and implement `when-let*` like this: + +```lisp +(defmacro when-let* (bindings &body body) + "Bind `bindings` serially and execute `body`, short-circuiting on `nil`. + + This macro combines `when` and `let*`. It takes a list of bindings + and binds them like `let*` before executing `body`, but if any + binding's value evaluates to `nil` the process stops and `nil` is + immediately returned. + + Examples: + + (when-let* ((a (progn (print :a) 1)) + (b (progn (print :b) (1+ a))) + (list a b)) + ; => + :A + :B + (1 2) + + (when-let* ((a (progn (print :a) nil)) + (b (progn (print :b) (1+ a)))) + (list a b)) + ; => + :A + NIL + + " + (alexandria:with-gensyms (block) + `(block ,block + (let* ,(loop :for (symbol value) :in bindings + :collect `(,symbol (or ,value + (return-from ,block nil)))) + ,@body)))) +``` + +There are a few things we can talk about here before we move on to `if-let*`. + +First: we've documented the macro. The examples are a little more verbose than +the previous ones, but the added side effects explicitly show the +short-circuiting evaluation. + +This implementation is much less trivial than the ones we've got so far, so +let's look at a macroexpansion to see what's happening: + + +```lisp +(macroexpand-1 + '(when-let* ((a nil) + (b (1+ a))) + (list a b))) +; => +(BLOCK #:BLOCK563 + (LET* ((A (OR NIL (RETURN-FROM #:BLOCK563 NIL))) + (B (OR (1+ A) (RETURN-FROM #:BLOCK563 NIL)))) + (LIST A B))) +``` + +The `when-let*` expands into a `block` wrapped around a `let*`. As we're +binding each variable it's checking for `nil` with `or`. If it ever sees `nil` +it will return from the block immediately to escape. If it never sees `nil` it +will eventually reach the body and return normally. + +We could have used a series of nested `let`s and `if`s here, and it would have +been easier to read. But the fact that all the variables are bound in a single +`let*` will be important later, so you're just going to have to trust me for +now. + +We've named the block with a gensym to avoid clobbering any `nil` block the user +might already have set up. I explicitly specified the `nil` return value in the +`return-from`, but this isn't required because it's optional. + +`if-let*` is a more difficult, because we need to make sure the appropriate +branch gets evaluated: + +```lisp +(defmacro if-let* (bindings then else) + "Bind `bindings` serially and execute `then` if all are true, or `else` otherwise. + + This macro combines `if` and `let*`. It takes a list of bindings and + binds them like `let*` before executing `then`, but if any binding's + value evaluates to `nil` the process stops and the `else` branch is + immediately executed (with no bindings in effect). + + Examples: + + (if-let* ((a (progn (print :a) 1)) + (b (progn (print :b) (1+ a))) + (list a b) + 'nope) + ; => + :A + :B + (1 2) + + (if-let* ((a (progn (print :a) nil)) + (b (progn (print :b) (1+ a)))) + (list a b) + 'nope) + ; => + :A + NOPE + + " + (alexandria:with-gensyms (outer inner) + `(block ,outer + (block ,inner + (let* ,(loop :for (symbol value) :in bindings + :collect `(,symbol (or ,value + (return-from ,inner nil)))) + (return-from ,outer ,then))) + ,else))) +``` + +This is a little hairy, so let's break down what's happening. An `if-let*` will +macroexpand into something like: + +```lisp +(block outer + (block inner + (let* (...bindings...) + (return-from outer then))) + else) +``` + +We set up a pair of blocks and begin binding the variables. If all the bindings +succeed we return the `then` branch from the outermost block (and yes, before +you go check: `return-from` works fine with multiple values). + +If any of the bindings fail we return from the inner block immediately. This +skips all the remaining bindings plus the `then` and continues along to the +`else`, which executes and returns normally. + +A full macroexpansion ends up looking like this: + +```lisp +(macroexpand-1 + '(if-let* ((a nil) + (b (1+ a))) + (list a b) + 'nope)) +; => +(BLOCK #:OUTER568 + (BLOCK #:INNER569 + (LET* ((A (OR NIL (RETURN-FROM #:INNER569))) + (B (OR (1+ A) (RETURN-FROM #:INNER569)))) + (RETURN-FROM #:OUTER568 (LIST A B)))) + 'NOPE) +``` + +It wouldn't be ideal to implement `if-let*` as a nested series of `let`s and +`if`s, because you'd need to duplicate the `else` code at each level. The +nested pair of blocks might be a little harder to understand at first, but they +only include the `else` in a single place (and will be important for another +reason soon). + +## Consistency + +Now that we've got `when-let*` and `if-let*` short-circuiting on each binding, +it probably makes sense to change `when-let` and `if-let` to behave the same +way, instead of checking after all the variables are bound. Although the later +variables don't rely on the earlier ones for these variants, it would be good if +the behavior were consistent. + +To do this we can take our `-let*` versions and change the `let*` inside to +a `let`, update the documentation, and that's it: + +```lisp +(defmacro when-let (bindings &body body) + "Bind `bindings` in parallel and execute `body`, short-circuiting on `nil`. + + This macro combines `when` and `let`. It takes a list of bindings and + binds them like `let` before executing `body`, but if any binding's value + evaluates to `nil` the process stops and `nil` is immediately returned. + + Examples: + + (when-let ((a (progn (print :a) 1)) + (b (progn (print :b) 2)) + (list a b)) + ; => + :A + :B + (1 2) + + (when-let ((a (progn (print :a) nil)) + (b (progn (print :b) 2))) + (list a b)) + ; => + :A + NIL + + " + (alexandria:with-gensyms (block) + `(block ,block + (let ,(loop :for (symbol value) :in bindings + :collect `(,symbol (or ,value + (return-from ,block nil)))) + ,@body)))) + +(defmacro if-let (bindings then else) + "Bind `bindings` in parallel and execute `then` if all are true, or `else` otherwise. + + This macro combines `if` and `let`. It takes a list of bindings and + binds them like `let` before executing `then`, but if any binding's value + evaluates to `nil` the process stops and the `else` branch is immediately + executed (with no bindings in effect). + + Examples: + + (if-let ((a (progn (print :a) 1)) + (b (progn (print :b) 2)) + (list a b) + 'nope) + ; => + :A + :B + (1 2) + + (if-let ((a (progn (print :a) nil)) + (b (progn (print :b) 2))) + (list a b) + 'nope) + ; => + :A + NOPE + + " + (alexandria:with-gensyms (outer inner) + `(block ,outer + (block ,inner + (let ,(loop :for (symbol value) :in bindings + :collect `(,symbol (or ,value + (return-from ,inner nil)))) + (return-from ,outer ,then))) + ,else))) +``` + +## Declarations + +Before we finish, we should make sure we've done things *right*. Something +that's often forgotten when making new control structures with macros is +handling declarations properly. When writing a normal `let`, you can put +declarations immediately inside the body, like this: + +```lisp +(let ((foo (some-function)) + (bar (some-other-function))) + (declare (optimize safety) + (type integer foo) + (type string bar)) + (do-something foo bar)) +``` + +If we think about how our `when-let` (and the `-let*` version) macroexpands +we'll see that we don't need to do anything — it will work fine the way +we've written it: + + +```lisp +(macroexpand-1 + '(when-let ((foo (some-function)) + (bar (some-other-function))) + (declare (optimize safety) + (type integer foo) + (type string bar)) + (do-something foo bar))) +; => +(BLOCK #:BLOCK586 + (LET ((FOO (OR (SOME-FUNCTION) (RETURN-FROM #:BLOCK586 NIL))) + (BAR (OR (SOME-OTHER-FUNCTION) (RETURN-FROM #:BLOCK586 NIL)))) + (DECLARE (OPTIMIZE SAFETY) + (TYPE INTEGER FOO) + (TYPE STRING BAR)) + (DO-SOMETHING FOO BAR))) +``` + +This is why I insisted on implementing the macros with a single `let` binding +all the variables. If we tried to do this with a series of nested `let`s and +`if`s we'd have to try to parse the declarations and put the appropriate ones +for each variable under the corresponding `let`, and this would be an absolute +nightmare (plus you wouldn't even be able to exclude `nil` from the type, +because the `if` wouldn't happen until after the declaration!). + +Unfortunately `if-let` is going to be some more work. Let's think about an +example: + +```lisp +(if-let ((foo (some-function)) + (bar (some-other-function))) + (declare (optimize safety) + (type integer foo) + (type string bar)) + (do-something foo bar) + (do-something-else)) +``` + +We're going to want the declarations to *only* apply to the `then` branch, +because that's the branch that has the variables whose types we might want to +declare. If the user wants some declarations in the `else` branch they can +wrap that branch in a `locally` and add them there. + +We're going to need a way to grab any declarations the user has given out of the +body of the `if-let`. Luckily Alexandria has a function called `parse-body` +that will do this for us. + +```lisp +(defmacro if-let (bindings &body body) + "Bind `bindings` in parallel and execute `then` if all are true, or `else` otherwise. + + `body` must be of the form `(...optional-declarations... then else)`. + + This macro combines `if` and `let`. It takes a list of bindings and + binds them like `let` before executing the `then` branch of `body`, but + if any binding's value evaluates to `nil` the process stops there and the + `else` branch is immediately executed (with no bindings in effect). + + If any `optional-declarations` are included they will only be in effect + for the `then` branch. + + Examples: + + (if-let ((a (progn (print :a) 1)) + (b (progn (print :b) 2))) + (list a b) + 'nope) + ; => + :A + :B + (1 2) + + (if-let ((a (progn (print :a) nil)) + (b (progn (print :b) 2))) + (list a b) + 'nope) + ; => + :A + NOPE + + " + (alexandria:with-gensyms (outer inner) + (multiple-value-bind (body declarations) (alexandria:parse-body body) + (destructuring-bind (then else) body + `(block ,outer + (block ,inner + (let ,(loop :for (symbol value) :in bindings + :collect `(,symbol (or ,value + (return-from ,inner nil)))) + ,@declarations + (return-from ,outer ,then))) + ,else))))) +``` + +Whew! We parse the body with `parse-body`, destructure what's left of it (to +make sure we have our two branches), and shove the declarations where they +belong. + +`if-let*` is exactly the same, but with a `let*` in the macro. I'll let you +write that one yourself. + +## Result + +We've now got `when-let`, `when-let*`, `if-let`, and `if-let*` working properly. +They all support multiple bindings, short-circuit appropriately, handle +declarations correctly, and are documented clearly.