For a long time I felt fuzzy on the meaning of a meta programming language. The metaprogramming paradigm sounded mythical. Esoteric. One most commonly associates the Lisp family of languages with metaprogramming. In fact, Lisp is referred to as a meta language.
It didn’t help that Lisp also bears a certain cult status. Let’s face it, comparatively few folks use some dialect of Lisp nowadays. Then again, a subculture of programmers use nothing but Lisp.
I’ve experimented with Lisp from time to time. First I plunged into Common Lisp, one of the most powerhouse and ‘industrial’ dialects. It didn’t leave me thrilled. I found the syntax too convoluted, structures too verbose.
I later began to explore PicoLisp, something far more minimalist (and singular among the family). Contrary to Common Lisp, both an interpreted and compilable dialect, PicoLisp is entirely interpreted. But it aims not to sacrifice too much performance in light of a far simplified architecture. Now this really appealed to me. I found the syntax more contained, structures more compact, programs prettier to read.
To step back, let me thus describe metaprogramming. Metaprogramming is a property that enables a language to be dynamically hackable in some way. It may be the flexibility to dynamically construct and execute code during the language runtime. Or modify its own code at runtime. Or tinker with the execution stack. Or fundamentally modify the language internal structures and redefine the syntax. Or employ a macro language (within the core language) to achieve further abstractions in the course of incremental compilation.
Lisp dialects enable one or more of the above metaprogramming strategies in a typically accessible and encouraged manner. Metaprogramming follows as a natural design choice in Lisp programming. It doesn’t carry the perception of a cryptic or an expert feature as may appear in other languages [strictly the author’s opinion].
Generally, any interpreted language enables metaprogramming to some degree. An interpreted language, I remind you, is one whose code is analyzed and executed by an interpreter or virtual machine at runtime, not requiring compilation into assembly, or transformation into something significantly lower level. A small handful of interpreted languages includes Lisp, Perl, Python, PHP, Ruby, Smalltalk, JavaScript, POSIX Shell. Some languages have the ability to run bi-modally (ex: Common Lisp, Java).
For my purposes, as long as the interpreted language allows the dynamic execution of that language code passed as data, it’s as ‘meta’ as I require. Perl, a “Gung ho” language that I loved, abandoned, and came to love again, enables eval constructs as (one of a few) means to do just that. (Perl is notorious for enabling multiple, often esoteric syntax for just about any operation.) POSIX Shell enables the eval statement in the same way. The Make build system too provides its own language syntax and dynamic evaluations. And others. There is no reason but artificial for an interpreted language not to facilitate dynamic code execution.
Hence we have this interpreted language property of treating a piece of data as code to execute at any point during runtime. Lisp, however, incorporates this paradigm beautifully into its very fabric. “Code = Data” is the Lisp motto. Data = Code.
Lisp (“List processor”) famously uses the parenthesized (s-expression) syntax. For the slightly simplified purposes of discussion, every expression is a parenthesized list. Functions, classes, loops, conditionals all take shape of parenthesized expressions. The list typically comprises of the function to execute in the first element, and the parameters in the remainder. Each parameter holds, in turn, another s-expression or a basic data element (integer, character, string). Sheer syntactical beauty, insofar as the original principles circa 1950s.
One of the ways in which LISP dialects can differ lies in their representation of those s-expressions (lists) that are entirely data (first element included), and those that represent immediately executable functions. And this need not necessarily be implicit. Sometimes we may wish to pass what looks like a function name (ie arithmetic operator) purely as data. And sometimes what resembles pure data garble may actually be a symbol to a function.
LISP dialects enable further syntactic whimsicalities to indicate the desired treatment of code/data where the implied behavior differs. This is where I find Common Lisp less desirable, sometimes even painful to read. Common LISP provides special macro syntax to differentiate compile-time code from run-time. CL also enables constructs that hardly resemble a simple list. There exists much polarity on the matter.
PicoLisp, on the other hand, takes liberties to interplay code and data in a more compact and seamless way, sacrificing a measure of runtime checking in principle of “know what you are doing”. Being entirely interpreted, it eliminates the need for special macro syntax, which I found of tremendous relief after countless dazed wonders through the labyrinths of Common Lisp. PicoLisp also allows the programmer to redefine virtually any symbol or inner behavior, whereas other dialects (CL included) might not only impose restrictions but conduct resource-consuming runtime checks. In general, I see PicoLisp as respecting more of the original traditions.
Does a language need to allow dynamic code evaluations, incremental compilation, or be self-hackable in order to solve certain problems? By no means. Any Turing-Complete language can represent and address any algorithmically conceivable task. But ease of representation make some languages more suitable for the problem, and some constructs more convenient. Some abstractions are more pleasant (and fun) to work with. Like spoken language, we desire a combination of richness, pragmatism, and dynamics in the form of expression. It helps to occasionally be able to look inward.
Questions, comments? Connect.