Closed dumblob closed 2 years ago
Forms (specifically, their ability to quote arguments implicitly, akin to fexprs) cover a lot of what traditional languages call macros, but not everything.
Let's define "code" in a little bit of a different sense than homoiconic languages usually do. It's generally true that with homoiconicity, "code is data". But even here, there's usually a rigid wall between what is considered "code" by default, and what is considered "data". If we plug (+ 1 2)
into a Scheme interpreter, that (+ 1 2)
is code. We can get its data representation by quoting it - (quote (+ 1 2))
returns the list (+ 1 2)
as data. But that requires changing the code! Our program now has this quote
expression in it that it didn't have before. Code and data are different things, even when we can represent either as either.
From this, I think we can define four quadrants:
Takes value as input | Takes code as input | |
---|---|---|
Returns value | Traditional functions - factorial , + , etc. |
Fexprs, quoting forms - sufficient to implement things like if or quote |
Returns code | ??? | ??? |
The bottom two boxes are a bit more niche, but are represented in the macro systems of several well-known languages:
#include
. If we were to type this construct, we'd probably say that it takes a string, and expands to some kind of code. Expanding here is different from returning a value!
define-syntax
. Even in homoiconic languages, we need a distinct feature to implement these transformations seamlessly, such that one piece of code expands into another.So, this is the type of functionality macros are intended to provide - expressions that expand into other expressions. The way they actually work in Basil is actually pretty straightforward: macro values are essentially just functions, with the added attribute that they are macros. They are required to return lists, which are spliced into their enclosing s-expression (akin to Scheme's unquote-splicing
) upon evaluation, instead of returning a value. The specific way this is implemented depends on a new built-in function, splice
, that behaves similarly to Scheme's quasiquote
(it's inserted implicitly around macro invocations, however, to simplify code).
With all this, we can implement all kinds of things that we couldn't otherwise:
The identity macro in Basil behaves like unquote-splicing
, as above, and behaves a bit like splats in other languages:
macro unquote-splicing list? = list
println (unquote-splicing [1 2 3]) # expands to (println 1 2 3)
We can implement unquote
just by wrapping our return value in a list:
macro unquote expr? = [expr]
println (unquote (quote 1 + 2)) # expands to (println 3)
def let :name? be :value? = def name = value # doesn't work! we'll always define the variable "name"
macro let :name? be :value? = [:def name := value] # with a macro, we can construct the expression properly
let x be 1
println x # prints 1
Note how in the last example we use quoted parameters in a macro - these aren't orthogonal to forms, they're a separate feature that synergizes with them very closely! So all told, I think these will drastically increase Basil's expressive power, while leveraging constructs that have mostly already been implemented - forms, functions, and eval.
Recent commits added "macros". Naturally the question arises why Basil needs macros if it already has forms (which is a more generic concept capable of everything macros can and much more).
@elucent could you elaborate?
Note I didn't do any deep investigation on macros nor forms in Basil as it's all still very much in flux.