Right, but the complete argument isn't "there are other ways to do this", the argument is "there are other ways to do this, and nearly every one of them is less error-prone".
I feel like there's such a wide variety of things that you could be trying to do with macros that this definitely true a lot of the time.
The same is true for popular libraries and whatnot that ship with the language, which is why I'm not particularly concerned about "defn" or "for" are macros. Those are macros in most implementations, I'm aware, but they're really-well-tested macros because pretty much every Lisp developer to ever write a significant amount of Lisp has tested them. "defn" and "for" aren't broken.
So I'd argue for this differently: defn and for and other pervasive macros aren't safe because they're well-tested, they're safe because they're trivial. You know that (defn ...) is short for (def (fn ...)). You know exactly the code that that macro expands to. And you can choose to type (def (fn ...)), or you can choose to type (defn ...). Same with short-circuiting "and", or "for", or "+=", or whatever.
It sounds like you're mostly talking about complex, hairy macros -- macros where you don't know exactly what code they expand to. But I dunno, the fact that macros can create monstrosities doesn't mean that you should have to type (def (fn ...)). It's a feature with an extremely broad scope, and can definitely be mis-used.
I'll point out that JavaScript already had async callbacks and promises (implemented as a library) when async/await was added as a third way to do basically the same thing, resulting in JS codebases that now have half-baked glue to make the three ways work together. I'm not sure anybody was waiting for async/await to do anything that couldn't already be done, and the churn of reimplementing working code to use a new feature didn't do anyone much good. But that's sort of a tangent.
This is a very good point. Destructuring assignment, maybe? Arrow functions? I think we agree that some language features are good additions, even if I picked a bad example :)
Codebases that use extensive macros to create DSLs eventually become write-only. The power and readability you see in toy examples and in the short run in your own code, rarely plays out in the long run, and when it does play out it's because of extensive testing and work--a lot more work than Bob and I have the bandwidth for.
I dunno! I can certainly see how an extensive macro-based DSL could degrade to something write-only.
I don't know when the long run starts, but my experience with macros is that they're just a huge productivity benefit that I wouldn't want to give up. I haven't seen a codebase degrade into an idiosyncratic mess -- perhaps, in part, because there's a lot more friction to writing macros in OCaml than in Clojure, so macros are only used where they provide a substantial and obvious benefit. Or perhaps it's that typechecking makes it much harder to write poorly-behaved macros. Or perhaps I just got lucky with my cubicle assignment :)
So I'd argue for this differently: defn and for and other pervasive macros aren't safe because they're well-tested, they're safe because they're trivial. You know that (defn ...) is short for (def (fn ...)). You know exactly the code that that macro expands to. And you can choose to type (def (fn ...)), or you can choose to type (defn ...). Same with short-circuiting "and", or "for", or "+=", or whatever.
That's probably true of a lot of macros, but I would be surprised if the macros included in Lisp are all of that triviality.
And the biggest perceived payoff of macros developed in user-space is going to be the macros that aren't trivial.
This is a very good point. Destructuring assignment, maybe? Arrow functions? I think we agree that some language features are good additions, even if I picked a bad example :)
Sure, I see your bigger point, and arrow functions are a good example of it. I mean, obviously you can do `function(foo) { ...; return bar; }` and then use bind() to fix the fact that that doesn't close around the right things at all, but arrow functions are just so much clearly less error prone that there's no real argument they're an improvement.
I'm actually not aware of destructuring assignment being available in JS, but it's great from Erlang, so I'll have to look into that.
This is something I think about a lot because I'm writing a compiler/interpreter for my own programming language: programming languages often introduce too many features before they're really thought out well enough--meanwhile lots of those features would be just fine as libraries. The result is an inconsistent language with lots of ways to do the same thing that don't play well together. C++ has this problem so badly that they've developed toolsets for subsetting--i.e. choosing the set of features of the language you use and returning errors if you use features outside that set. And then there's stuff like C which just solves that problem by rarely adding features.
The most powerful languages, I think, tend to be ones that didn't add a ton of features, and made good choices on the features they did add. For one example, I think Erlang got their threading features really, really right, and a lot of other programming languages are going to regret going with other threading models and bolting on an Erlang-style model later.
I don't know when the long run starts, but my experience with macros is that they're just a huge productivity benefit that I wouldn't want to give up. I haven't seen a codebase degrade into an idiosyncratic mess -- perhaps, in part, because there's a lot more friction to writing macros in OCaml than in Clojure, so macros are only used where they provide a substantial and obvious benefit. Or perhaps it's that typechecking makes it much harder to write poorly-behaved macros. Or perhaps I just got lucky with my cubicle assignment :)
I dunno! I haven't written much OCaml, though I've written some F# which is supposed to be pretty similar. I do think making powerful-but-dangerous features harder to use can be a good strategy for dissuading their use except when they're really needed, so you might be on to something there.
Comments
I feel like there's such a wide variety of things that you could be trying to do with macros that this definitely true a lot of the time.
So I'd argue for this differently: defn and for and other pervasive macros aren't safe because they're well-tested, they're safe because they're trivial. You know that (defn ...) is short for (def (fn ...)). You know exactly the code that that macro expands to. And you can choose to type (def (fn ...)), or you can choose to type (defn ...). Same with short-circuiting "and", or "for", or "+=", or whatever.
It sounds like you're mostly talking about complex, hairy macros -- macros where you don't know exactly what code they expand to. But I dunno, the fact that macros can create monstrosities doesn't mean that you should have to type (def (fn ...)). It's a feature with an extremely broad scope, and can definitely be mis-used.
This is a very good point. Destructuring assignment, maybe? Arrow functions? I think we agree that some language features are good additions, even if I picked a bad example :)
I dunno! I can certainly see how an extensive macro-based DSL could degrade to something write-only.
I don't know when the long run starts, but my experience with macros is that they're just a huge productivity benefit that I wouldn't want to give up. I haven't seen a codebase degrade into an idiosyncratic mess -- perhaps, in part, because there's a lot more friction to writing macros in OCaml than in Clojure, so macros are only used where they provide a substantial and obvious benefit. Or perhaps it's that typechecking makes it much harder to write poorly-behaved macros. Or perhaps I just got lucky with my cubicle assignment :)
That's probably true of a lot of macros, but I would be surprised if the macros included in Lisp are all of that triviality.
And the biggest perceived payoff of macros developed in user-space is going to be the macros that aren't trivial.
Sure, I see your bigger point, and arrow functions are a good example of it. I mean, obviously you can do `function(foo) { ...; return bar; }` and then use bind() to fix the fact that that doesn't close around the right things at all, but arrow functions are just so much clearly less error prone that there's no real argument they're an improvement.
I'm actually not aware of destructuring assignment being available in JS, but it's great from Erlang, so I'll have to look into that.
This is something I think about a lot because I'm writing a compiler/interpreter for my own programming language: programming languages often introduce too many features before they're really thought out well enough--meanwhile lots of those features would be just fine as libraries. The result is an inconsistent language with lots of ways to do the same thing that don't play well together. C++ has this problem so badly that they've developed toolsets for subsetting--i.e. choosing the set of features of the language you use and returning errors if you use features outside that set. And then there's stuff like C which just solves that problem by rarely adding features.
The most powerful languages, I think, tend to be ones that didn't add a ton of features, and made good choices on the features they did add. For one example, I think Erlang got their threading features really, really right, and a lot of other programming languages are going to regret going with other threading models and bolting on an Erlang-style model later.
I dunno! I haven't written much OCaml, though I've written some F# which is supposed to be pretty similar. I do think making powerful-but-dangerous features harder to use can be a good strategy for dissuading their use except when they're really needed, so you might be on to something there.