If you've had experience with C and M4 macros, you've had too much experience with them. :) They just suck. The various branches of the Lisp family tree -- Common Lisp, Dylan, Scheme -- all have macro systems that don't cause crippling spinal trauma. But macros are really besides the point: they came up mostly because I wanted an example of a kind of abstraction that regular procedural abstraction doesn't work all that great at, and not surprisingly the obvious examples come from languages that have non-procedural ways of building abstractions. If I have the tools to capture an abstraction, I'll be more likely to keep an eye out for them.
For example, I really like Perl 6's built-in grammar support. Writing recursive descent parsers by hand isn't actually
hard, per se. It's just that the code comes in a very stereotyped form and it's a great idea to sugar away the repetitious bits, so that when you are debugging you don't have to first figure out whether an error happened because you made a conceptual error or just because you mistyped some boilerplate.
Another example is Ocaml's module system. It is, honestly, the closest I have ever seen a language come to making software out of interchangeable parts. The basic idea is to make linking part of the module language: you can write code that is parameterized on the modules it uses. Parameterized modules (called
functors) take some modules as arguments, and then build a module. For example, you can build a case-insensitive string map like this:
module CaseFoldCmp =
struct
type t = string
let compare a b =
compare (String.lowercase a) (String.lowercase b)
end
\t
module CaseFoldingMap = Map.Make(CaseFoldCmp)
And
just like that, you have a case-folding map module that has all of the operations of the standard library's Map module. This is very cool.