You can't say this about macros without being "afraid" of them, though. I tend to agree but admittedly have not worked much in any language where macros are widely used. I feel like, if all they are is "just functions" then why don't we all just keep calling functions like we've been doing?
> I feel like, if all they are is "just functions" then why don't we all just keep calling functions like we've been doing?
The major problem with function calls in most languages (ie. not Haskell) is the call-by-value evaluation order.
For example, we could try writing an `unless` function (an "if" which only has an "else" branch):
(defun unless (condition branch)
(if condition
nil
branch))
;; Delete "foo.txt", unless we've been told to keep it
(unless keep
(delete-file "foo.txt"))
Clearly this won't work for call-by-value: the call to `delete-file` will be executed before our `if` gets a chance to decide anything.
There are a few ways around this:
- Call-by-name/need: This is what Haskell does, but it turns execution order inside-out. This re-orders the code's side-effects, which is why Haskell has to enforce the correct ordering using datatypes like IO.
- Ban such uses. Just use "if". If that's not good enough, propose a language extension and wait for it to appear in compilers.
- Use thunks. To delay something, wrap it in a function. To force it, call the function. This works, but splits our values into two worlds, causing a combinatorial explosion of decisions to be made (should condition be a thunk? Should we return a thunk?):
- Implement a macro system. This acts on the syntax, before it's been evaluated into a value. This is splitting "callable things" into two worlds, functions and macros, rather than all values. This actually isn't an issue most of the time; since functions' APIs vary so much already (arity, return values, types, etc.)
- Do everything with syntax rewriting! That's what languages like Pure and Maude do. In effect, everything's a macro. It takes discipline to ensure execution follows a straightforward path though (which usually boils down to "write everything as if they were functions")
The major problem with function calls in most languages (ie. not Haskell) is the call-by-value evaluation order.
Another problem that keeps us from replacing macros with functions is that function arguments can't be the binding position that introduces a new variable. To use an old classic example,
(let ((x 3)
(y 4))
(+ x y))
could be translated to ((lambda (x y) (+ x y)) 3 4), but no function could introduce new names `x' and `y' into scope. Scope is a compile time notion, and a function happens at run time.