← Back to context

Comment by capableweb

3 years ago

Personally, the biggest benefit of lisps is that features to the language can be added by not only the language designers, but by the users.

JavaScript wanted to add the synthetic sugar for async/await, so the language had to be redesigned with that in mind. In a lisp, async/await could be implemented by a library instead, keeping the core still small and nimble.

This of course is also a foot gun when used improperly, where some codebases can be harder for new developers to grok everything, as they basically have to learn a new language built on top of the standard language. But I've found that if you keep the "new language features" to a minimum and have good documentation, developers quickly get onboard and start to be productive.

That footgun doesn't seem to be a huge problem. The culture around macros seems to be "avoid if possible, use if necessary". It just becomes a skill like any other related to programming.

The alternative is _extremely costly_ in comparison. Code generators, transpilers, configuration, tooling, bespoke IDE features... All of that, diverging from each other in the most idiosyncratic ways and it all needs version control, RFCs, release management, documentation, design and development, breaking changes...

But with an extensible language you have all of this for basically free. People just make things, share things and the most useful and stable things bubble up.

  • The culture around macros should be that if you're making any kind of halfway complex library, you better have macros to simplify the common uses, and make the code shorter and more readable.

    The users of the library having to write those macros is the negative situation; anticipate the kinds of macros users will want, and provide them.

    If you use general macros over basic Lisp, try to use famous ones. E.g. for gensyms, use with-gensyms. If you don't use Norvig's or Graham's exact implementation, at least make yours 100% compatible.

Javascript programmers have to learn frameworks. You cannot escape from the fact that programs extend the language.

When you write a function, or define a new type, you're extending a language.

In human languages, nobody would dispute the idea that the formation of new nouns and verbs extends the language.

Lisp is kind of like a language where you can add a new conjunction and preposition, not just a new noun or verb. To the uninitiated, that somehow seems like it must be harder.

The fundamental problem is that you have some piece of syntax, and it denotes some process (or whatever) which is entirely implicit. Whether that syntax is a new kind of macro-statement or function call is immaterial. If you have no idea what it is, you have to go to the definition.

The behavior behind a single function call can be mind-bogglingly complex.

  • I recently returned to a Common Lisp program I have not touched for 9 years. It makes some use of the regex engine cl-ppcre (the > nine-year-old-version planted into the program), and I had to touch a few bits of code where I needed to introduce some new uses of that.

    I had made a number of uses of a macro called register-groups-bind and just from looking at my own old uses of the macro, I was able to figure out how to use it again.

    It is easier to use than most of the cl-ppcre API!

       (register-groups-bind (x y z)
                             ("regex..." input [options...])
         ;; here, x, y, z are bound to groups from the regex
        )
    

    I needed to classify an input which could look like 123, 98% or 3/4. Integer, integer percentage or ratio. Easy:

      (register-groups-bind
        (num denom percent count)
         ("^(\\d+)/(\\d+)$|^(\\d+)%|^(\\d+)$" nq :sharedp t)
         ;; code here ...
      )
    

    In the code, if count is true, we got the integer. If num is true, so is denom and we have the fraction. If percent is true, we got the percentage.

    CL-PPCRE exposes the low-level objects: scanners you can create with create-scanner and use via the scan generic function. Using that would be a lot harder and verbose than the friendly macro.

    My old code was easier to understand and maintain for me because of the easy macro.

> JavaScript wanted to add the synthetic sugar for async/await, so the language had to be redesigned with that in mind. In a lisp, async/await could be implemented by a library instead, keeping the core still small and nimble.

Sure, but try hacking in infix notation or more complex notations like list[index] access, and you'll quickly see why hacking that stuff in is a bad idea. Lisp severely punishes adding syntactic sugar if it diverges at all from prefix s-expressions. Look at Clojure's member variable access for a real life example of how this plays out.

And if we're willing to make concessions that our syntactic sugar can only be as sweet as the overall design of the language allows, I think it makes sense to concede the same to Javascript and admit that Promises existed as libraries for years before async/await, and worked just fine in lots of production code.