Hacker News new | past | comments | ask | show | jobs | submit login
Stop Writing JavaScript Compilers, Make Macros Instead (jlongster.com)
242 points by jlongster on Jan 8, 2014 | hide | past | favorite | 133 comments



I'd be afraid of inheriting any codebase that made extensive use of these JS macros.

You really have no idea what this is doing (the canonical example from sweetjs.org):

  class Person {
    constructor(name) {
      this.name = name;
    }

    say(msg) {
      console.log(this.name + " says: " + msg);
    }
  }
It's not javascript -- it's your own made up "language" of macros.

I do believe with a steady hand this could lead to some great things, but the example on sweetjs.org seems rather heavy handed. Having different `class` macro definitions between codebases that use sweet.js will devolve into madness.


You're more or less making up a "language" every time you write an API. Among other things, the extent to which you notice this depends on the degree to which one API interoperates well with another one, and the degree to which an API is specific. (See also DSLs in such as Ruby.)

I'm not saying you're wrong, exactly, because there are qualitative differences. Manipulating syntax is definitely more powerful, and enables some truly crazy and wonderful constructs. It should be used responsibly.

But when we define an API, we're inventing a new system of primitives for the problem at hand. It's really not as conceptually dissimilar from a language as we'd like to think.


The difference, though, is that the sweet.js example of classes aims to implement something that is traditionally a language feature. APIs tend not to do this.

You might argue that the distinction between APIs and language features is arbitrary. But I'd say there is a meaningful difference. An API helps you tackle a particular problem domain, such as making an HTTP request, communicating over a USB port, or writing an image. Whereas a language feature is much more generic. It's not limited to any particular problem domain. Rather, it cuts to core of how we express problems in general.

If we're all ostensibly writing JavaScript, I don't think we should be using multiple, incompatible variants of the same language features.


As Dave Thomas (of OTI not pragprog) said once. "Its important to be able to debug at the level of the abstraction". So in theory making up a language is the same as writing an API. In practice when you are in a hurry trying to fix something and your livelihood depends on it you may end up cursing the macro writer. I do think they can work when there's only one person on the team, which sometimes happens, or if the team are mature and disciplined, which rarely happens. They can also work in any team for trivial stuff like logging etc.


You're absolutely right, and we need to establish a good culture that doesn't abuse macros. Of course, some people always will and let's hope we don't have to work with that code.

There are a lot of ripe ES6 features to implement as macros. That `class` macro is actually taken from ES6 and has been standardized for JavaScript, so it's not exactly random.


> we need to establish a good culture that doesn't abuse macros

I'm sure everyone who has ever looked at C macros has thought the same thing. It's a great theory.

In practice, having macros will lead to someone abusing them and eventually, that abuse will be become institutionalized.

Don't say "it could never happen here" because it can and it inevitably will.

I don't have a specific problem with all of this, except I keep thinking a better solution would be to hammer browser manufacturers to keep their shit up to date or explain why they are not doing so.


I think the Clojure community manages to do pretty well in discouraging macros except as thin sugar over functionality avaible through normal channels. It also has some very complicated macros that give it a type system and lightweight threading and channels that would have have had to be implemented by the language designers in other languages.

I'm willing to run into the occasional macro abuse to get goodies like core.async and core.typed.


> I don't have a specific problem with all of this, except I keep thinking a better solution would be to hammer browser manufacturers to keep their shit up to date or explain why they are not doing so.

That's the thing though. When a new proposal comes out, if it's released in the form of a macro it can be tested without vendor buy in. The pressure relief on vendors to keep up to date with the standard is just an unintended (maybe negative * ) side effect.

* Personally I like being able to count on "current" features and I'm not opposed to using shims to patch the environment up to a workable level.


Do you even Lisp, bro?

(Forgive the irresistible pun, but the sentiment is sincere: This feature is based on Scheme, so looking at C seems misguided.)


But alas, the browser wars are upon us once again. FF vs Chrome is approaching the silly.

On the other hand, there are merits in their differences. Chrome, although it would prefer Dart, looks upon JS as legacy that they can approach via a good VM/JIT, and a brilliant Developer Tools suite.

FF on the other hand is leading into the JS futures on two fronts: asm.js for both performance and C/C++/.. integration into the browser, and ESnext. Google is very reluctant to invest in ESnext now, just look at the compatibility charts.

This makes sweet.js strategic, it lets us at least experiment with ESnext features as far as macros can take us. I'm hoping sweet.js can also help us make use of asm.js from within the browser buy building macros for things like structs, which halve or more memory footprint of object arrays.

Lets be aware of the war, and do what we can do to resolve it.


The C preprocessor is terrible, but rampant cpp insanity is much, much less prevalent these days -- the C culture has made its peace with the technology. A similar accommodation would doubtless happen with JS macros, particularly as there's almost no chance that the implementation would be as brainless as C's.


The syntax "crowding" could be solved by scoping macro rules. Maybe `macro class, foo, bar` at the top of the file?


The next big thing sweet.js is working on is modules. You will be able to actually pull in macros just like you would ES6 libs:

import { foo, bar } from "macros";

The scoping of macros will all stay in tact; any other macros from "macros.js" will be available, but the code that `foo` or `bar` expands to will be able to access them (this is really nice for helper macros and other things).

So yes, modules are going to be a big part of distributing and composing macros.


Racket shows how, with strong module support, a whole "tower of languages a.k.a. macros" can work reliably. Else not so much.


Also, macros in racket are lexically scoped. Any modern macro system should follow their design, IMHO.


Agreed, and happily sweet.js is very consciously modeled after Racket.


That's the best thing I read this morning and a reason why I'm going to give sweet.js a closer look :)


I just noticed a typo: I said "will be available" when I meant "will not be available". Any macros you don't import will not be in scope.


Being able to define a language tailored to your problems is a wonderful thing. Don't be afraid of it.

Scheme and other Lisps macros to build new syntax on top of a small core language.

Are you tired of typing (function() { ... })() and other such forms? If so, you will like macros.


Right. Look at the many wonderful and widely used applications or websites implemented in those heavy DSL systems!

Here is the full list:


Let's look at DSLs that which they were DSLs. To be kind we'll limit ourselves to javascript.

On the building side we have Grunt and the less popular but still useful Jake.

Ember's property system is a DSL.

Angular's DI syntax is a DSL and as much as I love Angular there's really nothing to like about the horrible mix of ([{ that one has to use here.

Actually Ember and Angular and just about every meaningful javascript framework defines several small DSLs.

Pencil Code is an excellent learn-to-program DSL built on Iced Coffeescript.

Coffeescript's crazy syntax makes DSLs slightly more approachable than Javascript but only for a while.

Express and most server frameworks define a DSL.

Anytime you find yourself passing a non trivial config struct you're really playing with a DSL.

This is by no means a complete list.


In addition to HN (as noted by my sibling) Facebook has extended both PHP and JavaScript with their own DSL's and syntax. I'm sure there are many more, but a lazy comment deserves a lazy answer ;)


It is different.

Using DSL to describe your domain (application) is normal. One application = one domain.

Learning in addition 15 different DSLs for libraries and tools, just because it's cute, is not the way to go. Soon you will argue about the exact definition of monad instead of shipping code.


This is a funny comment to read on Hacker News.


He did say "wonderful" ;-)


For a good example, look no further than SRFI-41[1] which defines a language for "streams", lazy lists that may have no end. SICP covers this topic as well.

Macros are used to great effect in Lisp land. They can be used to great effect in JavaScript land, too. With great power comes great responsibility.

Does anyone know Arc well enough to point out some macros used in HN's source?

[1] http://srfi.schemers.org/srfi-41/srfi-41.html


unless I can import a macro like clojure then no thanks. javascript debugging is already a major nightmare since any object can be edited anywhere, i dont need macros multiplying that disaster.


Actually you could easily use macros to reduce this nightmare. And yes, I wish for sane module system in JS too - my ideal is Racket in this regard, especially after recent addition of nested submodules.


>javascript debugging is already a major nightmare since any object can be edited anywhere

No it really cannot, except if it's exported there.


> Having different `class` macro definitions between codebases that use sweet.js will devolve into madness.

Urgh, I just got a sinking feeling in my stomach because I know how right you are.


> Having different `class` macro definitions between codebases that use sweet.js will devolve into madness.

As long as you don't have different definitions in the same JS file, it's not really a problem because each file can be compiled down to JS independently of the others. I'd imagine you could even add a package.json flag to specify which version of a sweet.js "syntactic sugar" library to use for a given package, and npm could compile the files automatically on install.

The more transparent the sweet.js workflow can be (much like LESS/SASS is for CSS), the more likely it is to be adopted.


It could be made to work technically, certainly.

But it'd be a complete mess of a code base. Code is read much more often than written, easily understanding a code base is incredibly important, and really hard in larger systems.

If even your syntactical constructs behave subtly different (let alone the code you're writing!), you're setting yourself up for unpleasant surprises.

There's an excellent book on UX, "Don't make me think". The same applies to code: strive to reduce the cognitive load it takes to understand a piece of code as much as possible.


strive to reduce the cognitive load it takes to understand a piece of code as much as possible.

Agreed. And it seems macros can really help with that, by encouraging a DRY codebase.


As others have noted, macros in theory could really help with that. But with macros in practice, I wouldn't be so sure. Macros essentially add syntax to languages.

Syntax to help with common tasks is nice, but if you end up with lots of ad hoc syntax for lots of very divergent, and maybe not so common tasks, you're in trouble. The problem is just the multitude of syntax constructs that typically end up way underspecified, as opposed to the much more formal and strict models of a language that went through a specification process. Imagine learning not three kinds of loops (for, while, do), but 300 subtly different ones in one code base. Ugh.

On the other hand, lots of boilerplate make code unreadable just by the sheer amount of it. This is a very hard balance to strike, I'd rather err on not giving every developer on a code base the power to add new syntactical abstractions.


Even if it's consistent within files, you can still run into big problems. If the different macros supply slightly different functionality, you could end up in a situation where you have two different types of classes that can't be used interchangeably. It's not a problem if you write all the code, but once you start pulling in other code all bets are off.


It's still a problem for the poor person who has to read those different files and understand the underlying semantics.


Do we get madness because different libraries have different implementations of AJAX?

In most lisp dialects, macros are lexically scoped.

As long as sweetJS follows lexical scoping, a set of macros could be contained within a self-executing lambda like most libraries already are.


I have only one thing to say here, to you and to all people who are opposed to macros and new languages in general: go learn some more (let's say 5) languages and then come back[1]. Don't bother before that.

Why? Because learning languages gets easier every time you do it. And it opens your eyes to languages' internal structure, the way languages are composed, which makes using given language effectively easier.

So now you know 5 or 8 languages and you discovered a few basic principles behind most of them and you understand how semantic constructs compose and you know about various syntactic representation of any given semantic statement (and the other way around, similar syntactic constructs having different meanings). And you come upon a macro, like the one in the example.

You don't know what it's doing - well, you just check the docs and then you know. It's as easy as learning what a function does, really. So what was learning those other languages for? It was just to reduce your fear of introducing new syntax for things. You won't think "oh, how dangerous this could be" but rather you just start using it, because you do it (change syntactical constructs you use) all the time when you switch between languages.

At this stage you can learn some Lisp. And write some real macros. And maybe try Nimrod, which has wonderful macros too. And maybe Rust, which I didn't try but I read good things about. After this you'll understand what macros are, how they are constructed and how they compose - and write many of your own - and then you look at the class example above and you instinctively[2] know that there has to be a syntax transformer which matches class keyword, a keyword and a block, and then there has to be another transformer, perhaps recursive (like in Scheme) or just iterating over the body (like in defmacro) and that all they do is to add some keywords here and there and a statement or two at the top of the block. In short - you know exactly what to look for and even docs are unneeded, you just take a quick look at a macro source and go on using it happily. Alternatively you can look at generated code and work out macro implementation from it easily, too.

I'm a happy user of LiveScript, and CoffeeScript before that. The only reason for not switching to JS with sweet.js is the fact that I would need to write a whole host of macros by myself, while in CS and especially LS they are already written. And of course modifying their grammar is not that hard either. Had I somehow been unable to use LiveScript, I'd use sweet.js for sure. What I'd implement? Probably some kind of "let" statement for sane scope management, a loop macro for iterating over custom collection classes (loop in CL, for/* in Racket, also in others), function currying (partial application) and maybe something like "with" context managers from Python would be among the first. There are many things which are better abstracted with syntax than with (for example) functions.

Would the language be JavaScript? Well, hard question, it would be a superset with underlying semantics intact, but it sure wouldn't "feel" like JS. But would that matter? Not at all - syntactic extensions would save me a ton of time and I'm used to many different syntaxes anyway. And I expect anyone who'd like to work with me to be able to pick up any language in reasonable time - the ability to pick up a few additional syntactic constructs on top of already known language is essentially the same thing, so I think such person would have no problems with it either.

Anyway, macros are good; using them is good; the only dangerous thing is having many people implement essentially the same macro over and over again, but I think this could be solved with a sane module system for macros which I read is already planned. There should be more languages rather than less; more exploration of different syntaxes and semantics for things, not less - and macros are one way of making this happen. And if you have problems with learning what a "class name body" does, then I can only refer you to the first paragraph.

[1] After I wrote this post I realized I could sound a bit rude. It was not my intention and I'm sorry if I offended anyone; while written generally, it's actually only a description of a road I personally traveled, so obviously YMMV.

[2] I know completely nothing about sweet.js in particular, so I'm completely just guessing.


I have written non-trivial programs in C, C++, Java, Javascript, ActionScript, PHP, Python, Haskell, Scheme, Go, and Sawzall, and also played around with prototypes or toy-quality programs in Common Lisp, Ocaml, Dylan, Erlang, Ruby, and Perl. I've written compilers, templating languages, desktop GUIs, web GUIs, mobile GUIs, parsers, machine-learning based data extractors, and distributed systems. I also lead a team, and I'm trying my hardest to bring a large, heavily used product into the modern era when it comes to web technologies. I was a huge fan of macros in college and the few years immediately afterwards. I've now done almost a complete 180 on them, and would encourage anyone I work with not to use them.

The reason is because I've come to understand that social factors trump technical factors nearly every time. The hardest part of writing a software system is almost never solving engineering challenges or writing code, it's communicating the solution to everyone who will have to touch it. Over a software system's lifetime, you will have to read code much more often than you will have to write code. Macros make writing code really easy and communicating everything about what it does, how it does it, and what invariants need to hold for it really hard. That's usually a poor trade-off for anything non-trivial.


I've found that working with a DSL structured around the problem domain improves communication and readability. Macros are a tool to allow you to easily implement these DSLs and have them fit nicely into the host language.


I actually like DSLs, but I think the right level of abstraction to develop them is by exposing the tokenizer, parser, AST, typesystem, semantic analyzer, and execution engine of the language as libraries in the language itself. Things like 'ast' and 'parser' in Python, package 'go' in Go, Clang for C++, etc. I've tried to fill this gap for HTML5 with Gumbo - it was initially designed to support a semantically-aware templating language in Google.

The reason I think DSLs are good but macros are bad is because DSLs make you think long and hard about the costs of implementing a new language. You have to pay for it up-front by writing a compiler and hooking it into your build system, even if you have help from the host language's libraries, and then you'll end up with something that's clearly a separate language and must be documented and learned separately. That cost makes it clear that creating a DSL is not something to do lightly, and you need a problem domain where there really are much higher-level abstractions that can't be captured by an existing programming language.

The problem with macros is that you can create a new language construct with a half dozen or so lines of code, use it in a few hundred places in the codebase, and now your maintainers have a few hundred problems.


But at that point what is the difference between a macro and a function, from the maintainers' perspectives?

Is it just that, when a maintainer sees a function call it's easier to guess what it does, then when they see the use of a macro?


But many DSLs can be implemented just as well without the use of macros.

This does not refute your point at all -- that macros can be very helpful when implementing DSLs -- just wanted to point out that macros are not a requirement for doing so.

Also, a DSL expressed using first-class entities will fit into and integrate with its host language more nicely.


While I completely agree with the second part of your comment, you didn't had to throw us your knowledge, skills and experience, it's anecdotal evidence and it weakened your point which was already strong enough.


I disagree completely, it's good to know from what position someone is arguing. And it's also interesting to see that someone with similar experience to mine came at completely different conclusion. So thanks nostrademons for the comment. While I disagree with most of what he wrote (I agree only with social factors trumping technical ones)it was still relevant as a whole.


Well, I think potentially made up (and practically uncheckable) experience and credentials don't make up for a trustworthy position, therefore they are completely unnecessary on a pseudonymous internet forum.


He was addressing "go learn some more (let's say 5) languages and then come back"


He could just have said "I did this previously and...".


> macros are good; using them is good;

Unless your problem doesn't absolutely need them. It's considered bad style to use them when not absolutely necessary because they are much harder to reason about.

Macros can be dangerous in the 'give a man a hammer, everything looks like a nail' way. Like when some Python people went mental over meta classes using them everywhere they weren't needed.

I love macros (in lisp) but you shouldn't need to use them that much. They aren't always good.


> I love macros (in lisp) but you shouldn't need to use them that much. They aren't always good.

Isn't this true for almost everything? For example a while back there was this talk "Stop writing classes", where someone argued that you should just use functions for many simple cases (with nice examples etc).

Generally you should abstract on the level most suitable to the problem at hand. Some problems are best (for some value of "best") abstracted with a global var, others with a set of functions, others yet with hierarchy of classes. Syntactic abstraction is just another tool in your toolbox and it is very handy sometimes; of course it's not fit for every problem. But no one is going to do a "Stop writing macros" talk anytime soon, just because no one is writing macros - most languages lack them completely or have only silly string-oriented preprocessors, and even in languages that support them macros are feared and avoided.

I would just like to see macros go "mainstream" - if this means they'd be abused a bit (like classes/OOP now, for example) then I think I can live with it. Probably - it's quite possible I'd come to regret it very quickly :)


> I have only one thing to say here, to you and to all people who are opposed to macros and new languages in general: go learn some more (let's say 5) languages and then come back[1]. Don't bother before that.

You know nothing about sync, I presume, yet you assume he/she only knows very few languages because you don't agree. Don't do that.

I don't agree with sync, just pointing out that "go home and learn some programming first" is not a great argument.


I already apologized for this in the post. I realized this after writing it and it was late and I'd have to rewrite much of the post and so on. Essentially you should read all "you" in the post as directed at the me from X years back; it's not directed at sync or anyone else really. Sorry for the confusion.


it seems like you are validating @sync's concerns: you are saying it's ok to make up your own language if you have collected good patterns from other languages.

please correct me if i'm misunderstanding.


> if you have collected good patterns from other languages

Not at all - I think it's ok to invent languages, period. Little or huge, DSL or general, using established syntax or creating new ones - making them is good. The results can be bad (and frequently are), but that's to be expected of every process.

Knowing different languages just helps in seeing syntax for what it is: another level of abstraction, not a set of rules cast in stone. You can come at the same conclusion with a proper university class or with any number of other means - I mentioned learning languages just because that's what worked for me.


Do you know exactly what your compilers are doing? Do you open your compiled C programs in a disassembler? I don't think worrying about "correctness of the implementation" is constructive.


It doesn't matter what the assembly looks like, if the definition of language is well-defined. With macros this is not true, different 'macro' definitions probaly work in a lot of different ways.

It's true that different compilers and interpreters sometimes do slightly different things, but atleast there's a standard which should improve over time. I don't see that happening with macros.

Not sure how modularity is achieved with macros either.


Macros CAN be well-defined, but I think the cherry picking of macros mentioned in the article leads to a "language" that is ill-defined vs picking up CoffeeScript or LiveScript. The idea of implementing ES6 via macros is pretty attractive though.


Why can't macros be well defined?


They can be, but rarely are.


I think it's less about correctness of implementation and more about abuse of the interface. Not that I agree, but I think it's a completely valid criticism, in the same way that you could argue against operator overloading in OOP languages.


Ummm... yes? Sometimes you have to. I was looking into a bug about a year ago that was caused by the compiler I was using (Intel) wasn't properly saving the return address in some situations. This led to seemingly random crashes and thrashed memory.

So yeah, "correctness of implementation" is something to worry about, you just haven't had to (yet).


I think the idea would be to do something like Racket's approach to Scheme, wherein the vast majority of the language is implemented via macros on a very small core language.


Why is this a problem with JS and not, say, Clojure?


I'd be afraid of inheriting any codebase that made extensive use of these JS macros.

I will not be doing metaprogramming again any time soon.

I'm doing just fine using composition/iteration and creating better, more fluent APIs.


I suppose you're not the first person to attack macro systems, but can you be clear why it would devolve into madness? It seems like you're saying that developing a common language within an organization is madness, even though this just appears to be factoring common work into syntax.

In the example you cited, it seems like a pretty good language versus other examples I have seen for defining Javascript types?


As someone with decades of Lisp experience, I can assure you that macros can devolve into madness. They have to be applied judiciously -- just like, say, operator overloading, and for similar reasons: you're not just adding new APIs to the language, you're adding new syntax.

So there will be newbies who make messes with macros. It's happened in the Lisp world for decades -- though not as often, probably, as some people fear -- and it will surely happen here. Some people will overreact to that (as they have to misapplication of operator overloading) and prohibit their teams from using them. Eventually, knowledge of how to use them well will spread wide enough that most people will be able to be trusted using a language that has them, even if that means, in many cases, that they know they don't have the experience to use them properly and so avoid writing their own.

In this vein I'll close with a quote from the great Lisp hacker David Moon: Functions compute; macros translate.


Agree. Implementing classes with macros is not only useless and potentially harmful, but also redundant, since ES6 has classes (which can be used today with dev versions of Firefox, or compiled with Traceur).


You and the comment to which you respond both seem to be mistaking the specific example on the sweet.js homepage, which is explicitly described as providing ES6-standard functionality early, for the limit of the possibilities inherent in a hygienic macro system. This is a pretty grave mistake, in that it makes understanding the proposition at hand nearly impossible.

sweet.js is more or less a Lisp macro system for Javascript. Gaining some familiarity with Lisp macros would probably serve you well in terms of being better able to understand what's on offer here and how useful it can potentially be.



good. now you know how everyone else thinks of you using compilers.

Can we all agree that each project needs to use a set of utility functions and move on?


Glad someone finally wrote a post about this. Coming from the lisp world, that's the thing I constantly see missing from languages wrapping javascript. They generally change the syntax a bit, add a few features to minimize boilerplate in some areas, and call it a day. Why would I want to learn a new syntax to program in a language I already know well? Unless you give me the tools to write code that writes code, I'm not going to bother learning/using a new javascript syntax.


At least TypeScript brings to the table something very valuable: optional type checking.


And sweet.js macros can be used to implement the type system of your choice in otherwise stock Javascript. (For that matter, you could un-overload the + operator so that it only works with numbers, and invent a . operator to do string concatenation.)

I can see a potential argument that doing type checking via macros costs more than precompilation in efficiency terms, but on the other hand, that's not necessarily so. How does TypeScript do type checking for non-constant values? That is, if I define function foo(bar: string) in a TypeScript library, then compile it, load the library in a Javascript interpreter, and call foo(Integer.parseInt("42")), what happens? I'm guessing that either foo() misbehaves, or that foo() throws an exception.

In the former case, there is no benefit to TypeScript because your type annotations are evanescent and misleading -- you can't rely on them, so you either have to write fences around input values just like you would without them, or risk your code misbehaving because it's written to rely on unreliable type annotations.

In the latter case, TypeScript is adding those fences automatically, so that a function which expects an integer argument can check whether it actually got one and fail if it didn't. That's basically what a macro system implementing type checking would do, too, and it seems to me that in both cases optimization would be merely an implementation detail.


It's not practical to implement a type system using macros. The problem is the software ecosystem. First of all, I don't think sweet.js macros can implement a full type system like TypeScript has which can load type definitions from other files, do type inference and support interface definitions.

Secondly, it's difficult to get a community around a single type system. Thirdly, it's difficult to get IDEs to support these sweet.js macro type systems.

The fact is, a sweet.js macro type system which is widely in use and supports everything TypeScript has and is supported by many IDEs doesn't support, and is unlikely in my opinion, if not technically impossible. In theory it might work.

If you compile TypeScript and then call it using JavaScript with the wrong type the code will misbehave. It's a downside, true, but a dynamic type checking system written in JavaScript would be too costly. Sweet.js macro type system wouldn't bring any benefit in that regard either.

You can rely on TypeScript type checking to make sure your code is correct. Similarly, if you develop in JavaScript, you can use unit tests instead of type checking to make sure your code is correct. Unit tests nor type checking can't guarantee that other code is correct, even if that other code happens to use your own code.

The benefits of TypeScript's type checking include the elimination of a class of unit tests, automatic refactoring, full intelli-sense support, better readability and automatic documentation (which doesn't remove the need of manual documentation).


You should have a look at Typed Racket [1] which is a type system for an existing language (Racket) built entirely with macros [2] that satisfies all of the criteria you want -- works with the IDE, safe interop with untyped code, etc.

[1] http://docs.racket-lang.org/ts-guide/ [2] http://www.ccs.neu.edu/racket/pubs/pldi11-thacff.pdf


There's also TypedClojure (https://github.com/clojure/core.typed) which is based on Typed Racket (is what I read in previous discussions about it here).


Wow, that's impressive. I didn't realise macros can be that powerful in practise.


Maybe this will help see the full scope of the power of macros: macros receive their arguments as an AST (i.e. a list) and are free to transform it as they please; they also have full access to the host language and are free to implement a type system checker that walks code and checks that all the types match. There are no restrictions here - the type checker would be regular scheme code that would be wrapped into a macro simply to make it run at compile time. So there's nothing less powerful about macros at all. It's just code that runs at a different stage (compile-time instead of run-time). After macros, in terms of power, you have reader macros, that receive their arguments as raw text and are free to turn it into any AST.


> For that matter, you could un-overload the + operator so that it only works with numbers

No you can't. First of all, you can't implement operator overloading, because Sweet.js is quite limited, as the macros have the form:

   <macro-identifier> <expression...>
Also, because you don't have the types when Sweet.js compiles the code, it means that you can't distinguish between numbers and strings or what have you.


Huh. This is what I get for talking something up before I've had a chance to play with it for myself -- you're right, Sweet.js doesn't allow for arbitrary syntax definitions, just those of the form keyword arguments body, with macro-expansion only on body, and no way of capturing the symbol prior to keyword.

I can still see a potential method of rewriting functions, whose arguments were decorated with type specifiers, to provide type checking. But that would involve prefixing the function body with type checking code to be evaluated at runtime, which would probably produce a monstrous increase in execution time, and would be ugly either way.

Well. I suppose I shouldn't have expected anything different, knowing that a real macro system requires support at the language level, and that a retrofit like sweet.js is perforce going to be limited in what it can hope to accomplish. But I find myself disappointed nonetheless that sweet.js is what it is, rather than what I wanted it to be -- and, I think, what a lot of people, not least of whom the author of the linked article, imagine it to be.


Actually sweet.js just landed infix macros [1] so you can match on syntax that comes before the macro name. This just happened last week so we haven't updated the docs or made an official release just yet.

[1] https://github.com/mozilla/sweet.js/pull/162


That's awesome.


Sweet!


Unfortunately this contribution is inhibited from being significant in value by the fact that TypeScript doesn't support full gradual typing [0] and has an intentionally unsound type system [1].

[0] http://siek.blogspot.com/2012/10/is-typescript-gradually-typ...

[1] https://typescript.codeplex.com/discussions/428572


Accidentally unsound is a problem, but intentionally unsound just means that they balanced soundness against other design criteria such as completeness, complexity, and tooling capability, then decided that they were willing to trade soundness for other things.

For onlookers, here's a lucid explanation of soundness and completeness in type systems: http://eschew.wordpress.com/2009/08/31/sound-and-complete/

The author of that article argues that soundness is more important than completeness, but concedes that there are other ways to view the world too.

The bottom line is that most ad-hoc lint tools are both unsound and incomplete, but they are still useful for finding bugs!


Since sweet.js code can't be directly run in the browser, you need some "compiling" or "interpreting" step. It's not obvious why this is any different from a compile-to-js language that had support for macros.

For example, someone wrote a macro system for coffeescript: https://github.com/mrluc/macros.coffee

Related discussions: https://github.com/jashkenas/coffee-script/pull/3171 https://github.com/gkz/LiveScript/issues/328


This isn't intended for people interested in other languages; if you like CoffeeScript, certainly use it. I really like ClojureScript myself. But there's a lot of reasons you already need a build step for JS anyway (especially for client-side), and this gives you a lot of power without having to completely switch languages.

It's worth noting that sweet.js is a robust macro system grounded in academic research (the core of it from the Honu paper). That CoffeeScript system is a bit of a hack and its flaws would show it the whole community started using it (I don't have time to go into this right now, but things like hygiene, etc are really difficult).


Hi, I made macros.coffee, and I'd like to say that this guy is right. It's non-hygenic (on purpose) and it's a hack.

And purely a labor of love! Slides: http://mrluc.github.io/hammy/12-7-rum-macros/deck.html#1 Code: http://mrluc.github.io/macros.coffee/docs/macros.html

I wanted to see how easily I could add (non-hygenic) macros to CoffeeScript, and if I could do it as an npm-installable module instead of a fork of the language.

It was also an experiment in making macros very clear to myself. Non-hygenic (CL-style) macros are very simple conceptually.

But the basic hack that made it possible to get true CL-style macros also makes the whole thing incredibly dependent on the structure of the CoffeeScript AST. Which makes it fragile.

I'm excited by BlackCoffee, actually. This is the first I've heard of it. Thanks a lot for giving me a nod in the github PR, and of course let's mention Oran Looney as well, who wrote the best deepcopy ever for Javascript (I wrapped this up as owl-deepcopy and made an npm module for it).


You can think of sweet.js like a modular compiler. Let's say you wanted to use the @ syntax or the -> function syntax from coffeescript. Include a macro that does it rather than build a full compiler to do it. Coffeescript could then be a collection of macros rather than it's own compiler. Those macros could be shared across other projects or you can pick one or two for a lightweight Coffeescript-esque experience without having to port your whole codebase.

There's a point where I think this breaks down. It's great for syntactic sugar but I think things like type analysis would become a bit more difficult.


The Lisps in which this concept has its heritage call that step "macro expansion". Granted that that won't happen properly if you use script tags to load a file of Javascript with macros -- but consider, for example, the possibilities inherent in making macro expansion an integral part of the loading process carried out by a library like Require.js. Macro expansion tends to be a lot lighter-weight than compilation does, because it's a simpler transformation; there's no obvious reason why integrating it into Require, or a similar library, should be particularly difficult to accomplish.


JavaScript compilers aren't only for syntax sugar. Closure Compiler, Dart2JS, and GWT are optimizing compilers. Most large Google applications for example would be hideously large without an optimizing compiler.


Definitely. It was a terse statement for simplicity. I was hoping that my sentence "it's not good for extending javascript" clarified that.


This is basically just saying "Choose Sweet.js in combination with some unknown array of macro libraries as your only language that compiles to JavaScript."

But because the article doesn't really frame it this way, it doesn't really explain to me why I would choose Sweet.js over ClojureScript or Scala.js or Black Coffee or whatever.


I barely trust Guy Steele with macros. You know who I don't trust with macros? Random JS coder dudes that haven't learned what true hardship is, when debugging a macro that is 5 levels deep and subtly changes the language because, hey, I have a macro! And having a macro means using a macro.

Language design is hard. Scheme has gotten it wrong. Over. And over. And over. They didn't get hygiene right for more than a decade. They still don't have it right. But people think they can easily invent new syntax that somehow doesn't do unexpected things. Your clever macro isn't so clever when some poor son of a bitch has been beating his head on a bug all day long just to find out your macro-that-looks-like-a-function isn't evaluating all its arguments because your macro-that-looks-like-a-function is really a function-that-is-a-macro. Don't get me started on missing IF branches and undefined behavior...


I would also recommend the WISP: A homoiconic JavaScript dialect with Clojure syntax, s-expressions and macros. More details at: https://github.com/Gozala/wisp


I strongly agree with everything the author writes, but I believe that sweet.js might not go far enough. For example, I am very fond of TypeScript. I'm also very fond of React's JSX. Yet, I cannot mix the two, and neither JavaScript extension could be expressed as Sweet.js macros.


>>>> "What if JavaScript actually adopted macros natively so that we never even had to run the build step?"

Honestly, that's pretty damn exciting. In my book, if JavaScript did that, it would very nearly complete a "worst to almost-first" transformation as a language. Starting with its early versions and all their ugly "bad parts", and advancing all the way to one of the most exciting, expressive languages to work with.


I find the title kind of ironic, given that sweet.js is still a compiler. :P

However, it is true it would be better to have one standard-ish compiler that can just execute whichever macros you throw into it.

That being said, you still run into the issue that `sync` brought up.


Best way I know of using macros in JS is https://github.com/Gozala/wisp, which generates pretty readable code. If you're going to pre-process, you might as well write in a nicer language...


So, after puzzling around with the macro for swap a bit, it seems like macros are like functions, except without the scoping rules, which suggests to me that we now have two kinds of functions, to keep track of in the code, each of which look identical, but behave differently. swap(a,b) looks like a function, but can interact with all the variables in the same scope (or above) as when it's called (is this the same for global functions?). Maybe I'm missing something big, but that seems dangerous when the macro is declared in another file, and I have to go figure out how it behaves.


Macros are not functions, macros are a mechanism for adding features to the language itself.


Maybe I wasn't clear. I am not saying that macros are functions. I'm saying that without seeing the implementation, both look identical, and thus could be conflated, but do not treat scope identically.


How do you deal with naming collisions of macros from different developers? Are you considering to introduce name spaces? To me macros as well as other surprise code modification stuff seem unfeasible in teams with more than a few developers. Don't you think you risk maintainability with a huge macro apparatus?


We're working on integrating macros and modules to address this. Once it's implemented you'll be able to do something like `import { m } from "macros.js"`.


sweetjs was a good idea, but I've found it hard to use, or rather..it's not the using part that's hard, but writing your own macro.

sweetjs has this hygienic thing that renames identifiers. To prevent it from renaming, there are many hoops that you have to jump through to prevent it.

sweetjs introduces of a lot of new concepts. These concepts are not as easy to understand as the ones in C. The macros are certainly more powerful. It's just that assuming it's C-like macro will give you the wrong kind of expectation with sweetjs.


Of course! These aren't C "macros"; they're Lisp macros, which is a wholly different, and far more valuable and reliable, proposition.

Granted, I'm not sure how much I like the idea of a macro system which finds macro instances by string-matching the source it's given. But in a non-homoiconic language, I'm not sure anything else is feasible, and I can easily see how something like this could improve Angular.js's dependency injection, for example.


Unhygienic macros aren't a C-ism: hygiene was an innovation (I think originally in Scheme). Ordinary Common Lisp macros are unhygienic.


So are sweet.js macros, or else they wouldn't need to have reimplemented GENSYM. But I get the impression (parent (parent)) had sweet.js macros mixed up with C-style string-replacement "macros", which was what prompted my comment.


Sweet.js is hygienic but you can choose to be unhygienic if you want.

It is the best of both worlds.

http://sweetjs.org/#getting_dirty__breaking_hygiene


I'm not super into the idea of code running on code because I likes it as simple as I can gets it. But, I know this notion is popular with lispers so I get it.


Don't be fooled by a legion of old-time lispers. Macros are a last resort in real-life Lisp (aka Clojure).

Hard to write (way harder than a function), hard to debug, hard to reason about, they're not functions (so fit worse in the rest of the language)... but powerful when you really need them.

"When you have a hammer...", and a desire to stand out from other languages made the lisp-macro myth flourish.


Well actually, in a modern lisp (like Racket), you use macros in a variety of ways.

In addition to "deep" things, often you use them as practical, simple alternatives to using stuff like "snippets" or heavy IDEs. You can "DRY" annoying patterns, without resorting to external tooling.

Although macros used badly can be mysterious, so can any excessive pre-processing and build magic.

Macros provide "an API for the compiler", and since the compiler is involved, it can be smarter than external tools.


Racket? I said real life! (just kidding :P)

As I said, when you really need them they're useful (e.g. in typed Clojure). Most (if not all) DRY patterns can be fixed using only functions.

The problem is, the macro mantra has been parroted for so long now it's part of the Lisp culture and its external image ("Lisp is homoiconic! You can modify code! Macros! MACROS!"), when it's actually one of the ugly (but powerful) parts of Lisp you should avoid most of the time. This confuses beginners and people interested in Lisp.

I see macros fitting mainly in DSLs (which is probably a code smell most of the time) and extending the language, as typed Clojure does. What other real use cases do you see for macros?


The real use cases for macros boil down to 3 main areas:

1. Changing order of evaluation.

2. Creating new binding forms.

3. Implementing a data sub-language a.k.a. DSL

Although arguably #3 doesn't require macros, arguably it requires macros to do elegantly (Rubyists might argue not) and reliably (instead of monkey-patching, using hygiene and a macro-aware module system that can handle towers/layers of such systems).


Well, 1 and 2 are the only cases where they are absolutely required, but I've seen other use cases. For example, the ClojureScript templating library Dommy uses macros to optimize selector code at compile time, which gives some impressive speedups (IIRC Prismatic found it to be twice the speed of jQuery).


Modern Lispers (and modern Lisp learners' resources such as Seibel's Practical Common Lisp) talk about macros in exactly this fashion. I'd be willing to argue that the lesson has been learned.


'real-life Lisp' (aka Clojure) is just bullshit.

There are many more real-life Lisp applications which are not written in Clojure.

I'm a long-time user of Scheme and especially Common Lisp. Macros are a consequence of Lisp. Lisp is originally about computation with symbolic expressions (see McCarthy). Application of that to Lisp itself leads to macros and similar. Macros were introduced 1963 to Lisp, extremely early on.

As a Lisp programmer one needs to understand what macros are for and when they are useful. By default I write functions. I also tell people to think in functions first.

But in real life, the more advanced Lisp applications are full of macro usage and macro definitions.

Sure they are harder to debug. But Lisp programmers have developed tools to deal with that. Not everyone wants to learn such a complex language, or is able to learn it or has time to learn it. There are a lot of simpler tools and even simpler Lisps - but for those who want and need this, a language like Common Lisp will provide the necessary flexibility.


Did you had a look at [gorillascript](http://ckknight.github.io/gorillascript/)? Its [prelude](https://github.com/ckknight/gorillascript/blob/master/src/js...) is written in gorilla macros :)


I enjoyed the article. I just have a factual correction: Lisp had real macros as early as the 70s, not the 80s. (Maybe even earlier, for all I know.)


Lisp macros were introduced in 1963 by Tim Hart at MIT.


Here's the original technical report:

ftp://publications.ai.mit.edu/ai-publications/pdf/AIM-057.pdf


If you need the same for Java: https://github.com/norswap/caxap


Very interesting! I've often thought that Javascript could actually be a pretty language with some syntactic sugar. It'd be even more interesting if you could run it client-side without preprocessing an sjs file into js first, sort of like how you can include LESS stylesheets and load a JS module to compile them in the browser.


You won't have good IDE support.

You won't be able to directly debug your code.

Also it can be really fun to reason about nested macros.


You can also use C macros with Javascript http://vpj.svbtle.com/coffeescript-and-macros-clean-and-fast


So wait, what does this have that's better than C macros apart from hygiene? I'm afraid I really don't follow.


There are many reasons why people are writing *->js compilers, lack of macros isn't the least of the problems for js. Please keep the compilers coming, no productive coder really wants to write js.


The author's point is that the best way to implement Javascript sugar like CoffeeScript is by writing sweet.js macros rather than by writing compilers.


I think things like syntactic sugar is perfectly fine, as long as I can desugar it in a straightforward way. I want to be able to programmatically desugar some piece of code, not have to Google it each time I am curious.

I think that if it is easy to investiage things like syntactic sugar, and not have it be buried in something like a compiler or lang spec, then DSL/language implementers (and anyone else, if the language permits it) could get away with implementing things that objectively make the language more complex to deal with, because to decipher it is only a query away, anyway.


Right. IDE features would be needed such as Emacs features which show the expansion of the macro you're on.


Sup dawg, I heard you wanted to stop writing JS compilers and start using macros so here's a JS compiler that implements macros.

/head asplodes


Don't tell me what to do. If I want to write a JS compiler, I can write a JS compiler, and it's nobody's business to tell me not to.


Don't post garbage on Hacker News.




Consider applying for YC's W25 batch! Applications are open till Nov 12.

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

Search: