Meta-programming = the broad idea of “programs that manipulate or generate programs”. It can happen at runtime (reflection) or compile-time (macros).
Macros = one specific style of meta-programming, usually tied to transforming syntax at compile time (in a pre-processor or AST-transformer). It takes a piece of code as input and replaces it with another piece of code as output, often based on patterns or parameters. Macros consist of:
- Rule‑based transformation: A macro is specified as a pattern (e.g., a template, an AST pattern, or token pattern) plus a replacement that is generated when that pattern is matched.
- Expansion, not function call: Macro use is not a runtime call; the macro is expanded before execution, so the final code is the result of replacing the macro invocation with its generated code.
Here are some programming languages and their meta-programming and macro capabilities.
NB! Take it all with a grain of salt. The result comes from working through a few drafts with perplexity.ai, and I have not had a chance to personally verify all of the cells. They do look generally correct to me overall, though. Corrections are welcome!
Metaprogramming + macro features
| Total | Language | Description |
|---|---|---|
| 19 | (Typed) Racket | Lisp dialect built explicitly as a macro‑centric, multi‑language programming system for creating languages and DSL's. (Typed Racket is the gradually typed version on top.) |
| 18 | Hackett | "Haskell + Racket". Haskell‑style pure and lazy functional lisp on Racket: strong static (not gradual) types with lisp‑style hygienic macros that are "type‑aware". |
| 17 | Common Lisp (CL) | Mature, industrial‑strength Lisp with massive macro ecosystem and runtime dynamism. |
| 16 | Arc | Minimalist Lisp designed for extremely concise macro‑heavy DSLs and exploratory hacking. |
| 16 | Elixir | Erlang‑VM language designed for DSLs, with powerful compile‑time macros and OTP‑centric platform macros. Macros inspired by Lisp/Clojure, but syntax inspired by Ruby. Semantics like Erlang, and macros influenced by Clojure. |
| 14 | Clojure | JVM Lisp that leans on JVM interop and defmacro‑style DSLs, with a strong but not Lisp‑style macro‑tooling culture. |
| 14 | Julia | High‑performance scientific computing language with its own scripting‑like syntax, Lisp‑style AST macros, and generated code that feels like inline DSLs. (Julia's parser is written in FemtoLisp, a dialect of Scheme.) |
| 13 | Coalton | "Haskell + CL". Haskell‑style statically‑typed functional language that integrates directly as a macro library into Common Lisp, with Lisp‑style macros and a focus on typed DSLs. Coalton is eager/strict and impure (like OCaml). |
| 12 | Nim | Statically‑typed systems language with Lisp‑style AST macros and compile‑time DSLs. |
| 12 | Rust | Systems language with procedural macros, compile‑time DSLs, and macro‑of‑macro‑style crate‑level macro‑tooling. |
| 12 | Scala | JVM language with powerful metaprogramming through macros, implicits, and DSL‑style APIs. |
| 12 | Scheme (R7RS‑small) | Minimal Lisp standard with full macro power, but small ecosystem of DSLs and macro tools. |
| 10 | Carp | Small memory-safe (borrow‑checked) and statically typed Lisp‑style language for game‑oriented code, with lightweight macros and DSL‑oriented design. |
| 9 | Jai | Game‑oriented language built on compile‑time code and #expand‑style macro‑like DSLs. |
| 8 | Ruby | Object‑oriented scripting language with open classes, runtime metaprogramming, and DSL‑heavy idioms. Ruby's design was heavily influenced by Lisp, and has a simple Lisp language at it's core (though surface syntax is different). |
| 7 | C++ | Systems language with templates and macros as semi‑metaprogramming, but not DSL‑centric by design. |
| 6 | Zig | Systems‑language with compile‑time code generation and macro‑style DSLs, but minimal macro ecosystem. |
| 4 | Python | Dynamic scripting language with runtime metaprogramming and decorators, but no AST‑level macro system. |
Scores range 0–19: 7 (metaprogramming) + 3 (compile‑time) + 8 (macro features) + 1 (ecosystem‑macro‑platform) = 19 max.
Each cell is either ✅ (yes) or – (no / limited).
* - Scheme (R7RS‑small)
| Feature / language | (Typed) Racket | Hackett | Lisp (CL) | Arc | Elixir | Julia | Coalton | Nim | Rust | Clojure | Scala | Scheme* | Carp | Jai | Ruby | C++ | Zig | Python |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Metaprogramming features: | ||||||||||||||||||
| Runtime metaprogramming | ✅ | ✅ | ✅ | ✅ | ✅ | – | – | – | – | ✅ | – | – | – | – | ✅ | – | – | ✅ |
| Runtime reflection / introspection | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | – | – | ✅ | ✅ | ✅ | – | – | ✅ | – | – | ✅ |
Runtime eval / dynamic code loading |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | – | – | ✅ | – | ✅ | – | – | ✅ | – | ✅ | ✅ |
| Build‑ or tooling‑level code generation supported | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Composes elegantly | ✅ | ✅ | ✅ | ✅ | ✅ | – | – | – | – | ✅ | – | – | – | – | ✅ | – | – | – |
Not verbose / concise for common patterns (defmacro etc.) |
✅ | ✅ | ✅ | ✅ | ✅ | – | – | – | – | ✅ | – | – | – | – | ✅ | – | – | – |
| Designed in from the start (core language embraces metaprogramming) | ✅ | ✅ | ✅ | ✅ | ✅ | – | – | – | – | ✅ | – | – | – | ✅ | ✅ | – | ✅ | – |
| Metaprogramming score (out of 7): | 7 | 7 | 7 | 7 | 7 | 2 | 3 | 1 | 1 | 5 | 2 | 3 | 1 | 2 | 7 | 2 | 3 | 4 |
| Compile‑time facilities (not strictly macros): | (Typed) Racket | Hackett | Lisp (CL) | Arc | Elixir | Julia | Coalton | Nim | Rust | Clojure | Scala | Scheme* | Carp | Jai | Ruby | C++ | Zig | Python |
| Run arbitrary code at compile time | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | – | ✅ | ✅ | ✅ | ✅ | – | ✅ (constexpr) |
✅ (comptime) |
– |
| Types as values at compile time | ✅ (in Typed Racket) | ✅ | – | – | – | ✅ | – | ✅ | ✅ | – | ✅ | – | ✅ (via defndynamic and members) |
✅ | – | ✅ (constexpr + templates) |
✅ (comptime) |
– |
| constexpr‑style type‑level / compile‑time computation | ✅ | ✅ | – | – | – | ✅ | – | ✅ | ✅ (const‑eval) |
– | ✅ | – | ✅ | ✅ | – | ✅ (constexpr) |
✅ (comptime) |
– |
| Compile‑time facilities score (out of 3): | 3 | 3 | 2 | 2 | 1 | 3 | 3 | 3 | 3 | 0 | 3 | 1 | 3 | 3 | 0 | 3 | 3 | 0 |
| Macro features: | (Typed) Racket | Hackett | Lisp (CL) | Arc | Elixir | Julia | Coalton | Nim | Rust | Clojure | Scala | Scheme* | Carp | Jai | Ruby | C++ | Zig | Python |
| Hygienic identifier binding | ✅ | ✅ | – | – | ✅ (manual but hygiene‑assisted) | ✅ | ✅ | ✅ | ✅ | ✅ | – | ✅ | ✅ (gensym but manual) | ✅ | – | – | – | – |
| Operate on AST / syntax tree | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | – | – | – | – |
| Pattern‑based transformations | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | – | – | – | – | – |
| Define new syntactic forms | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | – | – | – | – | – |
| Define new keywords / syntax | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | – | – | – | – | – |
| Override core language forms | ✅ | ✅ | ✅ | ✅ | ✅ | – | – | – | – | – | ✅ | ✅ | – | – | – | – | – | – |
| Multi‑phase / macros of macros | ✅ | ✅ | ✅ | ✅ | – | – | – | – | ✅ | – | ✅ | ✅ | – | ✅ (#expand) | – | – | – | – |
| Full‑fledged DSL / language building (via macros) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | – | – | – | – |
| Macro & compile time features score (out of 8) | 8 | 8 | 7 | 7 | 7 | 8 | 7 | 7 | 7 | 6 | 6 | 7 | 6 | 4 | 0 | 3 | 0 | 0 |
| Ecosystem‑wide macro‑platform (large, battle‑tested macro‑tooling) | 1 | 0 | 1 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 1 | 0 | 0 |
| Total score (out of 19) | 19 | 18 | 17 | 16 | 16 | 14 | 13 | 12 | 12 | 12 | 12 | 12 | 10 | 9 | 8 | 7 | 6 | 4 |
| (Typed) Racket | Hackett | Lisp (CL) | Arc | Elixir | Julia | Coalton | Nim | Rust | Clojure | Scala | Scheme* | Carp | Jai | Ruby | C++ | Zig | Python |
The score counts one point per row where the language can reasonably do what the feature describes (DSL‑building is counted as a full feature, even if “limited” in some languages).
The feature score is not an ultimate measure of meta-programming power, since a language (like C++) may have a higher score than another language (like Ruby), but generally be considered less tailored for meta-programming than the other language (Ruby is generally revered for its powerful meta-programming abilities).
In your table’s context, these are nice qualitative notes that explain why Ruby feels more “macro‑style” and DSL‑friendly, even though both sit at the same numeric metaprogramming score.
Macro features are varied and many, and thus in the total score they gain an undue weight, although runtime meta-programming may be just as, or even more, powerful.
Runtime meta-programming can cover a lot of cases, but macros are still useful when you need to change syntax, avoid evaluating arguments too early, or generate code that must be decided before execution.
Macros shine when you need to work with source code itself (treating code as data) rather than values. They can add new syntax, preserve control over evaluation, and generate boilerplate or domain-specific mini-languages without waiting until runtime. Since macros are compile time transformations. In other words, macros can make code more expressive and sometimes safer because mistakes are caught earlier in compilation.
Lisp-style languages make out 8 of the 18 languages in our list: Racket, Hackett, CL, Arc, Clojure, Coalton, Scheme, Carp. Lisp-style languages, with their homoiconic S-expressions’, has a natural ability to treat code as data, which make them ideally suited for meta-programming/macros.
Here are some more awesome-lisp-languages.
For the latest research-languages that could both advance metaprogramming and solve the statics-dynamics biformity that plague statically typed languages, see:
Kernel: uses hygienic runtime fexpr (functions that can take unevaluated expressions/AST) which may replace hygienic comptime macros (like Racket uses, which are restricted to the macro-expansion phase). Introduced
$vau(think of it by the name:interpret-k) which is used to create first-class special forms, letting you as the user of the language define new syntax within the language (e.g.$vauaka.interpret-klets you choose whether to try to eval,evaluate-k, an expression to concrete values, orapply-ka function to the expression(s)), rather than you having to rely on special compiler macros.Kraken: a purely functional statically typed language that can JIT-compile fexpr, which takes Kernel even further.
Seed: a language based on the compiled Chez Scheme R6RS language. But similar to Kraken since it takes inspiration from Kernel and injects a
$vauoperative that takes in an immutable env (caller's dynamic scope, in addition to the function's own lexical scope), for operative-style programming: the ability to create code as fexpr that receives unevaluated argument expressions (like traditional special forms). Although the language is not purely functional (unlike Kraken), since it uses an immutable env, it makes its fexpr AOT compilable (with compiler optimizations: no escape analysis needed). Read more on Lobste.rs. Seed builds atop Chez's R6RS Scheme kernel (~50+ primitives/specials) while injecting $vau as an extension for operative-style programming.
For further reading: https://github.com/oils-for-unix/oils/wiki/Metaprogramming
Top comments (3)
Hackett vs Racket macros
TL;DR:
So where Racket macros are more flexible in raw syntax extension, Hackett macros are “more powerful” in the sense that they let you metaprogram the type system and its vocabulary, not just the syntax tree.
(Typed Racket uses macro expansion to translate its typed surface language into a typed core language, and then typechecks the core language.)
Answer by perplexity.ai :
Hackett’s macros are not “more powerful” in the sense of being a fundamentally different macro system than Racket’s; they’re still built on Racket’s same hygienic macro metaprogramming engine. What makes them seem more powerful is that they are tightly integrated with a full, eager Haskell‑style type system, so macros can both inspect and manipulate type information during expansion. lexi-lambda.github
Integration with type information
In plain Racket, macros operate purely on the syntactic shape of the code; they don’t normally see or act on a type system. In Hackett, the typechecker is essentially part of macroexpansion, so a macro can: lexi-lambda.github
This lets you write “type‑aware” macros that can, for example, generate instance‑deriving boilerplate tailored to the actual type being derived, or adjust generated code based on whether something is
List aversusMaybe a. redditType‑driven language features
Because of this integration, many features that are baked into Haskell (like algebraic datatypes,
case‑style pattern matching, ordo‑notation) are not primitives in Hackett; they are implemented as macro libraries. For instance: lexi-lambda.githubdataandcaseare macros that emit typed AST fragments.This “tower of macros” model means you can redefine or extend how core language constructs behave, but still remain within the typed world, unlike typical Racket macros that are essentially type‑agnostic. github
Expressiveness within a typed λ‑calculus
Racket macros are very powerful for general‑purpose surface‑syntax and DSL creation, but they work in a mostly untyped or gradually typed context. Hackett restricts the base language to a more Haskell‑like, fully typed, curried, lazy λ‑calculus, and then uses macros to layer in richer surface syntax, infix operators, and convenient notations while preserving strong typing. So where Racket macros are more flexible in raw syntax extension, Hackett macros are “more powerful” in the sense that they let you metaprogram the type system and its vocabulary, not just the syntax tree. lexi-lambda.github
Hackett vs TypedRacket
TL;DR: TypedRacket is gradually typed, while Hackett is statically typed.
Answer from perplexity. ai :
Hackett and Typed Racket are both typed languages on top of Racket, but they are aimed at very different styles of programming and type‑discipline. Conceptually, Hackett is “Haskell in Racket’s syntax” while Typed Racket is “Racket with static types bolted on.” lexi-lambda.github
Language philosophy and typing discipline
Type system and inference
dataandcase) are implemented as macros atop a small typed core. lexi-lambda.github(if (number? x) ...)), but its Hindley‑Milner–style inference is weaker, especially for polymorphic functions. You often write explicit type annotations, and the type hierarchy closely mirrors Racket’s numeric and runtime types (Fixnum, Index, Exact‑Rational, etc.). www2.ccs.neuEvaluation order and purity
Syntax and macro story
Practical trade‑offs
In short, Hackett trades Racket’s mutability and gradual‑typing flexibility for a more Haskell‑like, strongly typed, pure, and type‑inference‑oriented experience, while Typed Racket trades Haskell‑style purity and advanced inference for tight integration with Racket’s existing ecosystem and idioms. github
Hackett vs Coalton
TL;DR:
Hacket = Haskell + Racket. Sits on top of Racket.
Coalton = Haskell + Common Lisp (CL). Sits inside CL.
Coalton and Hackett are both statically typed, Haskell‑flavored functional DSLs embedded in Lisp dialects, but they target different hosts and take different paths toward “typed Lisp.” news.ycombinator
Background and host language
Type system and style
Optimization and compilation
Relationship to the host
Summary table
In short: