Why Lisp macros are cool, a Perl perspective (2005)
lists.warhead.org.uk
lists.warhead.org.uk
It will be a little harder because the story and comment ids are all mixed together, you really have to poll a lot to claim the right id number.
The worst thing about a highly expressive language is that you can do anything you want to.
Expressive power vs. intelligibility (for the sake of common convention and communication in a team setting) is a real tradeoff, and the "principle of least power" does partially explain the popularity of slightly less-powerful languages like Java, Python, and Go, where there is generally just one idiomatic pattern for accomplishing a given task.
And that's one of the qualities of Forth: to stop worrying, stop being scared of yourself, stop being scared of the future... Or more precisely it learns you to fear the right thing: namely the Great Evil of Accidental Complexity, which often disguise itself as necessary evil.
The real problem is that even with Lisp's local maximum for expressive power, it is still just a local maximum: Trying to bolt array programming or type systems onto Lisp ends up with something that doesn't feel much like lisp anymore and certainly doesn't feel like APL or an ML. Is there another maximum? I think this is still worth pursuing, but it doesn't seem to be something that a company even as big as Google or Facebook or Oracle can put their back into.
What's important to Facebook and Oracle and Google? Popularity. And "intelligibility" as a social construct definitely seems to be related to popularity, but I've never been convinced to believe you must sacrifice intelligibility to get expressive power, or that we can't make programmers any smarter, only that we haven't figured out how to do either yet.
That's a valid question. Lisp macros open up a new dimension programming and now we have the problem to debug/maintain code with macros. The SETF machinery in one way tries to simplify code (the code then has one style of assignments, not different ways and one does not need to know a special accessor - just wrap a form which looks like it GETs the value and Lisp will figure out how to SET the value). OTOH the SETF machinery is non-trivial. It's usually quite usable, but some of the details are complex - especially since it is user-extensible.
It's definitely something to look for: programmers in a Lisp project might need some extra education when it comes to macro programming, to make sure, that they have a good understanding of the basic mechanisms. Also it always makes sense in a team to have more than one person looking at a macro implementation.
Unfortunately most people are not meant to be. Most devs are not even very good at coding, and hence PR reviews, style guidelines, design recommendations, linters, etc.
Humans are very flawed creatures.
Now add to that:
- if a feature exists, it will be used.
- at work, you don't have unlimited resources, you have constraints, and you will take shortcuts.
And now you have a good picture of why, yes, lack of expressiveness is a something that you might want.
The core ideas are timeless really, but the examples might be a bit more relatable today.
I'm reproducing the ToC here:
1. Does syntax matter?
2. The ingredients of Clojure's syntax
2.1 Data literals
2.2 Macros
3. Consequences
3.1 Verbosity is a solved problem
3.2 Separation of concerns: code layout ⊥ program structure
3.2.1 Example: the Builder Pattern
3.3 Code = Data = Data Viz
3.4 Tooling as libraries
3.5 An 'all-tracks' language: embedding paradigms
3.5.1 Example: Web UIs
3.6 Saner language stewardship
4. SummaryBasically: it's a form of escaping, and like other forms of escaping the escape character itself has to be represented as an escape sequence.
So in other languages where \ is the escape character, you have to put \\ to represent an actual slash rather than the start of an escape sequence. Here = is the escape character followed by the hex of the character, so to represent a literal = it needs its own hex value 3D after it.
Note that 0x3D is the "=" character in ASCII, so "=3D" in QP is "=" in ASCII. :)
This email has probably been through a few conversions to QP and back again between different email clients. Perhaps some buggy client got confused between an ASCII "=" and a QP escape sequence or something like that.
(defmacro ?= (symbol-or-symbols expr)
(let* ((symbols (etypecase symbol-or-symbols
(cons symbol-or-symbols)
(symbol (list symbol-or-symbols))))
(tmps (loop :for s :in symbols :collect (gensym (symbol-name s))))
(err (gensym "ERROR")))
`(multiple-value-bind (,@tmps ,err) ,expr
(if (not (null ,err))
(return ,err)
(setf (values ,@symbols) (values ,@tmps))))))This is one of the big advantages of Lisp macros over C macros. In Lisp you write macros using Lisp, including normal user-defined functions. In C, on the other hand, you write macros using "C Preprocessor directives"
https://docs.racket-lang.org/guide/stx-phases.html
I understand there may be some philosophical or theoretical interest in framing Lisp the way you do, but does that invalidate the close-to-the-app thinking about the costs of macros and where those costs are distributed?
In SBCL, we would commonly say that code is "evaluated" rather than compiled or interpreted. This is because "compiler" is referring specifically to when assembly is emitted and "interpreter" is referring to when code is being used to call already compiled code. In the case of SBCL, it is doing both of these things simultaneously when it is reading a ".lisp" file. For example, if this is the contents of my .lisp file:
(defun boop () (print "hello"))
(boop) ;; prints hello
(defun scoop () (print "goodbye"))
The boop function is being evaluated (and therefore compiled), and then called on the next line. In traditional algol based languages like c and java with a separate compilation phase, it would not be possible to call boop before scoop has been turned into machine code.
So you are asking how the costs of evaluating lisp are distributed?
For one, the assembler generated whilst evaluating the .lisp file can be cached in a .fasl file so that it does not need to be evaluated again to be loaded into other projects.
Another is that macro's can be evaluated once during function definition. e.g.
(defmacro moob () `(print "kellog"))
(defun foo () (moob)) ; evaluates to (defun funky () (print "kellog"))
(defmacro moob () `(print "fish")) ; redefining moob
(foo) ; will print "kellog" because funky was evaluated before moob was redefined.
In this case, the cost of calling moob is trivial because it only occurs once during the definition of foo. It will slow down the initial load of your program, but no subsequent calls to foo.
SBCL has both an in-core compiler (compiles to machine code in memory) and a file compiler (compiles to machine code in an file).
SBCL also has an interpreter (relatively new and which usually is not used), which executes Lisp source as data.
When running in the REPL (the most common way to interface with Lisp) the compile time and runtime Lisps are the same.
For example, here's a copy/paste from a REPL session that defines a macro that defines a function. The macro (at "compile time") prints information about the function (just its argument count) to stdout before using defun (itself a macro) to actually define the function.
Next I call the new function and print out a disassembly, just to show the function is in fact compiled
"CL-USER> " is the prompt in the REPL I use:
CL-USER> (defmacro my-defun (name arguments &body body)
(format t "Defining ~a, taking ~a arguments~%" name (length arguments))
`(defun ,name ,arguments ,@body))
MY-DEFUN
CL-USER> (my-defun omg-2 (value) (* value value))
Defining OMG-2, taking 1 arguments
OMG-2
CL-USER> (omg-2 34)
1156
CL-USER> (disassemble #'omg-2)
; disassembly for OMG-2
; Size: 33 bytes. Origin: #x52ED2714 ; OMG-2
; 14: 498B5D10 MOV RBX, [R13+16] ; thread.binding-stack-pointer
; 18: 48895DF8 MOV [RBP-8], RBX
; 1C: 488BD6 MOV RDX, RSI
; 1F: 488BFE MOV RDI, RSI
; 22: FF1425C0001052 CALL QWORD PTR [#x521000C0] ; GENERIC-*
; 29: 488B75F0 MOV RSI, [RBP-16]
; 2D: 488BE5 MOV RSP, RBP
; 30: F8 CLC
; 31: 5D POP RBP
; 32: C3 RET
; 33: CC10 INT3 16 ; Invalid argument count trap
NIL
CL-USER>
It is also possible to compile a Lisp program to an executable (or byte code or whatever) and run it, and not use the dynamic aspect of it.In an interpreter version of Lisp, the interpreter may expand the macros at runtime.
What you do is, you use macroexpand-1, like so:
https://stackoverflow.com/questions/50754347/macro-with-a-li...
Lisp has no compile time vs run time distinction. It only has lists. What is done with a list depends on what the first thing in the list is determined to be:
1. Function - evaluate the arguments then pass it to the function.
2. Macro - manipulate the list according to the macro and then get another list.
3. Special Form - follow the special rules for the special form. (The list of special forms varies from Lisp to Lisp, and are the elementary building blocks from which the language was built.)
In Lisp, macros are advertised as code operating on itself, because that is exactly what they are. And yes, they are exactly that cool.
That is from one of the founders of YCombinator, the author of http://www.paulgraham.com/onlisptext.html and http://www.paulgraham.com/acl.html.
It is certainly true that macros are a different kind of code from most ordinary code, because what they're doing is different. But importantly, they are written in the same Turing-complete language. I once wrote a program (a C implementation for Lisp Machines) that allowed the user to interactively execute C expressions. Obviously, it had to parse the C code according to the syntax of that language, but the parser produced list structure that consisted entirely of macro calls. The guts of the compiler were implemented as a collection of macros that translated the output of the parser into Lisp Machine Lisp. These macros did things like accessing and updating a symbol table — far beyond what you could do in a C macro.
[0] https://www.ccs.neu.edu/home/stchang/pubs/ckg-popl2017.pdf
There are Lisps that improve on the situation. In particular, vau calculi/fexprs are worth examining. http://lambda-the-ultimate.org/node/4093
There are also systems like Forths, where the compiler and interpreter really are the same system, in two different modes of operation. In these systems, there is only runtime; compiletime is a specific style of runtime.
The main contrast that I would draw between Forths and Lisps is syntax. Forths don't really have syntax; they have token-parsing streams. Lisps are extremely tree-oriented, but Forths are stack-oriented.
In a compiled implementation, code needs to be compiled before it gets run. When I want it to be compiled, I have to call functions like COMPILE or COMPILE-FILE.
ti.arc.nasa.gov/m/pub-archive/176h/0176%20(Havelund).pdf
For compiled code, there usually will be no macro expansion at runtime. Lisp will not macro expand code in compiled code, since all macros need to be expandable at compile-time.
But: if you explicitly want it, you can generate code at runtime and then you need to call EVAL or COMPILE. Then macro expansions might happen.
"Most Common Lisp implementations support creating native-format executable files. The typical route involves loading all of the program's code into the Lisp environment, dumping the current state of the Lisp process into a disk image, and identifying a start function (ie main).
Since the disk image contains the entire Common Lisp system (compiler, debugger, documentation strings, etc.), the executable will be typically larger than a compiled C program (even if you statically link the C code).
Another form of disk image requires a copy of the particular Lisp implementation to be installed on the recipient's machine. This is no different in principle from a C application requiring platform-specific dynamic libraries to run. This complicates deployment - the disk image is tied to a particular implementation version, and you typically end up having to include the Lisp implementation executable anyway.
Due to platform limitations, some Lisp implementations can't produce native executables (for example, ABCL can only make JAR files). Other implementations are only able to dump disk images that require a separate copy of the implementation executable. An alternative to executables is available on Unix platforms in the form of shell scripting."
If it needs 4 wordy paragraphs and you still don't know how to do it, I am going to scribe that up as non-trivial.
I have this alias in my .zshrc:
clc() { sbcl --no-userinit --load $1 --eval "(sb-ext:save-lisp-and-die \"$(sansext $1)\" :toplevel 'main :executable t)" }
Sansext is a custom function that strips the file extension from a filename, but you don't need that; you can read output filename from $2, or whatever else.For SBCL one needs to call a function, which creates the executable image.
http://www.sbcl.org/manual/index.html#Function-sb_002dext-sa...
There are more complex machineries for application generation, though.