Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Lisp Macros by Example (stopa.io)
161 points by stopachka on Dec 16, 2019 | hide | past | favorite | 38 comments


Really good choice of examples. Makes sense to someone coming from Java, JavaScript or Python background. I have bookmarked this already and I am going to send this article to my friends and colleagues to show them the power of Lisp macros.

By the way, what kind of development environment should I suggest to others who want to get started with Lisp? Very few, if any, use Emacs. Some use Vim. Most use Visual Studio Code. What kind of development environment would be most suitable to this kind of demographics?


Great to hear, thank you for the kind words Yori :)

For a development environment, I would suggest Cursive — it’s a plug-in on top of IntelliJ, and can fit right in for someone used to IDEs.

https://cursive-ide.com/


For CL, the quasi-standard is emacs and slime, probably followed by the vim-equivalent.


Thanks for the suggestions. Anyone here has used the Vim equivalents? There are a few choices like slimv, vlime, vim-slime. What's the difference between them? Are they as good as emacs and slime?


You can check this previous hn thread: https://news.ycombinator.com/item?id=21735148


This article really makes me want to learn lisp. Is there a risk that lisp’s extensibility creates lots of very specific dialects of lisp that can be difficult to share / understand?


Clojure best-practices warn against using macros when a function would do. It tends to complicate debugging and maintainability. However, macros are very powerful, and when they are the right tool for the job they can create magic and afford new ways of looking at a problem.

On the other end of the spectrum, Racket[0] fully embraces the DSL-creating power of macros.

To answer your question: I think that lisp's extensibility can be used to create something difficult to share/understand OR easier to share/understand [1]. Whereas some other languages might be limited by their ability to naturally express a domain-fitted solution, yet they make it easier to avoid creating a cryptic mess. So... lisps don't confuse people, people do.

[0] - https://racket-lang.org/

[1] - a good example of macro use is Compojure's use of a macro for http route definitions. https://github.com/weavejester/compojure#usage


Lisps are a highly readable family of languages, so once you know one Lisp it's pretty easy to read the rest, and very quick to get up to speed in them so you can write your own code in pretty much any other Lisp dialect.

I went from Common Lisp to Chicken Scheme to eLisp and found the transitions very painless.

I've also peeked at code in various other Lisp dialects, and they've always been very readable to me.

If you're asking instead about DSLs written in Lisp, then the relationship of those DSLs themselves to Lisp is up to the author of the DSL in question. They can make the DSL very similar to Lisp or very different. It's entirely up to them.

Most of the Lisp code I've seen has stuck pretty close to Lisp itself, which makes it easy to read for anyone who knows Lisp.


Great to hear this has sparked your curiosity.

Reach out if you end up having any questions!

As to the risk of specific dialects being too difficult to understand: this isn’t the case in practice. There’s a few of reasons for this, but the main one is that, you can’t deviate from lisp’s simple syntax too much — as soon as you do, it’s no longer a lisp, and can’t use macros in the same way


In my experience, yes. The rule to follow is to keep it simple and explicit. I have seen web request handlers that used a list method combination to save one line of code per handler for the price of less experienced devs spending 15 minutes and multiple failed attempts to explain their own 3-line handler the very day after they wrote it (not making this up).


From what I've picked up, creating Domain-Specific Languages is one of the Lisp family's strong points. Whether its a good thing or a bad thing is another matter.



Actually yes. The biggest issue I've had with using Common Lisp on a software team is everybody building their own languages. The remedy for this is to agree on standard macros and practices that everyone will abide by. Easier said than done, since CL programmers all come to the table with their own particular ways of doing things.


It should be noted that the first example can be implemented in C as well:

    static inline void *null_throws_helper(void *p, const char *msg) {
      if (!p) {
        fputs("uh oh: ", stderr);
        fputs(msg, stderr);
        fflush(stderr);
        abort();
      }
      return p;
    }
    #define nullthrows(arg) null_throws_helper((arg), #arg)
In C with the GNU statement expression extension, the above can be more concisely written:

    #define nullthrows(arg)                                                        \
      ({                                                                           \
        void *p = (arg);                                                           \
        if (!p) {                                                                  \
          fputs("uh oh: ", stderr);                                                \
          fputs(#arg, stderr);                                                     \
          fflush(stderr);                                                          \
          abort();                                                                 \
        }                                                                          \
        p;                                                                         \
      })
    
With C++, you can do better by preserving the original type of pointer, or throw an actual exception rather than terminating the program.

The C/C++ preprocessor macro system isn't great, but it's good enough to be really useful. For great design, I'd lean towards the hygienic syntax-rules system used by Scheme.


Complex macros needs to allow breaking hygiene unless you have a particularly masochistic stroke. I am a very big fan of r6rs syntax-case which I find is the sweet spot between the comfort of being able to break hygiene and still being able to trust your macro output is not shadowing any bindings.


In CL one uses gensym to introduce a macro-local symbol, or, for multiple symbols, with-gensyms as provided by the Alexandria library. No need for unhygienic macros at all.


Defmacro can be implented in 7 lines of syntax case, and gensym is in syntax case as well. The difference is that CL has you jumping through hoops to have something resembling hygiene, whereas it is default in scheme.

It is one of many ideological differences, and I dont understand why that one has become so divisive.


I'm not a very good programmer, but this article was really well laid out and easy to read. I'm slowly learning Lisp to hack on Emacs and org-mode/org-babel, and I wish more people wrote stuff like this article!


This made my day to read. Thank you :)


This snippet doesn't look right

  macro nullthrows(sourceCodeSnippet) {
    return code`
      const result = ${sourceCodeSnippet}; 
      if (result === null || result === undefined) {
        return result
      } else {
        throw new Error("Uh oh, this returned null:" + "${sourceCodeSnippet}");
    `;
  }
The first and the second occurrences of ${sourceCodeSnippet} look the same so I suppose it will just be evaluated twice. You don't want to evaluate the second occurrence, right? The lisp version doesn't have this problem.


In this snippet parentheses are in the wrong place

  (defmacro nil-throws [form]
    `(let [result# ~form] ;; assign the evaluation of form to result#
        (if (nil? result#)
          (throw
            (ex-info "uh oh, we got nil!" {:form '~form}) ;; save form for inspection
          result#))))
I guess, should be

  (defmacro nil-throws [form]
    `(let [result# ~form] ;; assign the evaluation of form to result#
        (if (nil? result#)
          (throw
            (ex-info "uh oh, we got nil!" {:form '~form})) ;; save form for inspection
          result#)))


Great catch! Updated, thank you :)


I think it's fine.

In the first occurrence, $(sourceCodeSnippet) is replaced in the code, and later evaluated (and the result of this evaluation will be passed to the const result).

In the second occurrence, it is replaced inside a string, so it doesn't get evaluated, but the source code is just treated as a string (and displayed in the error message).

Note that the macro nullthrows is just generating some code depending of sourceCodeSnippet, and this code will be evaluated later.


Since this is a pseudo code it's hard to tell what the intended semantics are, but if as you say "the source code is just treated as a string", then it's still not right. What if sourceCodeSnippet contains double quotes? OP conveniently uses single quotes in their example

  getUser(db, ‘billy’)
The whole point of lisp macros is that code is not treated as a string.


Hi perfunctory, thanks for the thoughtful read : ).

I'll update that snippet to include something like a `stringify` function, to make the intent more clear.

In terms of the intended semantics: - In some ways my intent was to show these limitations: unless we get code as data structures, our macros would be very brittle.


I understand that it's a powerful feature, but the fact that its overuse is so strongly warned against, and (apparently) so rarely needed makes me wonder if in practice it is that useful.

How often do rank-and-file lisp developers independently discover reusable programming abstractions that are only possible via macros? Or does the main benefit lie in not having to wait for the designers of your programming language to implement a feature that could be a library?

I would really appreciate some more stories of how people use macros in their day-to-day work!


> Or does the main benefit lie in not having to wait for the designers of your programming language to implement a feature that could be a library?

Precisely. And you are right, it's very hard to write useful, robust, reusable macros.


This article is almost but not quite readable without enabling javascript in the browser. All of the code examples are missing from the non-javascript version.


Thanks for the feedback pmoriarty!

What kind of code examples would you have liked to see?


Just the examples you already have in the article.

For example, for "Example 1: nullthrows" there's no code for that example that I can see without enabling javascript in my browser.

Or when you write: "Here's how we could implement that as a function in javascript:" there's no code that I can see there either.

It's the same for every other code block in the article. All invisible without javascript.


Ah, understood, thank you for clarifying!

Indeed, this is an unfortunate side-effect for medium: the only way to show code examples is to embed github gists.

May end up moving to some other platform.


Another downside of the gists is that it breaks in reader mode. The code samples just don't appear at all :(


Indeed!

I plan on moving off of medium. Hacking with the folks at OneGraph to create a blog in a pretty novel way. Stay tuned!


Nice article! If you like this, you might like the Julia language. It has "lisp macros" but generally has "python-like" syntax

Edit: here's a link to docs https://docs.julialang.org/en/v1/manual/metaprogramming/inde...


Missing closing parenthesis:

  createBill(addToCart(cart, updatePrice(item, 100))


Updated, thanks tzs!


Great relatable description with a motivating example.


Thank you for the kind words :)




Consider applying for YC's Summer 2026 batch! Applications are open till May 4

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

Search: