sjp
sjp

Reputation: 392

How do I make a macro for a struct generate a function method matching on the struct?

Pardon any confused terminology in the title, but imagine I want to have a little macro to mark structs I create as usable for some nefarious purpose. I write this little module:

module Usable

export @usable, isusable

isusable(::Type{T}) where T = false

macro usable(expr::Expr)
    name = expr.args[2]

    return quote
        $expr
        
        Usable.isusable(::Type{$name}) = true     # This in't working
    end
end

end

However, trying to use my macro

julia> include("Usable.jl")
Main.Usable

julia> using Main.Usable

julia> @usable struct Foo
           bar::String
       end

results in

ERROR: UndefVarError: Foo not defined

The struct was evidently defined just fine

julia> Foo("soup")
Foo("soup")

so it would seem the definition is required earlier than I expect. I am obviously missing something, but I can't figure out what.

Upvotes: 2

Views: 402

Answers (2)

Przemyslaw Szufel
Przemyslaw Szufel

Reputation: 42214

In the described scenario almost always you should use Julia's powerful type system and multiple dispatch mechanism not a macro. (Perhaps you have a good reason to do so but this is information for others.) The pattern is to simply define the desired behavior through an abstract type that is later inherited by a custom struct.

One example is here for adding a Comparable behavior to composite structs.

abstract type Comparable end

import Base.==
==(a::T, b::T) where T <: Comparable =
    getfield.(Ref(a),fieldnames(T)) == getfield.(Ref(b),fieldnames(T))

And now using:

julia> struct MyStruct <: Comparable 
    f::Vector{String}  
end;

julia> MyStruct(["hello"]) == MyStruct(["hello"])
true

Upvotes: 2

phipsgabler
phipsgabler

Reputation: 20950

Always have a look at the output of your macros:

julia> @macroexpand @usable struct Foo
                  bar::String
              end
quote
    #= REPL[1]:11 =#
    struct Foo
        #= REPL[4]:2 =#
        bar::Main.Usable.String
    end
    #= REPL[1]:13 =#
    (Main.Usable.Usable).isusable(::Main.Usable.Type{Main.Usable.Foo}) = begin
            #= REPL[1]:13 =#
            true
        end
end

The problem is that the macro output is expanded within the module it has been defined, which messes up what the names mean. In this case, we want Foo to refer to the namespace where it was defined, though:

quote
    #= REPL[1]:11 =#
    struct Foo
        #= REPL[10]:2 =#
        bar::String
    end
    #= REPL[1]:13 =#
    Usable.isusable(::Type{Foo}) = begin
            #= REPL[1]:13 =#
            true
        end
end

It's actually simple to get that -- just escape the output:

macro usable(expr::Expr)
   name = expr.args[2]

   return esc(quote
       $expr
       $Usable.isusable(::Type{$name}) = true
   end)
end

But do read the macro documentation again. esc is quite intricate, and you don't want to blindly apply it to everything you write.

The other thing (and I hope to get this right) is to splice in the module itself -- $Usable -- instead of referring to it by name. Otherwise you might have a problem if you rename the module name outside.

Upvotes: 5

Related Questions