Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I also don't see a nice way to implement something like with-slots as a fexpr. This relies on symbol-macrolet.

  (with-slots (a b c) obj
    ;; a b c refer to slots (assignable!)
    )
This has to evaluate obj to some hidden variable #:obj, and then replaces all occurrences of a, b, c with (slot-value #:obj 'a), etc.

I don't see how the fexpr can do that without performing the same replacement.

a, b and c are not lexical variables you can just bind and then evaluate the body. They are aliases: assigning to a sets slot a of the object.

One way is for the fexpr to contain its own eval implementation which takes an extra macro environment, indicating that when a, b and c are seen, alternative forms are to be evaluated in their place.

That just adds up to an inefficient implementation of symbol macros.

Also, how are places implemented. In particular, user-defined places. How can we have a set of fexprs like setf, push, incf, ... which can work with with arbitrary syntax denoting a place, including application-defined places.



Overall I agree: yes, with fexprs you lose some code introspection ability compared to macros. I haven't found it to be a big deal in my fexpr-based hobby Lisp so far.

Re your two points:

You could have "symbol fexprs", analogous to symbol macros, I guess.

For places I think the first-class solution as employed by T and others is better, and would work fine with fexprs: (set (name-of person-1) "sam") simply stands for ((setter name-of) person-1 "sam").

IOW, name-of is expected to be a reader function. Every reader function has a writer function attached to it, that we extract with (setter name-of). Then we call that writer function with the rest of the original arguments.



That's a fun one. symbol-macros don't have an equivalent in lisp functions as far as I'm aware - `(symbol-lambda-let (x (lambda () (list foo x)) (list z z))` ~= `(list (foo z) (foo z))` isn't a thing.

Speculate that such a thing did exist. Call it symbol-lambda for less confusion with the symbol-function that does exist in common lisp. It's a function that takes no arguments, being a distinct premise from a function that takes the empty list. `(apply func some-list)` would be an error. But one wishes to call it implicitly when it shows up in lists. Related to and distinct from thunks.

Thus it's a function executed by eval, not by apply. Specifically if eval calls into eval-symbol when called on symbols, one way to go is perform the environment lookup etc as usual, and then afterwards check whether the thing one found in the environment is a symbol-lambda. Something like:

    (define eval-symbol-lambda
      (lambda (evalled-symbol env)
         (if (symbol-lambda? evalled-symbol)
             (eval (open-symbol-lambda evalled-symbol) env)
             evalled-symbol)))

    (define eval-symbol
      (lambda (symbol env)
        (eval-symbol-lambda (lookup env symbol 'unknown) env)))
It doesn't take arguments, the same way symbol-macros don't take arguments.

The fexpr equivalent could be identical (since it's mostly distinguished from lambda by not evaluating arguments and there aren't any), or maybe the fexpr form gets passed the environment and the lambda form gets passed the empty environment instead on efficiency grounds.

With-slots then turns into something like:

    (let (#gensym obj)
      (symbol-lambda-let
        (a (slot-value #gensym 'a))
        (b (slot-value #gensym 'b))
        (c (slot-value #gensym 'c))
        ...))
I'm not totally convinced. Its straightforward to add to an fexpr-based evaluator. I think the proper model is that it's a function with a different calling convention to lambda and to fexpr. In the same way that eval-on-list tends to end up in a tail call to apply, eval-on-symbol can end up in a tail call to symbol-apply which usually returns the argument unchanged.

However passing one of these things directly to eval, as opposed to passing a symbol bound to one to eval, is also possible. So eval picks up a third case for eval-on-function, where fexpr and lambda get returned unchanged, but symbol-lambda/fexpr/name-tbd get evaluated.

Also the evaluation of the symbol-lambda might need to know what name it was invoked through. That usually isn't the case and interacts poorly with renaming variables. That also distinguishes passing it to eval as a function from calling it by writing down a symbol that resolves to one.

Overall this leaves me with a sense that I don't quite have the proper decomposition worked through yet.


Your thinking almost exactly parallels mine about this topic. I formed the same idea about lambdas that take only an environment being bound to symbols and whatnot.

I got to something resembling this part:

> With-slots then turns into something like:

That's when I hit the problem that "turns into" is macro code generation!

If the fexpr has to do code generation on the fly and eval it, it's doing a macro's job, badly, since the macro can do it upfront.




Consider applying for YC's Summer 2026 batch! Applications are open till May 4

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: