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

I've read through the article twice, and I still have no idea what the author is getting at.

The author suggests that "the obvious way to implement a DSL as a macro, as we saw with if-match, hard-codes the form of the new syntax class". I disagree. That's not what I'd consider the obvious way at all.

I'd consider the most obvious approach would be to pass the macro onto a polymorphic function of some description:

    (defmulti if-match*
      (fn [pat _ _ _] (if (list? pat) (first pat) (type pat)))

    (defmacro if-match [pat expr then else]
      (if-match* pat expr then else))
Macros have all the same capabilities for extensibility as regular functions. In Clojure at least, macros are just functions with some metadata attached.


That's a very clever use of defmulti that I hadn't considered --- consider that you may know more about writing extensible macros than the average lisper :P. My article was also aimed at being language-agnostic, so a Clojure-specific feature like defmulti wouldn't have been appropriate to introduce. (Although of course CLOS does have multimethods as well, but that's an even more complicated subject!)

However:

1. The code you give still isn't smart enough. It dispatches on the symbol at the head of the list, but that doesn't account for namespacing. So your pattern-macros will all end up in one giant namespace. You could probably invent something clever to account for this but...

2. My overall point[1] was that writing a macro-extensible macro shouldn't require cleverness or new code - it should be in the standard library! Indeed, ideally defining a "pattern-macro" should be accomplished via the same mechanism as defining an "expression-macro"; you shouldn't need separate, custom macro-defining-macros for each syntax class. I'd settle for it just being easy to define an extensible syntax class along with a macro-defining-macro for it, though.

[1] Admittedly, this point could have been far clearer.


You're making a distinction between macros and functions, but the only difference is that functions evaluate their arguments, while macros evaluate their return value.

The idea that there should be "pattern-macros" and "expression-macros" is confusing how macros are used with what macros are.

The namespacing issue can be solved in the usual way; by evaluating the symbol in some fashion. How that's done really depends on what you want the syntax to look like. The simplest mechanism is just to pass the macro on to another macro:

    (defmacro if-match [pat expr then else]
      (if (list? pat)
        (list pat expr then else)
        (if-match-on-type pat expr then else)))


I'm sorry, I don't really understand what you're getting at when you say that I'm confusing "how macros are used" and "what macros are". Perhaps we're just disagreeing about terminology?

What I mean by a "macro" is: a convenient way to extend the syntax of a particular syntax class by telling my language how to translate the syntactic construct I want to add into constructs it already understands. In this sense one definitely can have expression-macros and pattern-macros. Is there some reason you think one shouldn't have this in a language?

Your new proposed `if-match' is strange. I'm not sure how I'd use it to, for example, implement a "list" pattern-macro, such that `(list p1 p2 p3)' matches any list of three elements, whose first element matches p1, second matches p2, and third matches p3. You seem to intend that `((list p1 p2 p3) expr then else)` should macro-expand to the code that performs this pattern match. This is difficult for two reasons:

1. I'd need to shadow the definition of `list' with a macro that usually behaves like the list function, but if used inside of if-match behaves differently. This seems difficult, and is conceptually ugly.

2. AIUI, to get `((list p1 p2 p3) expr then else)' to macro-expand specially, `(list p1 p2 p3)' has to macro-expand to a symbol naming a macro, call it MAGIC, such that `(MAGIC expr then else)` expands to the desired result. This is difficult, because MAGIC needs to know about p1, p2, and p3. So `(list p1 p2 p3)' will need to dynamically define a new macro (with a gensym'ed name) that knows specifically about p1, p2, and p3, and return that. This may be possible, but is gnarly as fuck.

This is certainly very far from being "the obvious way to do it" to anyone but a wizard.

Maybe I'm misunderstanding you, or you made a typo?


My definition of macro would be: a function that's evaluated at compile time.

In this sense, the idea of an "expression-macro" and a "pattern-macro" makes as much sense as an "expression-function" or "pattern-function". We don't typically have different classes of function to fulfil different purposes.

The `if-match` macro you describe could be written as a function, albeit with less pleasing syntax:

    (defn if-match [pat expr then else]
      (if-let [match (if (fn? pat) (pat expr) (isa? pat expr)]
        (then match)
        (else match)))

    (if-match 0 0 (fn [_] true) (fn [_] false))

    (if-match (pattern/cons 0 'x)
              '(0 hi there)
              (fn [{:syms [x]}] x)
              (fn [_] nil))
So in the above case, we allow literal values to be passed in, or a function which returns a map of symbols to matching values. It's the function that allows for the extensibility.

A macro can easily smooth over the janky syntax of the `then` and `else` functions, and we'd end up something like this:

    (if-match (pattern/cons 0 x)
              '(0 hi there)
              x
              nil)
This is essentially what my previous example was alluding to, though in that I intended to use a namespaced macro rather than a namespaced function for extensibility.

You might well complain that `(pattern/cons 0 x)` is more verbose than `(cons 0 x)`, but it's also unambiguous, and doesn't require what would amount to a separate namespacing system, which a Lisp-1 like Clojure tends to reject.

Now, I don't think the above examples are magical in any way. The macro is a small improvement over the function, and the function isn't particularly complex. If higher-order functions are your idea of wizardry, then there's not much in Lisp that you wouldn't consider magic.

Maybe your objection just stems from the use of something like `p/cons` instead of just `cons`, while still desiring `cons` to be namespaced in some way. In which case, I'd suggest that what you're really asking is not 'can I have expression-macros and pattern-macros', but 'can I create new namespacing systems'.


Regarding 1., I don't think it follows that the pattern-macro will end up in one giant namespace. I'd love to understand why you think so.

And for 2, even though what you say seems desirable on the surface, you still approaches the problem in a way that is too fuzzy, or abstract. Just as saying "we should write more secure code" and then failing to attack the problem directly.

No offense, but even though you may have a nice idea, your explanation is a little too handwavy.


Regarding 2: The article punts on the question of how to implement such a system, because I think it's a hard question. I could have been clearer about that, and maybe in future I'll write an article speculating on some ways one could approach the problem. But I think it's an underappreciated problem, and I hope this article will spur other people to consider it.

Re 1:

In a.clj:

    (ns a (:refer-clojure))

    (defmulti if-match*
      (fn [pat _ _ _] (if (list? pat) (first pat) (type pat))))

    (defmacro if-match [pat expr then else]
      (if-match* pat expr then else))

    (defmethod if-match* clojure.lang.Symbol
      [pat expr then else]
      `(let [~pat ~expr] ~then))
In b.clj:

    (ns b (:refer-clojure) (:require a))

    ;; a 'foo pattern-macro that does negation
    (defmethod a/if-match* 'foo [pat expr then else]
      `(a/if-match ~(second pat) ~expr ~else ~then))
In c.clj:

    (ns c (:refer-clojure) (:require a))

    ;; a 'foo pattern-macro that is the identity pattern
    (defmethod a/if-match* 'foo [pat expr then else]
      `(a/if-match ~(second pat) ~expr ~then ~else))
Now, at the repl:

     user=> (use 'a)
     nil
     user=> (require 'b)
     nil
     user=> (if-match (foo x) 'yes x 'no)
     no
     user=> (require 'c)
     nil
     user=> (if-match (foo x) 'yes x 'no)
     yes
Note that 'require doesn't actually import the symbols defined by b.clj or c.clj, and despite that we're able to use the pattern-macro 'foo they define, because they're modifying if-match's dispatch-table. So our pattern-macros aren't being scoped the same way our regular macros are. I think this is wrong. Moreover, our pattern-macros have a single namespace, so we get collisions between what b.clj defines 'foo to mean and what c.clj defines it to mean, which is why (if-match (foo x) 'yes x 'no) changes behavior after the (require 'c).


It's worth clarifying that macros differ from functions in clojure in some pretty important ways, most obviously that you can't take their value - you can't (map macro coll) etc, which to me at least is a regular frustration.

That aside, clojure.core.match is implemented pretty much as you describe:

https://github.com/clojure/core.match/wiki/Extending-match-f...

I too was a bit confused about what the article was getting at.




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

Search: