4

I'm writing my own LISP based on Write Yourself a Scheme in 48 hours. (The code is here.) As a last exercise I want to implement macros. How can this be done considering that I represent expressions as a list of immutable datatypes. Can this be done simply in LISP itself or do I have to implement some function in Haskell?

My current implementation is written in Haskell and pretty much works like this:

  • Parse input and turn it into a list of expressions
  • Evaluates the expressions and substitute it tills it's a single expression
  • Return that expression and print it

The expressions is represented in Haskell like this:

data Expr
  = Sym String
  | List [Expr]
  | Num Int
  | Str String
  | Bool Bool
  | Func Env [String] Expr
  | Prim ([Expr] -> ErrorOr Expr)
  | Action ([Expr] -> IOErrorOr Expr)

Okey, now to the real problem. Macros doesn't evaluates its arguments but instead transforms into an expression by "placing" the arguments inside the form. Returning valid expression that may be evaluated or returned as a quoted list. I was thinking of implementing this by having a special evaluation function which only evaluates the symbols in the macros' form. How this could be achieved though is something I have problem understanding. The proper solution feels like I should "simply" modify the form by replacing the symbols inside it with the arguments, but that's not possible due to Haskell's immutability.

So, Clojure seems to have implemented macros in Lisp itself though. I can't interpret Clojure's solution but if this can be done it feels like being a bit easier than doing it in Haskell. I have no idea what macroexpand1(which macroexpand call) do, does it call some function from within Clojure's implementation? If so, then I still have to implement it inside Haskell.

If we look at how functions are evaluated:

eval env (List (op:args)) = do
  func <- eval env op
  args <- mapM (eval env) args
  apply func args

apply :: Expr -> [Expr] -> IOErrorOr Expr
apply (Prim func) args = liftToIO $ func args
apply (Action func) args = func args
apply (Func env params form) args =
  case length params == length args of
    True -> (liftIO $ bind env $ zip params args)
        >>= flip eval form
    False -> throwError . NumArgs . toInteger $ length params
apply _ _ = error "apply"

So, if I wanna implement a macro system, then I could probably remove the evaluation part of the arguments, then bind the macros parameters to its arguments, and have a special eval which only evaluates every symbol in the form, returning a new form that has the arguments put inside it instead. This is what I can't implement though, and I'm not even sure the logic is correct.

I understand this question is quite wide and could probably be more simply asked with "How do I implement a macro system in my LISP implementation written in Haskell

klrr
  • 113
  • 1
  • 7
  • you could implement auto currying, or varargs (handle arglists with *dot* in them) on params/args length mismatch (much more fun than just erroring out). :) – Will Ness May 11 '13 at 18:10

3 Answers3

3

You might try reading the interpreter implementations in Structure and Implementation of Computer Programs. The eval function that you show clearly only works for the default evaluation rule, and not for what the book calls special forms.

A normal Lisp eval function looks more like this:

eval env expr@(List _)
    | isSpecialForm env expr = evalSpecial env expr
    | otherwise = evalApplication env expr

evalApplication env (op:args) = do
  func <- eval env op
  args <- mapM (eval env) args
  apply func args

evalSpecial env expr@(List (op:args))
    | isMacro env op = eval env (macroExpand env expr)
    | otherwise = case op of
                    "lambda" -> ...
                    "if" -> ...
                    -- etc.
Luis Casillas
  • 29,802
  • 7
  • 49
  • 102
2

No, macros can not be implemented in Lisp itself, that's the whole point to them. You have to macroexpand each macro call according to its definition, as part of loading/compiling/processing a given expression.

You would have to alter your eval implementation to call macros on unevaluated arguments, and feed the results back into eval (not apply, like processing the normal function application would). As suggested by sepp2k in the comments, you'd represent your macros as Func... expressions, but hold them in a separate environment, where only macros are stored.

see also: Lazy Evaluation vs Macros

Community
  • 1
  • 1
Will Ness
  • 70,110
  • 9
  • 98
  • 181
  • How are macros a valid type of expression? Are you thinking of something like a `macro-lambda`? That's not how Lisps handle macros (that is a macro is not something that you can pass around or store in a variable). Or are you saying macro *calls* are an expression? That's true, but in his `Expr` type macro calls would already be represented as `List`s whose first element is a `Sym` containing the name of a macro. – sepp2k May 11 '13 at 18:19
  • @sepp2k doesn't his Lisp have to recognize `(defmacro ...)` forms, to have macros? Right now it surely recognizes `(lambda ...)` and translates them into `Func ...` expressions. To which `Expr` would he translate `(defmacro ...)` forms? I gather, to something distinct. – Will Ness May 11 '13 at 18:23
  • @sepp2k the other way of course is to push the whole of macro handling into the implementation. Reading a `defmacro` form would alter some run-time innards, calling `macroexpand-1` would access those internals, etc. Yes, that's also possible. Still it would lend itself to being implemented "in Lisp itself" even less. – Will Ness May 11 '13 at 18:30
  • It should be possible to just implement `defmacro` as an `Action`. I imagine that's how `define`, for example` is already implemented. The reason that `lambda` can't be implemented as an `Action` is that it needs to close over the environment, but `defmacro` doesn't have the same problem. – sepp2k May 11 '13 at 18:30
  • Actually I just looked at the github repository and the OP implements `def` and `defn` by simply handling `List [Sym "def", ...]` directly in the `eval` function. So I imagine he'd use the same approach for `defmacro`. – sepp2k May 11 '13 at 18:40
  • @sepp2k right, so they'd have something like `eval env (List [Sym "defmacro", Sym var, List params, List form]) = define env var $ Macro ....` and then `eval env (List (op:args)) = do { func <- eval env op ; case func of Func ... -> ... ; Macro ... -> .... }`. – Will Ness May 11 '13 at 19:03
  • But if you represent it like that, you'll be able to pass macros as arguments to functions or otherwise use them as part of other expressions. As I said, that would not be consistent with how macros in other Lisps work. What I'd do would be to introduce a second global environment that only contains macro names. In that environment macros could simply be represented as `Funcs` with an empty environment. By only looking into the second environment when a symbol appears as the head of the list, macro names could then only be used to call macros. – sepp2k May 11 '13 at 19:10
  • @sepp2k we could specifically forbid such things, but ISWYM, separate env for macros is much better. – Will Ness May 11 '13 at 19:30
  • @sepp2k and thank you for the discussion and clarifications. :) – Will Ness May 12 '13 at 05:48
  • A given Lisp dialect's macros can absolutely be implemented in that dialect itself. However, that introduces a boostrapping problem, that's all. The macros which the implementation relies on will have to be expanded somehow, and that will require an executable version of those macros. **This is just a subset/subtask of the compiler boostrapping problem.** Since macros are little extensions to the compiler, their boostrapping is part and parcel. – Kaz Jan 01 '20 at 06:00
  • @Kaz if you load those start-up macros through special treatment by your compiler then by definition they are not implemented in Lisp *itself*. the special processing by the compiler is part and parcel of macro mechanism in such implementation. if it's an interpreter, it must have special handling of macro definitions/invocations in its eval/apply routines. if you only have a Lisp without macros, you can't add macros into such Lisp via that Lisp's code only, is what my answer is saying. your comment seems to concur. – Will Ness Jan 07 '20 at 09:59
  • @WillNess A Lisp dialect's macros can be implemented in that dialect itself without special treatment by boostrapping. An executable version of that dialect already comes with compiled versions of those macros. That executable build can process the source code of those same macro definitions (including compiling them). Nothing in the implementation has to specially recognize those macros and give them any different treatment. The special handling is in how we originally bootstrap the implementation; e.g. using another Lisp implementation or whatever. – Kaz Jan 08 '20 at 01:13
  • @Kaz we're talking about two different things it seems. I'm talking about adding macros support into a Lisp which has no support of macros in it from the start, as is the question (as I read it). – Will Ness Jan 08 '20 at 07:04
  • @WillNess Well, that is pretty much every Lisp at the start of the project! Once we have enough symboilc processing in an interpreted Lisp without macros, we can use that Lisp to write a compiler. Initially, that Lisp can just be the same one that is interpreted. But we can then extend the compiled version of the language with macros. That version will no longer execute on the intepreter; it must be processed by the compiler. Then we can turn around and make the compiled macro expander available to the interpreter too! – Kaz Jan 08 '20 at 07:32
1

You don't need a special version of apply. Just call the regular apply without evaluating the arguments and then eval the expression returned by apply.

sepp2k
  • 363,768
  • 54
  • 674
  • 675