Hacker News new | past | comments | ask | show | jobs | submit login
LispyScript: A JavaScript With Lispy Syntax And Macros (lispyscript.com)
79 points by friggeri on Sept 21, 2012 | hide | past | favorite | 52 comments

Some time back, I tried to take JSON as the AST and see if I can build a Scheme-like programming language with it. I did it just for kicks, but if it interests anyone .. [1].

It's lacking in docs, but this might be of interest - you get a Scheme-like language (without tail recursion), keyword arguments, lambda, macro, quote & quasi-quote, let expression, where clauses, generators, and some degree of code isolation. The compiler is a very simple one and is written in "stream of thought" style [2] and so might be quite easy to follow.

[1] http://github.com/srikumarks/jexpr [2] http://srikumarks.github.com/jexpr/

Add "SweetExpressions" and I wonder how far from coffeescript it would get.

"SweetExpressions" is probably the best method so far to get rid of excessive parentheses in lisp. See http://readable.sourceforge.net/

As someone who has done a fair bit of lisp hacking I'll never really understand the paren-hate. Yes it's weird the first few times but I have never met anyone who has coded a significant amount of lisp who sees them as anything but an asset.

In lisp you're essentially hacking at the AST level, this is precisely where a huge chunk of lisp's power comes from, and parens are an incredibly natural and concise way to express this structure.

I have also written a lot of Python and I can honestly say significant white space as been an issue for me far more often than parens in Lisp (and I don't find significant white space to be an issue at all).

Sadly, the vast majority of coders never make it to "a significant amount of Lisp". Most who do tend to be very positive about Lisp, so it's not that the language veterans give it a bad word-of-mouth reputation. Quite the opposite.

There is clearly an "approachability problem" to Lisp, and it seems worth trying to solve. (If nothing else, the more popular a language gets, the more opportunities you will have to get paid to write in it for a living.)

Parens come up every single time the word Lisp is mentioned in "mixed company"--that is, both Lispers and non-Lispers. It's not just the most common complaint about the language, but the dominant complaint. (Thought experiment: What's the #2 complaint newbies make about Lisp? And how much longer did it take you to answer that question than #1?)

Originally John McCarthy had intended to use M-Expressions for Lisp, e.g. "car[cons[(A . B); x]]", but programmers preferred the parens-and-whitespace of S-Expressions instead, so they became the default.

There's no reason to dismiss the possibility of another such evolution, again based on nothing more than programmer preference.

No matter how familiar or useful you might find the existing approach, it's always possible that it can be improved.

There have been many attempts to solve the approachability problem by creating alternate syntaxes. Vaughan Pratt's CGOL, written almost 40 years ago, was perhaps one of the best.

Although these alternate syntaxes may be helpful to newcomers, they have never caught on. Expert users actually prefer the parenthesized form; so anyone working in Lisp for very long has to learn to deal with it -- and usually, once they do, they find it preferable.

You said it yourself: programmers preferred S-expressions, so they became the default.

The other important point here is that the non-Lispers are right, in a sense: editing Lisp without a paren-counting editor is just as tedious as one would expect. But with a paren-counting editor -- and even better, with something like Paredit that keeps the parens balanced at all times -- editing actually becomes easier, more fluid, and more pleasant than in other languages.

That is why expert Lisp users prefer the parens.

editing Lisp without a paren-counting editor is just as tedious as one would expect

That's the real barrier, IMO. Who wants to switch editors just to learn a new language with an (apparently) impoverished syntax?

Hmm. How many editors people actually use these days don't have a usable Lisp mode? I don't think your choices are limited to Emacs anymore. I'm fairly sure Vim has one; I don't know for sure, but would be very surprised if Eclipse didn't; not sure what else people are using.

If it really is the case that all Lisp needs to really take off is editor plugins for IntelliJ IDEA and Visual Studio -- hmm -- maybe we should do that.

I'm sympathetic, but I think the approachability problem says more about programming culture than it does about parens. People optimize for the ease-of-use case (read: syntax) over factors which have much more fundamental implications throughout the lifetime of a piece of software.

I mean, learning syntax is really not where we spend most of our time while writing software, right? All else being equal, the amount of time it takes to learn new syntax is orders of magnitudes less than the time you spend, in aggregate, finding and fixing bugs. So it's a misguided trade-off, and (fortunately or unfortunately) I don't think swapping square brackets for parens (or whatever) is going to change people's preferences for what looks and feels easiest for them.

> Thought experiment: What's the #2 complaint newbies make about Lisp? And how much longer did it take you to answer that question than #1?

I'm not sure what you're getting at, but the perennial #2 complaint is "There aren't any libraries!", and my response is typically the same (exasperated) answer as for #1: "You've written maybe 10 lines of Lisp in your life -- I've written several orders of magnitude more than that, and I can say that it's simply not an issue, and I wish the libraries we had in $(MAINSTREAM_LANGUAGE) were as high quality".

(I admit I am not a very good ambassador for Lisp.)

#3 is always "Who's going to maintain it?", at which point I remind them that more than half the team they've hired didn't know $(MAINSTREAM_LANGUAGE) when they started here, either, so apparently learning new languages isn't a showstopper.

There is no #4, because at this point they usually stop talking to me, and get that "He's one of those weird Lisp guys" look on their face, and politely make it clear that regardless of how many objections I can answer, Lisp will never be used for anything except Emacs.

I've coded a significant amount of lisp, and I agree the parentheses are completely an asset.

I appreciate the need to have parens to remove ambiguity and for something like an AST it is a very concise expression. It just seems like there are common patterns that could be abstracted out, such as the code at the bottom of this lispyscript example[0]. Every new line gets a open paren, LF + tab indicates another open paren on next line, otherwise LF is a closed paren. Again, I'm not a lisp guy (could you tell?) and maybe this is just because it's a js variant and not pure lisp code.

I appreciate when languages don't make me insert/edit/move over/count/deal with/etc. more characters than necessary. Maybe there's a good vim setup to do that already - that would go a long way to alleviate my concerns.

[0]: https://github.com/santoshrajan/lispyscript/blob/master/exam...

Abstract out how? For instance, this is valid code:

    (if (empty? x)
edit: lolz. failed at example. fixed.

foo is the result of if x is empty, and you can tell it is not a function call because it has no parens. Whitespace is merely a style consideration, as it is in C++ or Java.

There are ways to save time/trouble within the language. For instance, in Clojure

    (foo (bar (baz (quux x))))
can be rewritten as

    ((comp foo bar baz quux) x)
There's also stuff like apply, ->, and ->> in Clojure, which have similar purposes (flattening out the nesting a bit). And for anything particularly syntactically onerous, you can always add sugar with macros.

Ultimately syntax is more about ease than it is about power or simplicity. I think it's worthwhile to try to look beyond that into what the code is saying rather than how it's saying it. You have to do this anytime you learn a significantly different language anyway, right?

True, and to me the ease of use increases by getting rid of what I perceive to be superfluous parens. I'd rather write the first code block and have mean the same as the second. This is a trivial example, but when LF + indentation become significant it eases deep nesting.


    if (empty? x)
Less ideal:

    (if (empty? x)
The clojure comp shortcut looks good to me, but why even need the outside parens if it was the only line in that block?

    (comp foo bar baz quux) x
Ideally, that should be valid as well. I know this kind of reduction won't work in a lot of cases, but I appreciate syntax sugaring like this where I can get it.

Sugar is nice, but it ought to make the code clearer. I worry that in trying to make this appear simpler, in reality it would be substantially less so. I think significant whitespace works for a language like Python, but I'm less convinced that it's a great idea for a Lisp.

As it stands now, the trade-off is some unfamiliarity and discomfort up front for rather elegant consistency for the entire time you use the language. When you're optimizing for the people who'll use the language, the former is a lot less significant. :)

One thing worth noting is that Clojure does introduce constructs which use square brackets, braces, etc. It's a little nicer to use brackets for grouping, in lieu of parens for lists or declaring function params.

I agree with your priorities on how long term concerns trump up-front discomfort when learning a language. But to me it would be of long term benefit to never see parens that could be otherwise indicated by sig whitespace patterns like the empty/foo/bar indented block we have above.

The elegance seems to come from having a simpler parser, and one less thing to explain to people when they're learning the lanuage. As a user of a language and not a developer of a parser, the first doesn't concern me and the second should be trumped by long-term concerns. Using significant whitespace to eliminate syntax is elegant in its own way as well.

Again, maybe this is just because I'm not a lisper, but keeping implicit parens that follow a well-established pattern should be a trivial mental task for a developer. Could you provide a piece of lisp code as a contra example?

I guess what I'm trying to say is that syntax is probably the least significant difference between a Lisp like Clojure and your average imperative C-like. So you can make incremental improvements to the syntax, but since it's not going to significantly alter how and what kinds of programs you write the way immutability and purity do, I don't see it as all that worthwhile.

People don't like what's unfamiliar and a lot of parens are unfamiliar. That's mostly OK with me; people need to evaluate what they think is worth their time. But typically this evaluation manifests as a gripe about syntax. Syntax does matter but not nearly as much as we like to talk about it!

Myself, I only picked up Clojure a couple of weeks ago, give or take. I don't use it at work so this is in my spare time (which seems ever more shrinking).

Parens aren't a big deal, and it's kind of crazy we spend this much time talking about them instead of something more significant. (Hell, maybe people just enjoy trolling.) Omit all indentation in Java or C++ and you'll be doing the bracket-counting that you assume Lispers spend their time doing.

well, read some ML or Haskell code to see how much more pleasant it looks than the equivalent lisp code. i don't hate parentheses per se; i just think you miss out on a lot when they're your only syntactic construct. (of course, as you point out, you gain a lot too, but that is orthogonal to the basic experience of reading and writing code)

Considering the "readable" example:

  (define (factorial n)
    (if (<= n 1)
      (* n (factorial (- n 1)))))

  define factorial(n)
    if {n <= 1}
      {n * factorial{n - 1}}
The first one looks more pleasant to me. The second one looks broken and disorganized. My brain can't parse it as easily without the visual cues provided by the parens.

Of course, it looks better to me because I'm used to lisp. When I first saw lisp I thought the syntax was nuts. Point being - what looks "good" can simply be a matter of what you're used to.


    factorial n = case
    | n <= 1 -> 1
    | else   -> n * factorial (n - 1)
i can see what's going on at a glance - the syntax and layout of the code are a positive help to understanding it.

You changed the structure, though, to use something much closer to a cond instead of an if. Also, there are variants that require fewer ()s for this.


    (defun factorial (n) (cond
        ((<= n 1) 1)
        (T        (* n (factorial (- n 1))))))
Clojure (which I believe is similar to Ark with respect to not using extra parentheses around cond clauses, but I don't have Ark setup to verify that that's actually the case and then to test my demonstration with, so I'm going to do it with Clojure, despite the "weird" [] syntax, as it is orthogonal to the demonstration):

    (defn factorial [n] (cond
        (<= n 1) 1
        :else    (* n (factorial (dec n)))))

fair point (i was actually planning on adding a lisp cond by way of contrast, and then forgot). my contention is that the ml cond looks a lot less cluttered than the lisp one, and is therefore more pleasant to read.

the clojure one is nicer since it lacks the visual clutter of the per-case enclosing parens, but it feels slightly wrong from a lisp perspective since i'm now replacing [(a, b), (c, d)] with [a, b, c, d] which has changed the innate structure of the expression.

i actually like the racket convention of using square brackets to distinguish this the best of all the lispy options:

    (defn factorial [n]
        [(<= n 1) 1]
        [:else    (* n (factorial (dec n)))]))

For the record, that isn't valid racket code. This is:

  (define (factorial n)
     [(<= n 1)  1]
     [else      (* n (factorial (sub1 n)))]))

If you're looking at parentheses instead of using indentation effectively and looking at the first thing in the list you haven't learned how to read Lisp code yet. People look at Lisp and think that because of the parentheses they don't have to bother thinking about the typographic layout of the code like they do in C, so their code ends up with shitty formatting and they think it's because of the parentheses, when in reality it is because they didn't bother formatting it.

Getting rid of parentheses means you cannot use structured editing tools like Paredit anymore (http://emacswiki.org/emacs/ParEdit). You can't appreciate how slow and clumsy it is to edit code in other languages until you've been using Paredit for a while.

One sometimes valid argument against prefix notation is for expressions primarily involving binary math operations (+ * - / etc.). In some cases it's true, in other cases the Lisp code looks gnarly because people try to write the formula out without introducing intermediate variables. There's macros out there that will parse infix arithmetic (http://cliki.net/Infix); I've never used them so I can't comment on whether they improve code readability or not.

I don't see how the lack of (IMO) optional parens would prevent tools from working - they'd just have to be adjusted. Assuming this example from the ParEdit page is well-formed lisp:

    (defun paredit-barf-all-the-way-forward ()
        (if (eolp) (delete-horizontal-space)))
It doesn't seem that hard to rework a tool to use sig whitespace as indicating parens to have it operate the same over this code:

    defun paredit-barf-all-the-way-forward ()
        if (eolp) (delete-horizontal-space)

I experimented with a CoffeeScript-like Lisp in JavaScript once. It's pretty opinionated, but provides interesting syntax possibilities (infix notation, non-significant parentheses and commas for clarity, JSON-compatible syntax): https://github.com/tcr/syrup

    range = fn: [i]
      loop: [i, r = []]
        if: (i == 0) r
          continue: (i - 1), concat: (i - 1), r
    print: range: 5                # [0, 1, 2, 3, 4]

I was just coming here to wonder at the need for those chubby little lines with their quadruple chins. Seems like if you have blocks of regular indention you should be able to trim some of the fat.

I'm not a lisp guy though, so I'm sure that sentiment is naive.

have you tried clojurescript

I did - I don't like Clojure (because I much, much prefer Scheme to CL) but it seemed very interesting. However, it felt very heavy, almost like GWT. There is something very similar for Racket[1], but even though it's Scheme it still feels much to heavyweight and I couldn't bring myself to use it.

[1] http://hashcollision.org/whalesong/

I was very disappointed by whalesong. Instead of a source->source translator like parenscript, whalesong generates a bytecode level VM inside the browser. Doing so provides support for any racket language, but it does so at the cost of immense quantities of "pre-obfuscated" JavaScript.

The genesis of whalesong was to support "World"[1] games on the web, and to that end it seems successful. I don't think it makes a good general purpose tool. Perhaps someone wants to port parenscript to scheme?

[1 | http://world.cs.brown.edu/]

How slow is whalesong though? I know people like to say that js is slow, but it's really only the DOM manipulation that makes it so.

Well, it's a 700k interpreter and runtime. It isn't slow for simple things, but only because JS is really fast now. Interpreters are usually 5 to 10 times slower than whatever they are running in.

http://hashcollision.org/whalesong/examples/raphael-demo/rap... isn't bad

http://hashcollision.org/whalesong/examples/boid/boid.html is a little slow compared to http://graphics.cs.wisc.edu/Courses/Games12/Tutorial1/Phase1...

Some of the low framerate could be frame timing (whalesong could be using setTimeout poorly), but they seem to use about the same CPU.

Huh, didn't know that. For some reason I thought it compiled to JS.

I don't know whalesong.

But if you include a giant runtime, the compilation and evaluation of it on every page load, and downloading it (there's evidence to show many hit CDNs with cold caches, probably screwed up firewalls or proxies that mess with caching) can make for a suboptimal experience that can't be optimized without rewriting in something that doesn't require a bytecode VM embedded in your JS.

I would say Clojure is a lot closer to Scheme than CL. It is a Lisp-1 after all, and with a focus on immutable data structures.

Yeah, I'm pretty sure he meant it the other way (Scheme to CL) because I've heard a lot of Scheme people like Clojurescript, not CL people.

This is just a light lisp-like syntactic layer, as the docs say "LispyScript is not a dialect of Lisp. There is no list processing in LispyScript ." It's just enough to allow nice macros, which adds quite a bit, IMO. There's no CONS, etc. It adds a couple nice things, like an "each" function that does the right thing, and (= foo bar) becomes foo === bar.

Cool, but could do with shortening the 'function' keyword. It's just too long for the amount it's used.

Agreed. Luckily you have macros, so go ahead and do it:


Right, I think "you have macros" is a definite answer to any and all syntax-related issues :)

I experience a kind of jolt when I read lispy syntax mixed with constructs like [1, 2, 3]. It shouldn't be a big step to make a syntax for #(1 2 3) or (make-array 1 2 3) instead. Get rid of that infix comma syntax! Then there is the record syntax... {foo: 3, bar: 4}... Infix commas and colons! How about (make-object foo 3 bar 4) or #O(foo 3 bar 4)? Or support reader macros so that you can override []s and {}s to do something a little prettier.

Why not [1 2 3] and {foo 3 bar 4}? Clojure gets it right. :)

Two of the best parts of clojure (over other lisps).

For Common Lisp to support converting [1 2 3] to a vector:

  ; [] to #()
  (defun bracket-reader (stream char)
    (declare (ignore char))
      `(vector ,@(read-delimited-list #\] stream t)))
  (set-macro-character #\[ #'bracket-reader)
  (set-syntax-from-char #\] #\))
And for your in-place hash:

  ; {} to hash
  (defun set-hash-values (hash pairs)
    (when pairs
      (setf (gethash (car pairs) hash) (cadr pairs))
      (set-hash-values hash (cddr pairs))))
  (defun in-place-hash (stream char)
    (declare (ignore char))
      `(let ((hash (make-hash-table)))
         (set-hash-values hash ',(read-delimited-list #\} stream t))
  (set-macro-character #\{ #'in-place-hash)
  (set-syntax-from-char #\} #\))
I just sketched these together. I am a CL noob. No doubt there are more concise approaches.

According to the docs, (array 1 2 3) and (object "foo" 3 "bar" 4) works, too.

That's great, but unless someone proves me wrong, it lacks the "list"-style flexibility to do things like "create an object from my list": (object (array "foo" 3 "bar" 4)). Javascript's Array apply method could probably be used to do similar stuff, but that doesn't feel very lispy. FWIW Coffeescript lacks this kind of "object comprehension" as well (which Livescript has BTW).

What I get for not penetrating further than the examples. Yay!

Why the dependency on node?

The obvious reason would be that to make it work nicely as a compiler, they wanted to make it pull in files and compile them. It shouldn't be terribly difficult, though, to patch it and make it compile in-browser like CoffeeScript does with special script tags.

According to the docs it's pretty close to that already: http://lispyscript.com/docs/#browser

You'd just need to make it find the special script tags and eval them.

EDIT: Yeah actually it's already in there: https://github.com/santoshrajan/lispyscript/blob/master/src/...

Actually, it turns out it's already implemented: https://github.com/santoshrajan/lispyscript/blob/da6602126eb...

So any script with type "application/lispyscript" will automatically be parsed and run. The only requirements appear to be RequireJS and underscore.

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