I spent some time during the late 90s learning all kinds of cool ways to use Lisp, courtesy of a couple Boston startups, including IS Robotics (now better known as iRobot). And I'd helped introduce Scheme to another employer. Scheme made it ridiculously easy to create domain-specific languages, but it was obviously a niche technology.
Around the same time, I'd heard Matz speak to a room full of Lisp hackers at the Lightweight Languages conference. He'd elegantly explained why Ruby was cool several years before Rails became widespread. But I don't think many of us understood what he was saying until later.
When I finally realized—thanks to Rails—that Ruby could mimic many of the most common Lisp metaprogramming tricks, I mentally kicked myself and wrote this blog post. There were a lot of great responses from other bloggers, too. My goal was to help make other people aware of some fun new possibilities.
Anyway, if anybody has any questions, I'll try to answer them later this evening when I'm back online. It's fun to remember when this stuff was so new.
Metaprogramming is a pretty widespread feature these days. Even Rust, for example, has both Scheme-style rewrite-rule macros, and (unstable) programmatic macros, for example.
It's been tough for me as a new developer to learn this stuff without nearly as many resources as there are in the Python and Ruby communities for beginners, but there's no question this is the (near-term) future. Plus, a lot of that stuff just complects everything anyways and doesn't have to apply anymore (read: mutable state, no Value on Values).
Let's keep it Lispy. ;)
There's a reason there will never be a universal language.
Personally I'm happy equality doesn't work like common lisp. See here for a post explaining equality in CL: http://eli.thegreenplace.net/2004/08/08/equality-in-lisp
It's old and I don't use common lisp, so please correct me or the post if needed.
I've tried to get into lisp numerous times. Clojure was the one that clicked for me. I can't say if that's because of clojure or because it was just the right time for me after N times trying to get lisp.
Every now and then I poke at a scheme or CL. Scheme seems do-able for me, particularly Chicken Scheme, but I everytime I try CL, I come across what seems like too many functions to do what it seems like one function should do. Whenever I see something like SETQ, etc., that's a big turn off for me.
On the other hand, I really dislike Clojure startup times. Doesn't matter for server/long running apps, but when you are debugging or iterating, it gets to be a pain. There are some mitigations with their own set of issues.
Pixie and Hy have been brought to my attention. Pixie seems more or less abandoned, but Hy looks interesting. I haven't been able to tell from the docs if it has clojure datastructures (which are the best part of clojure), but Pixie does. So maybe sometime I'll try to bring those over to Hy and see if they get accepted.
Also, all programming languages implicitly have most of those equality operators. But yeah, Scheme's equality is a bit cleaner.
Not a super fan of Clojure datastructures. If I want to modify my cdr, than let my modify my cdr.
Note that linked-lists are only a good data structure in fairly narrow contexts (e.g. shared structure, certain types of mutation you can do). Since Clojure is focused on limiting mutation, it makes sense for them to use Seqs instead of linked-lists.
Racket's conses are merely immutable.
There is zero need to ever use SETQ for example. SETF is a pure superset of SETQ's functionality. For other examples, I don't know that I've seen a serious use of PROGV or PROG in this century either.
Most experienced lisp programmers never use EQ, since EQL is more well designed, and the overhead is either negligible or nonexistent. Yes it's annoying that EQUAL doesn't recurse into non-string vectors.
Useful equality in CL is roughly: EQL for comparing non-aggregate types and identity checking of aggregate types, EQUAL for comparing lists, and EQUALP for case-insensitive comparisons. Some people add in = for numeric comparisons, but that's a style question (seeing = means "these arguments are definitely going to be numbers" and not much else, as if you're doing an equality test between e.g. an integer and a float, then you're doing it wrong, and differing numeric types is the only time = is different from eql when all arguments are numbers).
Comparing non-primitives is typically done with domain-specific comparison functions, as there are many ways to decide if two arbitrary objects are "equal" (for the old-saw OO example of 2d geometric shapes, you might have shape-area-equal, shape-perimeter-equal ...).
Some things I like about CL:
I find its package and macro system combine to be an amazing sweet spot of simplicity and power for metaprogramming. Scheme's hygienic macro system is the stuff that earned several people their doctorates; CL's is explainable in under a minute, and 99% of hygiene problems are solved by the package system (IIRC Clojure inherits its macro system from CL, using namespaces to solve the "FLET" problem).
CLOS+MOP made me feel like perhaps OO wasn't the colossal mistake I'd always thought it was. It's not the only great OO system out there, but it is great and so much better than the various C++ derived systems that I was first exposed to that it will always have a special place in my heart.
The condition system is great. The only quick summary I can give is that you can have condition handlers run in the stack context in which the exception was thrown (which lets you do much more useful things than running in the context in which you establish the handler, which is how all other exception systems I've seen work).
Common Lisp has amazing development tools; in particular the combination of SBCL/SLIME puts nearly all other F/LOSS IDEs to shame:
It's a high-level language, but you can view a function's disassembly, capture instruction-level profiling data, recompile a single function and rerun all of that without stopping your program. The overhead of edit/compile/debug is essentially zero.
All that being said, I do need to take a look at clojure again (I went through Fogus's "Joy of Clojure" book when it was new, but haven't touched the language since).
1: http://pryrepl.org/ is apparently an attempt to bring some of this to Ruby, but I have on many occasions seen Ruby programmers say that "If you're using a debugger you're doing it wrong" and Pry doesn't seem to have a lot of users.
Personally, I greatly prefer ML style languages to lisp style languages. I get fed up with all the parentheses in lisps.
It's in the OCaml-to-JS family.
CL-USER 1 > (let ((n-macros 0) (n-with-macros 0) (with-macros nil))
(do-all-symbols (s (values n-macros n-with-macros))
(when (macro-function s)
(let ((name (symbol-name s)))
(when (and name
(> (length name) 4)
(string-equal name "WITH" :start1 0 :end1 4))
(push s with-macros))))))
1186 ; macros
182 ; with- macros
(with-slots (a b)
(+ a b))
(lambda (a b) (+ a b)))
Don't get me wrong, TCL is super cool, but it doesn't have quite the same powers as lisp. Although you are correct, it's a better lisp than ruby.
I remember tcl in the early 90s then it disappeared from my radar, mostly web and mobile apps. Honest question: is it still relevant in some application area?
So while it's not as popular, relevancy isn't an issue.
They had a Rails like stack in the first .com wave, but implemented in a mix of Tcl, Apache and C.
Tcl was used for almost everything, C for performance critical modules and DB FFI.
The interpreter was loaded as an Apache module.
We supported Informix, MS SQL Server, Sybase SQL Server and Oracle.
All major UNIX variants from late 99's and Windows NT/2000.
Lisp is a regular, explicit language. It does not strive for micro-density, but rather tends to achieve macro-density by allowing more expressive abstraction and transformation in the larger componentry of your projects. Its regularity around parenthetical s-expressions is the source of its expressive power.
Things like "(+ (aref x 3) 4)" are larger than "x+4", but the operations and source code representation in Lisp are directly accessible to matching and transformation without having to go through extra rigmarole to deal with classes of AST node objects or whatever. This makes the usefulness of these features immediately accessible for everybody, and useful for quick utility, not just major undertakings.
2) "Ruby gives you about 80% of what you want from macros"
I'm sorry, but this section is simply ignorant of what Lisp macros are for. Simple syntactic sugar is obviously possible with macros, but certainly not its major use. Providing abstract constructs that require custom code generation, creating lower-level boilerplate (like auto-creation of introspective data structures and customized support functions), and wrapping custom behavior around inline scopes are the primary uses I've seen and used. Ruby can do a little of it, but I see no "80% of what you want" there.
3) "Ruby's libraries, community, and momentum are good"
Quicklisp was created after this article was written, and has been a godsend to the community. Lisp tends to develop a very roll-your-own culture because of its quick accessibility of creating abstractions, but the ease at which you can publish & reach for libraries with QL has brought a lot of cultural shift towards reuse. There are a ton of both esoteric and practical libraries out there, and common libraries for threading, networking, OS interaction, etc, are the common case now (and actually have been for quite a while before Quicklisp as well).
There's tons of interoperating flexibility because the language provides a huge baseline (especially its object system), so it doesn't all need to lock into an on-top-of-Lisp infrastructure that would constrain the programmer's options. It's a good time to be in Common Lisp.
And a follow-up: https://news.ycombinator.com/item?id=2279453
I think HN should add a URL checker in the submit page.
Posts that are over a year old can be reposted. It's a way to get a fresh perspective on ideas.
If the URL checker were to present itself as a helpful aid for reposts ("You might want to use this title like the other 3 posters have used: X") it might avoid that result.
On a sidenote, I get a weird feeling from HN's simple UI/UX. On one hand, it feels like the old CLI machines that put the focus on "content" - kinda like "do one thing and do it right".
On the other hand, reading all the comments, getting notified for replies on comments, etc. is really hard. In this day and age, one would wonder if HN really hates JS (well, except for the upvote button).
They are however acknowledging some things that are broken and need fixing, and in recent days have been introducing small changes to correct those (eg. the unvote button, ability to fold). I do hear you on the notification for comment replies thing though, I was really surprised when I found that feature wasn't there, and at times end up missing out potentially good conversations because I didn't check for and notice the reply comment until too late.
Although, even top-tier programmers and professionals are eventually "human", and design rules apply to them as well. It's like saying these folks are more comfortable to use some strange user-undfiendly smartphone, while they mostly just use an iPhone instead.
They could also use a better CSS but who cares, text is king in this kind of sites. It displays quite well on mobile too, especially with Opera which reflows text after zoom (the very reason for using Opera).
But then again I thought "The Meme Hustler" was rubbish (well, actually, I think the reasoning would have been okay, but I thought the premise was totally, utterly, removed from reality), so I think it's safe to say me and Steve have a different taste in articles.
> LISP is a dense functional language.
> LISP has programmatic macros.
First of these is wrong and second is just picking something from feature list without any concern of practicality.
When someone writes Lisp as LISP and speaks about functional programming, I get the feeling that the author has not programmed with Lisp outside school (or knows only Scheme). Using toy examples don't help.
There are at least two major "schools" of lisp. Traditional Lisp style with Common Lisp as it's main representative, another being Emacs Lisp. The another is Scheme & Clojure camp that goes towards different goals.
I've actually spent something like a dozen years writing Common Lisp and Scheme macros for production software. But I try not to put the nastiest ones in blog posts. :-)
In my experience, real-world Lisp was often a surprisingly functional language, certainly by the standards of the day.
Based on working with real-life Lisp code bases at four different organizations, I also stand by my description of what Lisp macros in production software typically looked like in 2005. Sure, there was the occasional elaborate macro that implemented something like Prolog in Lisp, and it was glorious. But the actual day-to-day workhorse Lisp macros were less flashy—most of them delayed evaluation, improved syntax, or performed simple transformations.
Many of these things are extremely valuable, but you don't necessarily need the full power of macros to implement them.
The big stuff is there when it matters, but the little stuff makes up the day-to-day.
Also, most Schemes are way more traditional than Clojure. And no, Racket isn't Scheme.
Both Common Lisp and Scheme are multi paradigm languages by nature, but Scheme took philosophical step into different direction. This philosophical difference may follow into other Lisp dialects that are not exactly Scheme.
Meanwhile, I've been using Ruby for a while, and the metaprogramming capabilities are pretty cool. It sounds a lot like what all of the Lisp enthusiasts are saying is so awesome about Lisp to me. So I've always wondered what it is that's so awesome about Lisp that Ruby doesn't already have.
It's not always useful, but you'll be thankful when it is.
I don't want my teammates writing macros. When the whole programming community constantly talks about situations within regular, more expressive languages (with syntax) like Python et al, where teammates look at each others code and are baffled ... I struggle to see a scenario where inventing syntax through macros is a good idea, and won't make that situation so much worse.
> arbitrary syntax transformation in macros
> object system as powerful as CLOS
If I wanted object systems, there are plenty of options that don't involve so many parens, and do involve far more expressive syntax than lisps offer.
> and the ability to comfortably use almost any paradigm
I kinda want my team to pick a set of known paradigms. This is not an advantage. I've never used a modern programming language and thought 'this lacks paradigms, if only I could let my team invent some, that none of us will understand 6 months from now'.
> It's not always useful, but you'll be thankful when it is.
From what you've said, I won't. I'll just be upset my teammates invented their own language with macros and whatever paradigms they chose to use that day.
The lisp 'trade-off' (homoiconicity over expressive syntax) just seems like it's looking for power in the wrong places. Power like that is more often bad than good.
I don't see a meaningful difference here between Lisp and, say, C++. If you have a dysfunctional team writing C++, full of cowboy programmers who invent their own idiosyncratic abstractions without coordinating with anyone, the fact that C++ doesn't have proper macros doesn't really meaningfully limit the resulting damage. What stops it from happening in functional teams isn't that C++ the language prevents it from happening, but that the team has some kind of working process that allows them to successfully write code as a team.
I guess 'better' is the operative word I question here. You're creating syntax that no-one has seen before. How is that a good thing? Meanwhile in languages like Python, with syntax built-in, people build large apps without ever doing this, making their code far more understandable for the next person that touches it.
> I don't see a meaningful difference here between Lisp and, say, C++.
Agree :) (joke)
> If you have a dysfunctional team writing C++, full of cowboy programmers
At no point am I suggesting we can save ourselves from cowboys ...
> What stops it from happening in functional teams isn't that [...] the language prevents it from happening, but that the team has some kind of working process that allows them to successfully write code as a team.
But people are arguing 'the whole point' of lisps is that you can be free to do this, and that this justifies the 'paren-crazy', syntax-less language trade-off, and I'm yet to see any justification it's necessary to allow this at all.
What I'm saying is that all the lispy parens give you are footguns and all you're saying is that good teams don't use those guns. Well then why give them the guns at all. Why not give them a language with decent syntax instead.
No. Lack of macros makes code bigger, more error prone and harder to understand.
When I use something like cl-ppcre:do-register-groups, this one word hides all the loop orginization, and underlines meaning of local variables which are bound to regex groups.
Or cl-sql:select, which allows programmer to use SQL syntax as part of lisp while hiding special chars escaping, converting numbers to strings, ensuring sql-syntax is correct.
Or simplier example: gl:with-primitives, which hides glBegin(...); ... glEnd(). Note, it not just hides, it makes impossible to forget glEnd.
Macros makes life easier. Otherwise no one would use it. Macros such a convinient way to make an abstractions, that switching from Lisp to any other language becomes a great pain. One start to see tons of boilerplate code, which needs to be written by hands, checked for errors with eyes, which hides logic somewhere inside of boilerplate... It becomes better with practice, but nevertheless lisp nostalgia will last forever.
I mean, that's provably not true. Or at least not provably true. Let's me throw out at alternative statement: Macros make code harder to understand.
> When I use something like cl-ppcre:do-register-groups, this one word hides all the loop orginization
You don't need a lisp to hide things.
> Or cl-sql:select, which allows programmer to use SQL syntax as part of lisp
Eugh, why would I want SQL interspersed with my lisp.
> [...] gl:with-primitives, which hides glBegin(...); [...] Note, it not just hides, it makes impossible to forget glEnd.
Python has 'with' statements and 'context managers' that do this (make sure some cleanup happens). 
> Macros makes life easier [...] such a convinient way to make an abstractions
They might make life easier for the person writing them in the short term. And I can see how when you find a really solid case for a macro and document it well, open source it etc, it can be broadly useful, but then why not add that case to the damn language. Instead you open the floodgates to everyone in your team writing macros that aren't widely understood.
> Otherwise no one would use it.
A lot of people avoid them like the plague in all other languages.
> but nevertheless lisp nostalgia will last forever.
Maybe that's all it is though.
If lisp didn't have a with statement, you could add one yourself. And if scheme didn't have a with statement... well, you could always add one, but before dynamic-wind got added, it could break if you weren't careful. Some things you can't just patch in. Anyways, now you'll say, "but python does have a with statement!" But that was just an example: there are things python will never have that you can add to lisp yourself. Like multimethod dispatch and generics, anaphoric operators, lazy module loading, pattern matching, ADTs, regex literals (with readtables), non-deterministic operators (the classic amb/assert operators, for concisely expressing searching a list (although they're admittedly more toys than anything else)), and a full-on looping construct that allows for awk-style record processing of arbitrarily typed records from arbitrary sources (field splitting is handled by other functions). And that's just a few examples I came across. There are dozens of others.
That would never all be added to the language: there's way too much, and it's too specialized.
As for your theory that macros make code harder to understand, you don't have any proof. In fact, you have negative proof, as there's plenty of logic, good programming practice, and individual cases indicating that careful use of macros can make code easier to understand.
I don't want to. I want a language that has sensible norms.
> As for your theory that macros make code harder to understand, you don't have any proof. In fact, you have negative proof,
> as there's plenty of logic,
What logic? Show me. I simply don't believe you, as it's not provable.
> good programming practice
Still not proof. I believe the opposite, that it's not good programming practice to use macros.
> and individual cases indicating that careful use of macros can make code easier to understand.
So anecdotal evidence, not proof.
You have no more proof than I have. My personal experience is that in most mainstream languages, the most common arguments are over invention of frameworks/libs vs using existing ones, and those devs certainly would not be happy having macros thrown in the mix.
As for it not being good programming practice to use macros, it isn't good practice to use them excessively. However, they can be used to make your code much clearer. Here's the reasoning (no, it's not proof, you're right on that front, but it's better than what you've got, which is just your opinion, AFAICT):
The larger a codebase grows, the more likely it becomes to contain bugs. The most common way to decrease the size of the codebase is to abstract away common idioms, only express them in one place, and reference that from other parts of your program. This is what functions and objects do. However, some idioms are incapable of being expressed as functions or objects. These idioms must be repeated over and over again, and often result in insidious bugs when somebody gets them subtly wrong. Macros allow you to capture those idioms and express them once, eliminating bugs. They also make the code easier to understand, by abstracting away complex logic.
So, if you really think macros are bad, than what's wrong with that line of thinking?
Again, in your opinion. And sorry to start blunt because I'm trying to round this out as we're getting nowhere :)
To me, writing my code like it's an AST, amid a ton of parens is not sensible. I've happily not done so in many other languages for over a decade, and I'm very happy with that state of affairs (or at least I'm relatively happy with the modern incarnations of C-style langs, e.g. Rust, Crystal, ES6, and I would be very unhappy if I had to chuck that in tomorrow and write lisp).
Generally agree with your train of thought on abstractions, and see how you arrive at macros. But my take is that it's simply not a good idea to go as far as macros, especially when the syntax sacrifice is so great. If the language is truly missing something, take it up with the language authors.
(I've never encountered a situation in > 6 years of writing Python where I encountered something I could not do and needed macros for).
Again, my basic opinion is I see it as a trade-off, and one that I don't agree with. I think many others feel the same way if they get as far as understanding the purported benefits of homoiconicity. But most aren't willing to argue it on Hacker News and get downvoted to high hell (which is why I used a throwaway).
(I should note meanwhile in another thread someone wrote off the entire JS community as 'full of self-righteous assholes'  and got upvoted to top post).
Thanks for the discussion, it was an interesting experiment in arguing these thoughts I've had for a long time, as an 'abandoned' Clojure-curious dev. I'm a bit sad I didn't get any new answers, but appreciate the insight all the same.
As for syntactic "sacrifices", they're necessary for the most complex macros, not so much for the simple ones: rust has macros, too!
Because people in this thread are attempting to put-forward lisps' macros (and writing code as an AST) as some big win, without acknowledging that it comes with a massive cost. Lisp-y, inexpressive, paren-soup syntax. I'm saying myself (and presumably many others else lisps would be more popular) don't buy the trade-off.
If you like it, and your team like it then great. But don't act like it's definitely win-win, and people who choose not to use lisps just haven't achieved enlightenment yet.
But Lisp's syntax is actually pretty expressive and nice, once you get used to it, IMHO. You continually deny this, and while you're free to disagree (many do), you continually argue that the mine and other's opinions are Bad and Wrong. I find this frustrating.
I don't agree with them however.
What I'm arguing is that Lispers say that all the parens and lack of other, more expressive constructs are worth it because you get macros. I'm arguing that's a bad trade-off, because 1. that's a big sacrifice, 2. macros aren't a win.
Besides, who would invent their own language in macros for no reason?
Also, the syntax is one of the best parts about Lisp. It's agressively regular, so you'll never have to worry about screwups or ambiguity. And with readtables, the syntax can be more expressive than you might think. Although you did set the standards pretty low...
I think you're in the minority there. I would hope most people agree that Lisp 'sacrifices syntax in the name of homoiconicity'.
If not, then we have different definitions of syntax and I'm not sure any discussion on that is likely to go anywhere.
It also eliminates many syntactic bugs, makes searching & editing code easier for humans, makes editor tools easier to create and more broadly useful when they exist, and eliminates ambiguous precedence completely.
You begin to notice that syntax itself really is an anti-feature, after working across a wildly varying range of languages and styles, and designing many fully custom DSLs.
Getting rid of syntax that usefully expresses common cases absolutely is a sacrifice. What was that Turing tar pit thing about making things of interest easy? Lisp is powerful, but its lack of syntax makes it inexpressive.
> It also eliminates many syntactic bugs
> makes searching & editing code easier for humans
I disagree, code that lacks basic syntax such that programmers end up writing macros, does not make code easier to search and edit.
> makes editor tools easier to create and more broadly useful when they exist
I can see how that would work, because you're writing ASTs like you're a robot, which of course machines can understand better, just not humans.
> eliminates ambiguous precedence completely
That is a totally overstated problem. I've literally never been bitten by this in about 10 years of programming. Everyone in every other language just adds parens by default.
> You begin to notice that syntax itself really is an anti-feature
Having given Clojure I think 'a fair go', I just don't see that happening. Being able to read my teammates code will never be an anti-feature.
I just don't agree with the trade-off ...
Yet, humans can easily learn both styles. Whatever non-Lisp language you pick is as equally ununderstandable gobbledegook to non-programmers as Lisp. Even COBOL or SQL are way too explicit and finnicky for untrained, non-technical users to write complex applications. There's no difference in technical human understanding barriers between heavily magic-syntax'd code or explicit regular AST-like code. All it is is how used you are to one thing and not the other. (Rich Hickey's notion of the subjective "easy", as opposed to "simple")
Because most languages have traded off for the human-only case, having code writing code isn't even on the radar for most people. Write a few tightly-entwined code generators, and you'll start to see the barriers you hit. Full integration of these concepts opens up completely new productivity and clarity for both you the programmer, and your programming-assisting code, without any difficulties besides the programmer's arbitrary syntactic preference.
And you keep talking like it's a tradeoff. For you, it may be. But for many of us who write Lisp, the paren-laden syntax is actually enjoyable: it's unambiguous, easy to write so long as the parens match up, and easy to read if you've indented it properly. And if you've got a half-decent editor, it'll indent and match parens for you. With Emacs and paredit, it's even nicer than many other languages: you'll be flinging sexprs around before you know it :-D.
Futhermore, you seem to think 90% of Lisp is macros: in fact, I hardly ever write them. I'm glad they're there when I need them, but when I don't, I write perfectly ordinary code just like anybody else, except that it's more pleasant to write because there's faster iteration, better tooling for manipulating code, and a nice, regular syntax that lets me forget about it, so I can focus on the parts of my code that matter, and get s#!t done.
While I love _why's guide, which can be very useful for learning ruby metaprogramming (and ruby in general), the technique presented there is a bit outdated. Modern ruby doesn't require such an "ugly" style because we now have Object#define_singleton_method as a way of defining class level methods. There shouldn't be a need to use #instance_eval to define methods in the metaclass.
To say nothing of our macro superpowers. But then, CLers still insist on on keeping the shotgun hovering over their foot, and insisting it's the only way to go about writing macros. Syntactic Closures and Implicit Renaming are both pretty cool. You guys should check them out.
Scheme also has environments like DrRaket, even if the language now has moved beyond its Scheme origins.
I was confusing you with Rainer (who, by coincedence, goes by the username lispm on HN), who actually loves CL environments.
As for me, I set my store by Geiser. Integrated documentation from all the implmentations it supports, repl (as well as the capability to connect to a remote one), support for all three big Schemes, is an emacs extension, and has excellent docs? Consider me sold.
I meant in what concerns papers, videos and magazines articles back from those days.
Look at this example from the article:
(defmacro with-each-natural-number (n expr)
`(each-natural-number (lambda (,n) ,expr)))
It assumes that the programmer already wrote a function each-natural-number that does the required functionality when another function is passed into it. That's the Ruby way, because it only has functions, not macros. A good Lisp programmer would never write such inefficient code. Why create a local function here when you can easily write a macro that expands into a loop with expr as its body?
So, indeed, if you write in Lisp as if it were Ruby, then Ruby is an acceptable Lisp. If you write in Lisp as it's supposed to be written, then Ruby is ridiculously restrictive, and slow.
From Google's Common Lisp style guide ( https://google.github.io/styleguide/lispguide.xml#Macros ):
You should follow the so-called CALL-WITH style when it
applies. This style is explained at length in
http://random-state.net/log/3390120648.html. The general
principle is that the macro is strictly limited to
processing the syntax, and as much of the semantics as
possible is kept in normal functions. Therefore, a macro
WITH-FOO is often limited to generating a call to an
auxiliary function CALL-WITH-FOO with arguments deduced
from the macro arguments. Macro &body arguments are
typically wrapped into a lambda expression of which they
become the body, which is passed as one of the arguments of
the auxiliary function.
One of the problems here is that the function does get an anonymous function passed. That means the function can provide dynamic scope via constructs like CATCH, LET with dynamic bindings, UNWIND-PROTECT, and so on. But it can't rewrite any code in the passed function.
This can only be done in a macro.
> A good Lisp programmer would never write such inefficient code. Why create a local function here when you can easily write a macro that expands into a loop with expr as its body?
The nice thing about the call-with style is that it makes it easier to redefine your macro's functionality without recompiling all the call sites.
And hey, most of my Scheme code barely has any macros (so call my idioms a decade out of date, we have a macro system equivalent to CL's, and I've never felt much need to use it. And yes, most Schemes do have a CL-equivalent macro system, it's just nonstandard), and I still love lisp.
Lisp has many, many, many, cool advantages, but I still think never having to worry about syntax or indenting style is the greatest one: people underestimate just how much the little things matter. :-)