Ever curious about languages and language design - and always wanting to make my own, I've been investigating what makes "Lisp so darned good" with a friend. The opinions expressed here are my own - I don't know if my friend will agree with them or not. But I'm going to compare Smalltalk's use of BlockClosure's to Lisp's use of Macros and explain why in the end Smalltalk's approach is superior.
First, I need to explain why they are the same. So I'll do that with a nice practical example. Say you want to open a file, do something with it, then have it close once you're done. The regular code to do this would be something like:
file open: 'blah.txt'. file doSomething. file close
This has some disadvantages. If the doSomething fails, will the close ever happen? So, the code should be rewritten as:
file open: 'blah.txt'. [file doSomething] ensure: [file close]
Hey, this doesn't even handle the case where the file may not exist! So again, we have to rewrite the code to do this:
[file open: 'blah.txt'] on: FileNotFound do: [:sig | ^self]. [file doSomething] ensure: [file close]
Now, most programmers would take that pattern and copy+paste it around thinking they have finally got the answer. What if they don't? What if you need to change it again universally for all files. What if your input starts becoming a URL instead of a local machine file name. Too bad! You've Copy+Paste'd the code every where. In-steps where Macros and Blocks meet:
openFile: filename ifNotFound: notFoundBlock do: aBlock
| file |
[file open: filename] on: FileNotFound: [:sig | ^notFoundBlock value].
[aBlock value: file] ensure: [file close]
And to use it, we write:
self openFile: 'blah.'txt ifNotFound: ["do nothing"] do: [:file | file doSomething]
In Lisp, you'd also write such a macro. In Lisp it'd be called with-filename or something along those lines and it'd be written as such:
(defmacro with-openfile (filename, &body body) `let (file (open-file (filename))) body (file) close-file (file))
I'm no lisp expert and I don't know if the code above works, but the intent should be clear enough :)
So! What's different between using Macros and Blocks. First off, Lisp can do exactly what Smalltalkers do - except to use their function they have to write 'lambda' in front of the argument every time. Smalltalkers enjoy a syntax sugar for lambda which is [].
The macro does -not- take a lambda block as its argument. Instead, it rewrites and inserts code in to the calling location. This has two side effects:
- The resulting code will naturally run faster
- The resulting code will be much harder to debug
Instead of debugging you call to open a file and doSomething, you're suddenly debugging all the stuff you wanted to hide. In the Smalltalk block solution, the debugger lets you skip past all the code in openFile:notFound:do: and go straight to the block of code that is relevant to your program.
This is a huge win situation - it makes the land you live in - the code - easier to explore and work with. But, there is a big draw back. It runs slower.
Normally, code that runs slower shouldn't matter. Computers are fast and 'optimise' is the last step any programmer should take when developing software. So am I suggesting already that the only reason lisp macros win over Smalltalk blocks is speed?
Not quite. I think Smalltalk can win there too!
In a self-optimising environment, the block code should be 'unwound' or 'inlined' back in to the calling code. So should the code that calls the block. This results in a flattening of the code structure - exactly like what the macro does. Naturally, this requires a sophisticated dynamic inlining optimiser - but such a thing isn't out of reach.
There is one more disadvantage I'd like to sprook with the Lisp Macros. It's the "thought" expense. As a developer - why do I have to decide if a "piece of code" is a macro or a method? In the Smalltalk way of doing things, you don't. There are no macros and with the right optimisations important bits of code end up running like a macro any way.
In lisp though, the developer is required to -choose- at that point whether they are making a macro or a function. This is akin to having to decide if your number should be an Integer or a Float or a Fraction or a Double or.. something else. You shouldn't have to make these -optimisations- ahead of when they are required.