Hacker News new | past | comments | ask | show | jobs | submit login
Eslisp – An S-expression syntax for ECMAScript/JavaScript, with Lisp-like macros (github.com/anko)
144 points by galfarragem on Oct 27, 2020 | hide | past | favorite | 67 comments



I gave years using parenscript [1] to create web-apps and as a sole developer on the project it totally won me over. Lisp macros + javascript. There is such joy in being able to mold the language to your needs and to completely eliminate any boiler plate duplicate code. Combined with SLIME + SBCL and you have a completely live javascript exprience. Even pushing code to the browser, no reload necessary.

[1] https://common-lisp.net/project/parenscript/


I found Clojurescript https://clojurescript.org/ to have a better ecosystem than parenscript.


You don't need an ecosystem with lisp. That's been both the reason why it's survived for so long and why it will never be popular.


You don't need an ecosystem with lisp.

Is the implication here that people build on top of ecosystems because they're incapable of implementing things themselves in plain JS, and that lisp will make them capable?

That's not the reality I see as a developer. The team I work with are entirely capable of building all the things we need, but we build on an existing ecosystem of open source code because that way we can do more with the available resources. Ecosystems are as much about efficiency as they are about ability.


The implication is that it is trivially easy to implement what is impossible in other languages in lisp.

So you never have to try and follow the moving target of an ever evolving ecosystem. You just pick a language version and build your own with surprising ease. The amount of time you save from breakages upstream is amazing and something no one talks about.


>Is the implication here that people build on top of ecosystems because they're incapable of implementing things themselves in plain JS, and that lisp will make them capable?

Not exactly, but Lisp (specifically Common Lisp but also other lisps) will make far easier to implement whatever is missing thanks to the various features that enable very quick development, quicker than JS or Python. And interfacing with C code is also easy too. Interfacing with Java libs, even easier (using Armed Bear Common Lisp.)


The problem with ecosystems today is that they lack aesthetic due to leaky abstractions. That Worse is better hasn’t been proven definitively and might never be!!!


There is an ecosystem though https://github.com/CodyReichert/awesome-cl


Every time I hear a glowing testimony like this, I get a little hopeful but then I remember the current state of the JavaScript ecosystem. Perhaps you could shed some light on this?

Is it possible to make externs or definitions of library functions? How much time does it take to take a fresh library and write those externs to access them in Parenscript land? And what limitations did you run into?

Cheers.


Parenscript is nice because you don’t need complicated externs: it has a pretty straightforward mapping to JS symbols and no runtime. However, the downside is that it feels more like “JavaScript with a new syntax” than like Common Lisp or another lisp. I personally find the trade-offs acceptable because it’s fees fairly natural to right JS in a lispy style, but it’s not the “pure” experience of something like Clojurescript.


I've heard good things about jscl[1]; since it's bootstrapped from regular cl, it's probably less javascript-y.

1. https://github.com/jscl-project/jscl


The tradeoff is that it actually has a runtime which means it adds to your bundle size and (potentially) makes interop complicated.


Fully agree. Parenscript is a great "compatibility layer" if you already know Common Lisp.


Ah, finally, the ideas from Lumen are percolating slowly into the mainstream: https://github.com/sctb/lumen

(It's a lisp for both JS and Lua.)

Eslisp is 2015, which is around the same time that Lumen was started: https://news.ycombinator.com/item?id=10264970

Though from the commit history, Lumen was actually started in 2012. Regardless, both projects seem to be roughly the same idea, which I find very interesting.

"block" is better named "do", and "lambda" is better as "fn", but those are minor details. Most of the power of the idea seems present.


I named it "block" because the structure it generates is literally called a Block in the ECMAScript spec https://www.ecma-international.org/ecma-262/5.1/#sec-12.1. I wanted to make the correspondence as familiar as possible to JS programmers.

You're probably right I should have called function expressions fn. You can alias it with (macro fn lambda) though if you prefer that.

First I hear of Lumen! What a coincidence that they started at the same time. It seems their language is an "overlay" entirely separate from both Lua and JS which only runs at compile-time, kind of like Lua is to Terra http://terralang.org/. I think that means Lumen can't load macros from JavaScript modules at compile-time like eslisp can, but I'm not sure. I'll do some reading. Thanks for the link.


It can. Loading macros is as simple as (when-compiling (load “foo.l”) nil)


The most important reason for "do" is avoiding excessive indentation, when starting with the first expression on the same line. And "fn" is good for something that you will want to avoid taking too much space, since you want to sprinkle them everywhere.


Greenspun's Tenth Rule of Programming: "Any sufficiently complicated C or Fortran program contains an ad-hoc, informally-specified bug-ridden slow implementation of half of Common Lisp." Now with more JavaScript.


I've tried really hard with eslisp to defy Greenspun's Tenth, and to do justice to the Lisp tradition. The macros in eslisp are actual macros with scope and proper hygiene. They can perform arbitrary computation, and can arbitrarily modify the AST. You can even access the macro table during compilation to completely gut the language from the inside if you want.

So I guess the goal is to be a purpose-designed, fairly well specified, only somewhat buggy, slower-but-not-debilitatingly-so implementation of 80% of Common Lisp. :)


>80% of Common Lisp.

So, does it include bit-arrays, custom hash tables, multimethods, method combinations, the condition-restarts signalling system, ability to place items on the stack instead of on the garbage collector, type declarations that enable speed optimizations, saving the running state to disk, and upgrading the instances of a class to a new class?

I don't want to be smug, but 80% is a bold claim. Again, I think Eslisp is useful and maybe i'll try to introduce it to my team (they're all JS/TS developers) to see if they get the GATEWAY DRUG to Lisp.


Actually that's probably a description of how JavaScript itself came into existence.


We could have had Scheme if things had gone differently...

http://speakingjs.com/es5/ch04.html


"By then, Netscape management had decided that a scripting language had to have a syntax similar to Java’s."

I wonder how their reasoning went. I find it generally confusing, if two different languages have similar syntax (why did Java have to look a bit like C?). I guess, I need a visual hint reminding me of the expected semantic.

Anyhow, even if the original JavaScript (perhaps "ScriptingScheme" or "WebScheme"?) would have more resembled Scheme, I doubt it would have used S-expr for long. Over the years I developed the needed parenthesis-blindness, but can't imagine most Web developers having that patience.


> slow implementation of half of Common Lisp." Now with more JavaScript.

Yes, but Eslisp doesn't even implement 10% of Common Lisp...

Not that eslisp isn't useful. I think it's useful!!


This is honestly really neat to see! Can it run in reverse, ie: take JS code and turn it into S-expressions? If it can then this would be an interesting way to write JS using an S-expression aware editor like emacs!


It unfortunately can't do it in that direction, at least yet.

It would be quite difficult in the general case to keep JavaScript formatted the same after it's been munged into S-expressions and converted back again. It would work the same, just look different. And the comments would disappear!

However, there has been some work into making a spec for a JS Concrete Syntax Tree (as opposed to Abstract Syntax Tree) which would retain whitespace, comments, and other such concrete "junk" that an AST typically abstracts out. https://github.com/estree/estree/issues/41; https://github.com/cst/cst It would be super cool to turn up at work in a JS shop armed with Emacs, and writing JavaScript at lightning speed using macros and structural editing, without anyone noticing.

It's a good idea. I'll make a note of it.


Since it's just a representation of Javascript AST in s-expressions, that certainly seems possible.


OH HI INTERNET. Author here. I just woke up. I will eat breakfast, then fix the package (issue #58 I see you) and answer questions.

Edit: Is fixed.


One suggestion - it isn't worth using the archaic "lambda" to mean a function. You could use "function" or some abbreviation of it like "fn" or "fun".


You can use "function" too.

There are 2 because JavaScript has 2 different structures for making functions; function expressions (which I chose "lambda" for) and function declarations (which I chose "function" for). See https://github.com/anko/eslisp/blob/master/doc/basics-refere... for examples.

Function declarations are statements, and you need to provide a name for the function.

Function expressions are expressions, and the name is optional.

In practice they often end up being interchangeable, but I just wanted to make sure any JavaScript is definitely representable.


At least in my personal experience, I strongly disagree that "lambda" is archaic... admittedly it might just be me (or maybe my specific background as a Haskeller), but I call anonymous functions "lambdas" all the time.


There is enough justice done to the term in Haskell. Won't blame you for it. But other languages aren't on the same footing.


Lambda is far from archaic. I call them lambdas all the time. Python explicitly uses lambda as the keyword. Java, C++ and Kotlin all use "lambda" to describe closures in the official docs. Rust uses both closure and lambda. All of these languages except python either are new (Rust, Kotlin) or have had lambdas added in the last ten years.


The fact that "lambda" came from "lambda calculus" is of such little relevance to mainstream programming today that we don't need to be reminded of it every time we want to write an "anonymous function". IMO, Clojure did the right thing there.

I'm not against the concept of lambdas (obviously, I love them since I made a scheme dialect with them for video editing). The word today is too specific for a plethora of subtly different representations/implementations of the "lambda" of "lambda calculus".

If "lambda" is supposed to mean "function", choose "function". No harm done.


Actually in Clojure I tend to distinguish:

Anonymous function:

(fn [a] ... a)

And lambda:

#(... %)

I don't think this is official nomenclature, but I've been using that to distinguish between both syntaxes.

In truth though, anonymous functions is the more accurate terminology, because lambdas are actually not functions, they're supposed to curry and use term substitution. Where as most programming languages use anonymous functions, which support multi-arity and sometime even variadic arity, and don't use term substitution, but more common argument binding.


Trivia: You can name fns:

  (map (fn first-or-last [x]
         (or (first x) (last x))
       my-seq)
(Of course the name has no semantic effects as you can't refer to it from code, but it's there in debugging / stack traces and serves as a comment for the reader).


Actually you can refer to the name inside the anonymous function for self-recursion I believe.


Rust should only use "closure," lambda isn't used as far as I know.

(I spent a bunch of time trying to sort this terminology when writing the book.)

That said, I don't really know if I'd agree that it's archaic, just less common. Python being an extremely popular language that uses it explicitly (as you say) makes it hard to really suggest that it's archaic.


https://doc.rust-lang.org/stable/rust-by-example/fn/closures...

First paragraph here uses both. I suspect that they use closure throughout the rest of the article, but the fact that they feel the need to reference the other name shows that lambda is still in common currency.


Gotcha, thanks.


My understanding here is that you can just configure this to your own liking. If you don't like lambda, just (macro fn lambda) and you're set. The names chosen by the original writer doesn't really matter when you can reconfigure the language.


I suppose I should've just said "it isn't worth using "lambda" to mean a function". Too many comments latched on to "archaic" instead of the intended point.


Technically, a function is a named lambda. There are subtle implications for both in PL design, so it's important to keep the naming distinct.


I'd be interested to hear why you were downvoted. Maybe lambda refers to arrow functions rather than convention 'function' functions?


If curious see also

Show HN from 2015: https://news.ycombinator.com/item?id=10264970


How does it handle symbols that aren't legal javascript identifiers? Is that on the user or are the some safety conventions in place in the compiler?


During compile-time, it doesn't care. So you can use "illegal" characters for the names of macros.

  (macro what? (function () (return '(a b c))))
  (what?)
That compiles just fine to the JavaScript code

  a(b, c);
It does try to validate identifier names during code generation though (using the esvalid module), so if you give it

  (? 1 2)
then it will error with

  [Error] Identifier `name` member must be a valid IdentifierName
  At line 1, offset 1:

  (? 1 2)
(In a terminal, the offending character is highlighted.)


For those interested in such things, there is also a Scheme-like Lisp that compiles to Python, called Hy (https://hylang.org).

It can be surprisingly fun and convenient to have the full macro power of Lisp layered on top of these dynamic languages.


or do the reverse, call Python from Lisp: https://github.com/bendudson/py4cl or call Lisp from Python: https://github.com/marcoheisig/cl4py

(or if it's because of Numpy, see NumCL https://numcl.github.io/numcl/)

in my taste Hy lacks a good lot of Lisp advantages… no closures with "let" by default, no image-based development and the excellent Lisp REPL, no Lisp's interactivity (in Hy or Python a process must restart after a code change, whereas in Lisp you compile one function with a keystroke and voilà, it's here to test), no Lisp's efficiency, no Lisp object system, etc. https://lisp-journey.gitlab.io/pythonvslisp/


Hy got my attention a while ago but I haven't played with it yet. Would you find yourself turning back to Python due to missing/annoying features? And is there an easy way to turn your code into a Python script so your co-workers can read it? :D


Quite neat indeed, will give it a try. Curious to see how it compares to https://github.com/jcubic/lips and ClojureScript.


Some words on that subject from the README and a link.

"I wanted JavaScript to be homoiconic and have modular macros written in the same language. I feel like this is the adjacent possible in that direction. Sweet.js exists for macros, but theyre awkward to write and aren't JavaScript. Various JavaScript lisps exist, but most have featuritis from trying too hard to be Lisp (rather than just being a JS syntax), and none have macros that are just JS functions"

https://github.com/anko/eslisp/blob/master/doc/comparison-to...


This inspired me to use same AST library to generate JavaScript from Scheme in my Scheme based lips called LIPS. I was planing on writing compiler (first reading more about compilers) maybe it would be simpler than I thought.

But still I think that `((. console log) "hello")` is not as nice as `(console.log "hello")` as in LIPS. My old code was using the same syntax but I've changed the interpreter and you now can use dot notation on any JavaScript objects.


[ To save readers a search, LIPS is at https://github.com/jcubic/lips ]

I'm happy that eslisp has inspired other language ideas. LIPS is cool. The auto-resolving Promise feature is a great idea that's only really possible when you implement the language as an interpreter like you have. Generated code for that would be a mess...

I agree about the (. console log) thing. Such a common operation should be easy to type. I only did it this way to avoid adding any more syntax to the language, because I thought I could solve that problem later by making the syntax configurable by users.

...which I haven't really done well yet. Eslisp has a system called _transform macros_, one of which is https://github.com/anko/eslisp-propertify which turns `console.log` atoms into `(. console log)`, making the syntax legal. The transform macro system is really awkward to use and limited in functionality. I'm working in private on replacing the whole parser with one that user macros could modify, based on https://github.com/anko/partser. Parser combinators are the future!


This reminds me a lot of wisp (https://github.com/Gozala/wisp), which I particularly liked. I wish these things had more traction, given JavaScript’s ubiquity but tendency for rather convoluted embellishments.


Wisp is neat, and was an inspiration for eslisp. Wisp introduces a lot of syntax, and abstractions to pretend it's on a lisp runtime, which make it harder to learn, and makes it less obvious what happened when stuff breaks. And you can't invoke the compiler inside macros. Those are the things I've tried to improve in eslisp.

Finding traction for a new language is difficult. It requires quite a large mass of work, documentation, and polish, before people view it as stable enough to invest their time in.

For now, I'm happy even if eslisp is viewed as a convoluted experiment. If it dies, perhaps like I found Wisp and had ideas, someone will find this and have ideas. Projects die, progress continues.


Just as suggestion, extend it with the following

https://readable.sourceforge.io/


Anyone care to compare this to Clojurescript?


ClojureScript is much more mature


See the author's comment from a previous discussion:

"ClojureScript tries to be Clojure, and Parenscript tries to be Common Lisp. Eslisp tries to be JavaScript; it's intended to just be an obvious one-to-one syntax replacement.

I wrote a brief comparison in the docs: https://github.com/anko/eslisp/blob/master/doc/comparison-to... "

https://news.ycombinator.com/item?id=10266254

I am definitely appreciating eslisp's almost one-to-one mapping to JavaScript. Finally I can program JavaScript with macros! I've searched for something like this before but didn't find it unfortunately.


Could this or something like it be used to port org-mode to JavaScript environments like VS Code?


Hygienic macros. Good.

I used sweet.js in the past to implement Go-style channels in JavaScript so that I could handle complex asynchronous semantics (including buffering) more elegantly.

https://www.sweetjs.org


Neat but I want this with type checking


Someone just needs to write tslisp that converts the TypeScript AST into s-expressions...


>Someone just needs to write tslisp that converts the TypeScript AST into s-expressions...

Well, this is actually a very good idea!


I know I’m asking for a square that’s also a circle


A squircle[1] isn't a bad analogy for this, particularly if it's a super ellipse, parametrically you can adjust between circle and square at either limit. The same can be convenient with types too

[1] https://en.wikipedia.org/wiki/Squircle




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

Search: