As I begin writing this, I am listening to songs of which very few are played on the radio. They are loud. They are heavy. They are intense. An unfortunate combination of genetics and too much time at a keyboard has left me hopelessly bald – woefully unable to grow the long hair of decades gone by – but I still catch myself headbanging now and again.
Although neither a composer nor a musician, I like to be highly present and immersed in my musical selections. What makes a heavy metal song enjoyable? A certain tensioned mix of subtly-varied repetition in the drums, bass, and rhythm guitar. Those oh-so-sweet signature riffs that set a given song apart from all others. And vocals… growling and screaming. Bang your head!
Errr… wait a second. I guess that this is supposed to be about computer programming and heavy meta, not heavy metal. Mea culpa!
Errr… wait another second. Subtly-varied repetition, characteristic uniqueness, semi-human utterances, and periodically banging your head? This could be about software development after all!
What Does “Meta” Mean?
Merriam-Webster tells us that “meta” comes from Greek, and means “more comprehensive” or “transcending”. More-comprehensive programming that transcends programming? Sounds good.
Modern colloquial speech uses “meta” to indicate that something is cleverly self-referential. Clever self-referencing programming? Sounds good.
“Meta” also has been backronymed to mean “most effective tactics available”. You guessed it: Sounds good.
Combining the above definitions, let’s say that metaprogramming means taking programming to a new level, particularly in a clever or self-referencing way, to maximize effectiveness. Sounds really good!
Shopping List and Reasoning
The first examples were developed using:
• SBCL 2.0;
• PostgreSQL 12, installed locally on the default port, with a database called “db”, accessible by user/role “user” and password “&dasPassw0rt”.
Initially, you will only need SBCL. You can wait on Quicklisp and PostgreSQL.
The samples are basic enough that versions hopefully are a non-issue. The “Common” in Common Lisp (of which SBCL is an implementation) speaks to achieving commonality between divergent historical dialects, not to widespread use; in light of potential language barriers due to my unusual choice of language, I have attempted to keep the examples particularly simple.
Let us focus momentarily on the proverbial elephant in the room: Why Lisp?! Its distinctive syntax features s-expressions (meaning “symbolic expressions”), which at first blush often appear to be Lots of Irritating Silly Parentheses. However, I argue that Lisp looks like a more-readable JSON that uses parentheses instead of square brackets.
Perhaps the comparison seems unfair: JSON is used to represent data, not to write Turing-complete programs. Lisp uses sexprs to do both. Programs are data, to be transformed and manipulated in much the same way that one utilizes JSON data; after transformation, they are different programs.
John McCarthy, mathematician and inventor of Lisp, originally had specified that Lisp should have a syntax more closely resembling that which is typical today. However, the computing hardware of the late 1950s was indescribably primitive compared with modern options. The s-expression representation of code was a time-saving measure – dare we say hack or even kluge? – that facilitated faster implementation of a working interpreter. It was intended to be temporary. Yet there emerged a group of programmers who preferred s-expressions, and Lisp never changed syntax.
Put directly: Lisp has the syntax that it does because it facilitates metaprogramming. The metaprogramming machinery is much simpler and more uniform than C preprocessor macros, Java annotations, or Roslyn compiler extensions. It will allow us to examine metaprogramming in a matter of lines.
Let’s Get Started
Start SBCL. At the prompt, enter the line:
(if (< 1 2) "smaller" "not smaller")
SBCL should inform you that the answer is "smaller". You may be questioning my sanity, but hopefully can find comfort in SBCL’s.
Now let’s do something twisted! Don’t worry about spacing; line breaks and spaces are for readibility in Common Lisp, yet are syntactically interchangeable. (Exception: The semicolon denotes a single-line comment that ends at the line break.)
(unintern 'if) (shadow 'if) (defmacro if (&body body) `(cl:if ,(first body) ,(third body) ,(second body))) (if (< 1 2) "smaller" "not smaller")
Wait... what did SBCL just tell us?! It said something about “IF”, but then... what?! Try again.
(if (< 1 2) "smaller" "not smaller")
What the... we are being told that 1 is "not smaller" than 2!
Congratulations on having written a simple metaprogram! Although about as useful as “hello world”, I hope that it was more entertaining. We have altered the language itself!
The unintern statement told SBCL not to use the default if. The shadow line indicated that we will be providing our own if. The defmacro statement essentially instructed, “Take any if statement and replace it with the standard if implementation from package cl, but swap the second (true case) and third (false case) body parts.”
What Common Lisp terms “macros” are more like compiler/interpreter plug-ins. For additional information, I emphatically recommend Let Over Lambda, by Doug Hoyte; said book takes Lisp macro building to the extreme.
Common Lisp further provides control over:
• whether code is executed at load-, compile-, or run-time;
• the meaning of individual characters (e.g. curly braces) in the language;
• exactly what happens when a class is defined.
The first two points, though useful, require a certain amount of familiarity and comfort with Common Lisp. Let’s ignore them. The third, however, is the focus of our next example.
A Useful Example
For this example, you will need Quicklisp and PostgreSQL prepared as per above. Start a fresh SBCL instance and type:
Quicklisp will grab Postmodern and its dependencies. Now type:
(postmodern:connect-toplevel "db" "user" "&dasPassw0rt" "localhost") (postmodern:query "select 'Hello, world!'")
Did you really think that we’d make it through without at least one “hello world”? The response should look like:
(("Hello, world!")) 1
The first value is a Lisp representation of a single row, containing a single column, containing the string "Hello, world!"; the second value signifies that the resulting list of rows contains 1 element. This tells us that Postmodern loaded successfully, that we connected to the database, and that PostgreSQL returned data for our SQL query. No metaprogramming... yet.
At the SBCL prompt, type:
(defclass ringy-dingy () ((area-code :type string :col-type text) (location :type string :col-type text)) (:metaclass postmodern:dao-class))
What is important to note here is that we are using the standard Common Lisp command defclass. Yet the col-type parameter in each member slot specification is not standard; indeed, our use of the postmodern:dao-class metaclass has extended defclass with some additional magic.
You should receive a result "CREATE TABLE ringy_dingy (area_code TEXT NOT NULL, location TEXT NOT NULL)". Postmodern automatically examines the class definition to craft the table-creation SQL!
(postmodern:query (postmodern:dao-table-definition 'ringy-dingy))
In your favorite PostgreSQL admin tool, examine the list of tables. You will find that the ringy_dingy table has been created.
By no means is this all that Postmodern provides. However, this alone should serve as an example of the power and flexibility that metaprogramming can provide.
We have only just begun.