Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Fun with Macros: If-Let and When-Let (stevelosh.com)
97 points by stevelosh on July 9, 2018 | hide | past | favorite | 30 comments


I like the macro transformers in R5RS Scheme. Here's how the multi-binding if-let and when-let look:

  (define-syntax if-let
    (syntax-rules ()
      ((if-let ((var value) ...)
              consequent ...)
       (let ((var value) ...)
         (if (and var ...)
             consequent ...)))))
          
  (define-syntax when-let
    (syntax-rules ()
      ((when-let (binding)
                 body ...)
       (if-let (binding)
               (begin body ...)))))


Arc has corrupted me to barely be able to tolerate all those parens.

  (mac iflet (var condition . body)
    `(let ,var ,condition
      (if ,var ,@body)))

  (mac whenlet (var condition . body)
    `(iflet ,var ,condition (do ,@body)))


What kind of project are you working on with Arc? Or are you just having fun for now ;-)


What’s a example of these macros being used usefully? All I can find are contrived examples.

Why not use `if` and `when` directly? Is creating new names for variables — which is what `if-let` and `when-let` seems to contribute — that common of a pattern?

I must be missing something.


They're pure convenience, but they're pretty convenient. It's very common to have patterns like:

  (let ((result (do-some-calculation)))
    (when result
      (operate-on result)))
`when-let' would just skip one "layer" of code, and I believe avoid assigning a local variable if (do-some-calculation) fails. Just a convenience.

(edit: trying to fix formatting)


Never mind, it still binds a local variable (at least in elisp, where I just tried it).


It has to, otherwise it would have to execute the initform twice.


Yeah, that finally clicked afterwards...


I find them pretty useful in Clojure. Note that it’s practically never about assigning a strictly boolean-valued expression (which would indeed be pretty useless) but rather something that is ”truthy” or ”falsy” ie. implicitly convertible to boolean and you want to do something with the actual value if it is truthy. Other languages have similar constructs, eg. C++:

  if(auto p = get_ptr()) ...
And Rust:

  if let Some(value) = get_opt() ...
The latter, permitting arbitrary pattern matching, is especially useful and a common idiom.


We stole it from Swift.

To your parent, the RFC is pretty short and has good motivation: https://github.com/rust-lang/rfcs/blob/master/text/0160-if-l...


> We stole it from Swift.

Stole is probably too harsh a statement given the semantics are pretty different: Swift's if-let has closer semantics to TFA's as it only works with Optional but allows multiple patterns (bindings in TFA's version) while Rust's works with any pattern but only allows a single pattern (AFAIK).


I believe thats right, yeah.

And sure, you could make that argument, but in terms of how it happened, it was largely “oh, that looks good. We should do that.” So maybe we modified it a bit, but still. :)


nothing wrong with stealing! great artists and all that. :-)


Oh, it comes up all the time when doing pattern-matching, which many Lisp programs do a lot of. "Anaphoric" macros like AIF [0] are one solution:

  (awhen (foo 3) (bar it))
Here IT is bound to the result of (FOO 3), provided that's nonnull. I don't really care for this style, but I see a lot of people using it. I have a macro I sometimes use for this purpose called BCOND ("binding COND"), e.g.:

  (bcond ((let ((x (foo 3)))
            x)
          (bar x)))
The rule is that if the test-form of a BCOND clause is a LET form, the scope of the bindings is extended to the end of the clause. I'm not going to say it's beautiful, but it's quite general; in particular, an arbitrary predicate can be applied, not only a nonnull test, and can involve any or all of the bound variables. I wrote the first version of BCOND in 1980, so I think it's safe to say the need it attempts to address is felt with some frequency.

[0] https://www.common-lisp.net/project/anaphora/


> that common of a pattern?

The point of homoiconic languages like lisps, where the language is just the syntax tree, is that you can make your own nodes. `if-let` and `when-let` are patterns, they don't have to be common, the definition of which remove at least one node. It's not any different than the idea of having lots of small functions but at the syntax layer. Do this enough time and your actual business logic starts get pretty terse.


TXR Lisp has iflet, whenlet and condlet built in. They are used in places in the standard library. A bunch of occurrences (of all three!) are in the compiler, for example:

http://www.kylheku.com/cgit/txr/tree/share/txr/stdlib/compil...

Note that they all support multiple bindings, but not the and semantics: only the last binding is tested. Treating a nil value out of any of the bindings as a failure is way too constraining.

It is also permissible to omit the variable name from the last binding; in that case the expression is used as the controlling expression; e.g.

  (whenlet ((a (expr1))
            (b (expr2))) ;; we can delete b, if not needed
    ...)
Therefore, this is possible:

  (whenlet ((x (expr1))
            (y (expr2))
            ...
            ((complex-expr x y ...)))
    ...)
That is, bind some variables (that may or may not be nil: they are not being tested), and then reference them in a complex test expression.


> Why not use `if` and `when` directly? Is creating new names for variables — which is what `if-let` and `when-let` seems to contribute — that common of a pattern?

This seems an awful lot like saying "Why use functions? All that they are doing is giving new names to variables" (although, to be very clear, I realise that there are meaningful differences between the two). To be sure, nothing can be done with these macros that could not be done without them—that's proven by the fact that they are coded as macros in a language that doesn't natively provide them—but it might be harder to read or write, or … whatever metric you value. I think that experience shows that any binding facility that is offered will make someone's programming life happier (although maybe many somebodies' debugging lives less happy).


> What’s a example of these macros being used usefully?

Sure, here are a couple examples I found from grepping my src directory.

roguelikelike game I made for a game jam: https://github.com/sjl/vintage/blob/master/src/main.lisp#L19...

prolog compiler: https://github.com/sjl/temperance/blob/master/src/compiler/3...

cl port of robotfindskitten: https://github.com/sjl/sattyrday/blob/master/src/002-afk/mai...

generative art lib: https://github.com/sjl/flax/blob/master/src/drawing/api.lisp... https://github.com/sjl/flax/blob/master/src/looms/004-turtle...

There are plenty of other things that use various versions of these macros too. ASDF uses them all over the place. And of course Clojure code uses their versions. A Github search for when-let and if-let will give a lot of examples (though you have to wade through all the import matches... unfortunately Github search doesn't let you search for literal strings like "when-let (" to exclude those).

> Why not use `if` and `when` directly?

Convenience, especially when you have multiple in a row:

    (when-let ((a (foo))
               (b (bar))
               (c (baz)))
       (blah a b c))
    ; =>
    (let ((a (foo)))
      (when a
        (let ((b (bar)))
          (when b
            (let ((c (baz)))
              (when c
                (blah a b c)))))))
> Is creating new names for variables — which is what `if-let` and `when-let` seems to contribute — that common of a pattern?

I'm not sure what "new names for variables" means here. `let` (and these convenience macros) defines local variables. People create local variables for things pretty often.


Thanks.

> I'm not sure what "new names for variables" means here.

All the examples I could find were of the template

    (when-let ((name1 scalar1)
               (name2 scalar2)
        ...)
where presumably the `scalar`s were standins for existing named bindings. Same for `if-let`.

I wasn't awake enough to contemplate that the `scalar`s above could probably be arbitrary expressions.

But skimming your blog post again, I do see this example about half-way through:

    (let* ((name (read-string))
           (length (length name)))
      ; ...
      )


clojuredocs[0] has some good examples. I more often use when-some or if-some over when-let or if-let, mainly because I'm processing over collections and I want to do something to the result only if the collection isn't empty (and the function I want to call doesn't like being called with nils or empty collections).

Ultimately it's a convenience macro. Saves you a nesting level.

[0]: https://clojuredocs.org/clojure.core/when-let


Maybe I'm missing the point of the exercise, but since the author is already using Alexandria in these examples, and Alexandria provides when-let and if-let macros as well, then... why not just explain - or at least compare with and discuss - the implementation in Alexandria?

Skipping docstrings, those are exactly:

  (defmacro if-let (bindings &body (then-form &optional else-form))
      (let* ((binding-list (if (and (consp bindings) (symbolp (car bindings)))
                               (list bindings)
                               bindings))
           (variables (mapcar #'car binding-list)))
      `(let ,binding-list
         (if (and ,@variables)
             ,then-form
             ,else-form))))
  
  (defmacro when-let (bindings &body forms)
    (let* ((binding-list (if (and (consp bindings) (symbolp (car bindings)))
                             (list bindings)
                             bindings))
           (variables (mapcar #'car binding-list)))
      `(let ,binding-list
         (when (and ,@variables)
           ,@forms))))


Alexandria's versions are basically the same as the "multiple bindings" versions, about halfway through the post[1]. So they are explained/compared/discussed... I just didn't mention that those versions happen to be in Alexandria. I suppose I could add a note, though calling out a particular library for having a less than ideal implementation seems a little rude.

[1]: They do allow a single binding as a special case, true.


> calling out a particular library for having a less than ideal implementation seems a little rude

Why? As long as you call it "less than ideal", and not say "it sucks" :). Maybe in the end a patch will find its way upstream; I hear that someone is contributing to this library, sometimes.


Would you rob the author the fun of reinvention and rediscovery?


You could save a little work by noting that when is a special case of if and then writing when-let as a special case of if-let. Something like:

  (defmacro when-let (bindings &rest body)
    `(if-let ,bindings
         (progn ,body)))
That's how it's implemented in Emacs at least. Maybe there are some edge cases involving declarations or whatever where this wouldn't work.


You'd need to parse out the declarations, yeah. Otherwise they'd get shoved inside the progn which wouldn't work.


You could also introduce a "locally" form (for those wondering: http://www.lispworks.com/documentation/lw61/CLHS/Body/s_loca...)


Paul Graham called these "anaphoric" macros in On LISP.


The "anaphora" in "anaphoric" typically refers to how it binds the variable "it" automatically, without having to specify the variable name. Since if-let and when-let require you to explicitly specify the variable name, I don't think most people would consider them anaphoric macros. https://en.wikipedia.org/wiki/Anaphoric_macro

If you want actual anaphoric macros in CL, here they are: https://www.common-lisp.net/project/anaphora/


iflet and whenlet are written in C in TXR Lisp.

See the me_iflet_whenlet function in eval.c:

http://www.kylheku.com/cgit/txr/tree/eval.c#n4125




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: