JuliaLang / julia

The Julia Programming Language
https://julialang.org/
MIT License
45.43k stars 5.45k forks source link

"fake" multiple dispatch for different expression types in macros #20929

Open ExpandingMan opened 7 years ago

ExpandingMan commented 7 years ago

It would be really cool if one could do something like

macro m(expr::ExprHeadType{:call,:(=)})
    f(expr)
end

macro m(expr::ExprHeadType{:block})
   g(expr)
end

which would be equivalent to

macro m(expr)
    if expr.head ∈ [:call, :(=)]
        return f(expr)
    elseif expr.head ∈ [:block]
        return g(expr)
    else
        throw(ArgumentError("You gave me an expression I don't recognize."))
    end
end

(of course we should think of a better name than ExprHeadType). Of course, this would have to then require that the expr argument be an Expr rather than a Symbol or literal. Any macros that aren't written with ExprHeadType would behave normally.

Additionally, it would be nice to be able to dispatch functions on expression types (mainly for the purpose of helping macros), sort of like it's currently possible to do with specific values using Val. (I don't think that's a crazy extrapolation from current behavior, but I know very little about the compiler.)

Is this completely crazy from a compiler perspective, or something people might be interested in?

quinnj commented 7 years ago

I'm pretty sure I remember this being brought up before and people thought it was a good idea. I also know there's been talk of "finalizing" our AST/Expr representation for 1.0 as well.

StefanKarpinski commented 7 years ago

This would largely rely on changing the AST representation to a more type-based one, where instead of using Expr everywhere with different values for the .head field, we use different types to represent different syntax nodes with specific structures. This would be a fairly disruptive change, but if we're going to do it, before 1.0 is certainly the time to do so. Now that macros can do dispatch (it's real dispatch, btw, just on ASTs), it would also allow writing some transformations more concisely and conveniently.

bramtayl commented 7 years ago

Going further, there could even be fully typed trees, like:

ForExpr.iterator IfExpr.condition

ExpandingMan commented 7 years ago

Going further, there could even be fully typed trees, like:

ForExpr.iterator IfExpr.condition

I would be very worried that that sort of thing would make symbolic manipulation too complicated. You'd have to remember or look up the fields for every possible type of expression. I think using head and args makes it pretty easy to infer what the structure of an expression will look like, it just gets really annoying sometimes to have to write big ugly conditional constructs to identify the heads. (And it seems somehow "anti-Julian"!)

bramtayl commented 7 years ago

I can see how it might be an issue to iterate. Maybe a dict? IfExpr.args[:condition] for example

JeffBezanson commented 7 years ago

This is the kind of thing pattern matching is good for.

JeffBezanson commented 7 years ago

What I mean is that while dispatching on expression heads is somewhat useful, you need to be able to match on full tree structures to get any real benefit. Expression structures are also very flexible and dynamic, so they are not a great fit for the design assumptions of our multiple dispatch. This kind of programming really demands a feature that converts a series of tree patterns to a nest of if statements. Such an approach also makes it easier to change the representation of expressions, since you only need to change the pattern compiler.

ExpandingMan commented 7 years ago

I've found #12102 which seems to be a suggestion that pattern matching for expressions be included in Base. The result seems to be that the poster wrote the MacroTools package which seems to still be actively maintained.

I find it interesting that most of the discussion in that post is about users not necessarily being knowledgeable about the AST. Personally, I find that to be a very small issue, I simply use the REPL to check what the heads of various expressions are, and usually the args are pretty obvious. Actually writing the macro code once I know exactly what I need to do is what I'm still finding slightly awkward.

ExpandingMan commented 7 years ago

It seems that this has now effectively been implemented through Val, though this cannot directly be used in macro definitions.

Maybe I'm crazy but I could have sworn that at some point Val was not working for Symbols. However, now it does. My original example becomes

h(::Union{Type{Val{:call}},Type{Val{:(=)}}}, expr) = f(expr)
h(::Type{Val{:block}}, expr) = g(expr) 

macro m(expr)
    h(expr.head, expr)
end

It's not beautiful, but it's certainly an improvement.

Is there some simple way of using Val to allow macros declarations to behave as if they're dispatching on expression types?

vtjnash commented 7 years ago

Is there some simple way of using Val to allow macros declarations to behave as if they're dispatching on expression types?

Use a real pattern-matching library (like the previously mentioned MacroTools package)?