10 ms·
Generalized Macros
- remexre 3y agoI wonder if this might be made even more powerful by using a zippers instead of a single left-right pair; then, the generalized-macro could traverse the entire top-level form?
- ianthehenry 3y agoI think that a zipper would provide a much nicer interface for making "distant" transformations, but just to be clear you can traverse the entire AST with this approach by recursively returning macros that return macros and then re-assembling the entire tree once you get where you're going. Just, ah, not the easiest code to write :)
- nickdrozd 3y agoThis post discusses a variety of Lisp macro that doesn't merely expand into something else, but actually reaches out to rewrite its surrounding context. > So people have spent a lot of time thinking about ways to write macros more safely – sometimes at the cost of expressiveness or simplicity – and almost all recent languages use some sort of hygienic macro system that defaults to doing the right thing. > But as far as I know, no one has approached macro systems from the other direction. No one looked at Common Lisp’s macros and said “What if these macros aren’t dangerous enough? What if we could make them even harder to write correctly, in order to marginally increase their power and expressiveness?” The first example discussed is a defer macro that can be invoked with "no indentation increase and no extra nested parentheses". As a macro-lover and an indentation-hater, I think this is a brilliant and hilarious idea.
- saurik 3y agoI implemented something such a thing for Clojure a while back using reader macros (which Clojure wants to not support but thankfully you can hack them in).
- kerkeslager 3y agoThis post persuaded me to not take much of a look at Janet. This is also related to why I think that Lisps haven't become the most widely-used programming languages. Lisp enthusiasts like to point out the power of macros, and macros are the raison d'être for Lisp's homogeneous s-expression syntax. Most other features in Lisp (such as first-class closures and higher-order functions) can exist without s-expressions, but the powerful thing about s-expressions is that they enable Lisp macros. But with great power comes great responsibility. When I'm writing a program I want as little responsibility as possible while still being able to solve the problem at hand. I don't want to be responsible for memory management and bounds checking, and I don't want to be responsible for the hygiene of my macros at both the definition and the call site. With C, the responsibility of memory management and bounds checking comes with a power that people actually need to solve problems. For me these problems usually come up in the context of writing my own hobbyist interpreters/compilers, but there are a lot of real world cases where these come up. But often you don't need the capabilities of C, and I'd argue as a result that there are a lot of cases where using C is a bad idea because it's not the best way to solve your problem. And here's the hot take: the power of Lisp macros isn't actually ever worth the responsibility in my experience. The problem Lisp macros solve is "this code is more verbose/ugly/boilerplate-y/etc. than I want it to be", which just isn't the problem you're writing the program to solve. Whenever you reach for a macro, there's another tool you could be reaching for to solve the actual problem at hand. At the very least, you can just write the code the macro would expand into. There's inherently never a case where the macro is the only way to solve the problem. If you're good at writing macros, you won't always get burned by them, but nobody is ever perfect at writing macros, so everyone gets burned sometimes. If you're writing software that you actually need to work, the risk is rarely worth it. When I have written production code in a Lisp (mostly Clojure) I've rarely reached for macros, and often bugfixes have been removing a macro that was of the "so preoccupied with whether or not they could, that they didn't stop to think if they should" variety. And if you spend enough time avoiding and removing macros, you start to wonder why you're destroying your eyesight trying to match parentheses, when the entire reason for the parentheses is to enable something you have to avoid and remove. And don't get me wrong: macros are cool. "Because I like them" is a totally valid reason to write macros and Lisp.
- 3y ago
- munificent 3y agoReally interesting article. Maybe not a good idea as a language feature, but certainly an interesting one. This is my new favorite typo: (defmacaron . lefts [key & rights] ~(,;(drop-last lefts) (get ,(last lefts) ,(keyword key)) ,;rights)) We're working on a macro proposal for Dart and I wonder if users would like them more if we called them "macarons".
- Graziano_M 3y agoI noticed that as well, but it's not a typo: https://github.com/ianthehenry/macaroni/blob/master/src/init.janet#L20 https://github.com/ianthehenry/macaroni/blob/master/src/init...
- ianthehenry 3y agoOh whoops! That’s what I called them in my prototype so I didn’t shadow Janet’s built-ins. Forgot to update that one when I copied it to the blog post :)
- gumby 3y agoI am diabetic so no, I could not use this feature.
- foodoos 3y agoThere's a stronger abstraction than "generalized macros" and the author of this article missed it.
- invalidOrTaken 3y agocare to enlighten us?
- blatant303 3y agoenlighten us please. I'm really curious.
- sparkie 3y agoInteresting idea. I'll play around with when I get home. I use Kernel a lot, which allows you to write first-class operatives which can influence the bindings of their caller, but they don't allow modifying the body of the calling function.
- JonChesterfield 3y agoCan you recommend an implementation for experimenting with Kernel? I'm unsure about the implications of the wrap/unwrap primitives and how they interact with apply, so more interested in a correct/reference implementation than a high performance one.
- sparkie 3y agoklisp is the most complete implementation I've used. Website is now down and original bitbucket host too, but mirror here: https://github.com/dbohdan/klisp https://github.com/dbohdan/klisp For slightly better performance, there's bronze-age-lisp, which uses klisp and x86 assembly. Mirror: https://github.com/ghosthamlet/bronze-age-lisp https://github.com/ghosthamlet/bronze-age-lisp Performance will never be great due to the nature of the language, which is incompatible with usual forms of compilation. I'll try to give a brief explanation of wrap and unwrap. A combination is of the form `(combiner combiniends)`, where combiner must be either an operative or applicative. In the case that it is operative, the combiniends are passed verbatim to the operative, without being reduced. If the combiner is applicative, the combiniends are reduced, by evaluating each item in the list using the metacircular evaluator, until a list of arguments is returned. The arguments are then passed to the underlying combiner of the applicative. Operatives are constructed using `$vau`, and applicatives by using `wrap` on another combiner. Usually the underlying combiner is operative, but the language used in the report is clearly permitting you to wrap other applicatives too, so in the case that you evaluate an applicative whose underlying combiner is applicative, and the underlying combiner of that is operative, then the list of combiniends would be reduced twice before being passed to the final operative. I've honestly not encountered a single use-case for this in my time using Kernel, but who knows. It might just be easier to consider that `wrap` wraps operatives into applicatives. The description of the evaluator from section 3 of the report is a pretty clear explanation of what happens. * If the expression to be evaluated is a pair, then: * The car of the pair must be a combiner * If the combiner is operative, call the operative with the cdr of the pair * If the combiner is applicative, evaluate cdr of the pair to produce an argument list 'd. eval the cons of the the underlying combiner of the applicative with 'd. ($define! eval ($lambda (o e) ($if (not (environment? e)) (exit)) ($if (pair? o) ($let ((c (car o))) ($if (operative? c) (call c (cdr o) e) ($if (applicative? c) (eval (cons (unwrap c) (eval-list (cdr o) e)) e) (error "not a combiner in combiner position")))) o))) ($define! eval-list ($lambda (l e) ($if (null? l) () ($if (pair? l) (cons (eval (car l) e) (eval-list (cdr l) e) (error "operand to applicative must be a list"))))))
- abecedarius 3y agoRe prior art: I'm at least vaguely reminded of "expansion-passing style" by Dybvig, Friedman, and Haynes. https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.64.1086&rep=rep1&type=pdf https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.64... (Just from a quick skim of this long post.)
- mncharity 3y agoRiffing on related work, Macros for DSLs[1] notes non-locality (and there's racket's dsl emphasis[2] in general). Apropos composition, I liked the PADL'23 Modern Macros video[3] (seemingly twice submitted to hn without traction). [1] Macros for Domain-Specific Languages https://par.nsf.gov/servlets/purl/10220787 https://par.nsf.gov/servlets/purl/10220787 https://docs.racket-lang.org/ee-lib/index.html https://docs.racket-lang.org/ee-lib/index.html [2] From Macros to DSLs: The Evolution of Racket https://drops.dagstuhl.de/opus/volltexte/2019/10548/pdf/LIPIcs-SNAPL-2019-5.pdf https://drops.dagstuhl.de/opus/volltexte/2019/10548/pdf/LIPI... [3] PADL'23 Modern Macros https://www.youtube.com/watch?v=YMUCpx6vhZM https://www.youtube.com/watch?v=YMUCpx6vhZM
- hzhou321 3y agoLISP has been singing homoiconicity as its feature. I lately start to think homoiconicity is really a wart. The macros are a system to program the code, while the code is a system to program the application. They are two different cognition tasks and making them nearly indistinguishable is not ideal. LISP has a full-featured macro system, thus hands down beats many languages that only possess handicapped macro system or no macro system at all. It uses the same/similar language to achieve it is mere accidental. In fact, I think LISP is an under-powered programming language due to its crudeness. But it's unconstrained macro system allows it compensate the programming part to certain degree. As a result, it is not a popular language and it will never be, but it is sufficiently unique and also extremely simple that it will never die. What if, we have a standalone general-purpose macro system that can be used with any programming languages, with two syntax layer that programmers can put on different hat to work on either? That's essentially how I designed MyDef. MyDef supports two forms of macros. Inline macros are using `$(name:param)` syntax. Block macros are supported using `$call blockmacroname, params`. Both are syntactically simple to grasp and distinct from hosting languages that programmers can put on different hats to comprehend. The basic macros are just text substitution, but both inline macros and block macros can be extended with (currently my choice) Perl to achieve unconstrained goals. The extension layer can access the context before or after, can set up context for code within or outside, thus achieve what lisp can but using Perl. We can extend the macros using Python or any other language as well, but it is a matter of the extent to access the macro system internals. Inline macros are scoped, and block macros can define context. These are the two features that I find missing in most macros systems that I can't live without today. Here is an example: $(set:A=global scope) &call open_context print $(A) print $(A) subcode: open_context set-up-context $(set:A=inside context) BLOCK # placeholder for user code destroy-context
- packetlost 3y ago> I lately start to think homoiconicity is really a wart. I sorta agree. The simplicity of the syntax and representation makes it particularly amenable to dynamic modification above basically every other language though. There's a bunch of other properties that make this to case though, such as a very dynamic type system, first-class functions, extremely simple syntax, etc. so it's really a combination of a bunch of factors. That being said, I think homoiconicity is actually a useful feature, but runtime macro expansion is the dangerous part. What I'd really like to see is a Lisp with very well defined execution orders. That is, macros must be expanded at compile time, and with clear symbols for defining what runs and when. I'm not talking about something like `(macroexpand ...)`, more like `(comptime (my-macro ...))`... `(define (my-func) (my-macro+ 'a 'b 'c))` where it's explicit that a macro executes at compile time, and usage of that macro must be denoted with a special character (`+`, in my example). I think a general purpose macro language is only as useful as the average code-gen/templating language. It's just string manipulation, which can be harder to reason about than true metaprogramming and might result in some ugly/verbose output and having to context-switch between 2 entirely different languages often. What's particularly powerful about Lisp macros is that usage of them doesn't look any different than a normal application usually, and writing a macro is only marginally different than writing a normal function.
- openasocket 3y agoMacros tickle a certain peculiar part of my brain. I have this idea of making a lisp that at its core is just some static single assignment language, or some other type of IR. And then all the traditional control flow features would be implemented using macros. You could even implement optimization passes as macros. I haven’t been brave enough to actually try it, but I imagine if I did I would need some more powerful macro features like this.
- cellularmitosis 3y agoIs this the idea behind shen and k lambda?
- kazinator 3y agoRegular macros can give you this in a disciplined way: You make a macro called crazy-macrolet which is used like this: (crazy-macrolet ((crazy-macro (left-forms right-forms arg ...) ....) (other-crazy-macro (...))) body) Inside body, you use the local crazy macros. A little language made of these crazy macros can be wrapped up in a big macro. (defmacro bobs-crazy-dsl (&rest forms) `(crazy-macrolet ((bobs-crazy-macro ...)) ,@forms)) Then, you can only use bobs-crazy-macro in code wrapped in (bobs-crazy-dsl ...). crazy-macrolet needs to implement a code walker to in order to expand those macros. Macros can be context-dependent without having access to the literal forms to the left or right (or elsewhere). In TXR Lisp, I implemented tagbody as a macro, providing a measure of CL compatibility. The go operators are local macros. They do not expand in a context-free way; they communicate with the surrounding tagbody. So for instance if go is asked to jump to a nonexistent label, it errors: 1> (expand '(tagbody (go a) b)) ** go: no a label visible 1> (expand '(tagbody (go a) a)) (let ((#:tb-id-0019 (gensym "tb-dyn-id-")) (#:next-0020 0)) (sys:for-op () (#:next-0020) ((sys:setq #:next-0020 (block* #:tb-id-0019 (sys:switch #:next-0020 #(((return* #:tb-id-0019 1)) ())) ()))))) So obviously, (go a) is behaving differently based on whether it can "see" that there is an a in its context, on the left or right side.