--- /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).
+
+<div id="toc"></div>
+
+## 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.