Stream: helpdesk (published)

Topic: Keyword Arguments in Macros


view this post on Zulip Dale Black (Apr 08 2023 at 18:48):

Is there a workaround to provide keyword arguments for macros in Julia? The basic approach doesn't work but I am guessing there is some workaround??

macro keyword_macro(f; kwarg1=false)
    quote
        println(f, kwarg1)
    end
end
syntax: macros cannot accept keyword arguments around

view this post on Zulip Dale Black (Apr 08 2023 at 18:53):

This kind of works, but is ugly

macro print_kwargs(args...)
    # Filter out dictionaries (assuming dictionaries are passed for keyword arguments)
    pos_args = [arg for arg in args if !(arg isa Expr && arg.head == :call && arg.args[1] == :Dict)]
    kw_args = [arg for arg in args if (arg isa Expr && arg.head == :call && arg.args[1] == :Dict)]

    return quote
        println("Keyword arguments: ", $(kw_args[1]))
    end
end
@print_kwargs arg1 arg2 arg3 Dict(:a => 1, :b => 2, :c => "three")
#returns Keyword arguments: Dict{Symbol, Any}(:a => 1, :b => 2, :c => "three")

view this post on Zulip Dale Black (Apr 08 2023 at 19:49):

I came up with something like this but I am guessing there is something better

_button_dict = Dict(
    "disabled" => false,
    "onclick" => ""
)

macro button(text, kwargs...)
    for kwarg in kwargs
        kwarg = eval(kwarg)
        _button_dict["$(kwarg[1])"] = kwarg[2]
    end

    quote
        $(@htl("""
            <button
                disabled=$(eval(_button_dict["disabled"]))
                onclick=$(eval(_button_dict["onclick"]))
            >
                $(eval(text))
            </button>
        """))
    end
end

@button(
    "hi",
    "disabled" => false,
    "onclick" => msg
)

view this post on Zulip Brian Chen (Apr 08 2023 at 20:04):

I'm not sure if there are easier examples, but check out how stdlib macros like @code_warntype and @info handle this

view this post on Zulip Brian Chen (Apr 08 2023 at 20:05):

For the latter https://github.com/JuliaLang/julia/blob/1cf5091b474f46a4fc1f2d648db9be168e610399/base/logging.jl#L399 looks relevant. I think matching on = Exprs will get you things which look like kwargs

view this post on Zulip Dale Black (Apr 08 2023 at 20:10):

Thanks! I don't understand those fully but I can look more into it!

view this post on Zulip Mason Protter (Apr 08 2023 at 20:51):

Definitely don't eval in a macro!

view this post on Zulip Dale Black (Apr 08 2023 at 20:56):

Hmm, would you be able to briefly explain why?

view this post on Zulip Mason Protter (Apr 08 2023 at 21:06):

Sorry for the delay, I'd write this maybe like so:

macro button(text, kwargs...)
    allowed_kwargs = Set([:disabled, :onclick])
    defaults = Dict(:disabled => false, :onclick => "")
    processed_kwargs = map(kwargs) do kwarg
        if Base.isexpr(kwarg, :(=), 2)
            if kwarg.args[1]  allowed_kwargs
                kwarg.args[1] => kwarg.args[2]
            else
                error(ArgumentError("Not an acceptable keyword argument $(kwarg.args[1])"))
            end
        elseif kwarg isa Symbol
            if kwarg  allowed_kwargs
                kwarg => kwarg
            else
                error(ArgumentError("Not an acceptable keyword argument $(kwarg)"))
            end
        else
            error(ArgumentError("Malformed kwarg $kwarg"))
        end
    end
    d = merge(defaults, Dict(processed_kwargs))
    s = """<button
                disabled=$(d[:disabled])
                onclick=$(d[:onclick])
            >
                $(text)
            </button>
        """
    :($(@__MODULE__).@htl($s)) |> esc
end

view this post on Zulip Mason Protter (Apr 08 2023 at 21:11):

Dale Black said:

Hmm, would you be able to briefly explain why?

There are many problems with it. So first of all, when you wrote kwarg = eval(kwarg) that actually ended up (re)defining global variables disabled and onclick. This is because eval always happens in the global scope, so it'll cause all sorts of confusion, e.g.

julia> macro foo(x)
           y = eval(x)
           y + 1
       end;

julia> x = 1;

julia> let x = -1000
           @foo x
       end
2

view this post on Zulip Dale Black (Apr 09 2023 at 05:11):

Wow there is so much to learn about meta programming in Julia! Are there any resources aside from the Julia docs? Those are kinda thin imo

view this post on Zulip Santtu (Apr 10 2023 at 13:12):

It's not just Julia where you should avoid the use of eval. I'd argue that there is no use for it anywhere ever, unless you're writing a REPL of your own.

view this post on Zulip jar (Apr 10 2023 at 15:48):

Empirical studies of eval usage
http://janvitek.org/pubs/oopsla12b.pdf
http://janvitek.org/pubs/oopsla21a.pdf

view this post on Zulip Brian Chen (Apr 10 2023 at 18:36):

Dale Black said:

Wow there is so much to learn about meta programming in Julia! Are there any resources aside from the Julia docs? Those are kinda thin imo

I remember people having wrote about this but not who and where. Maybe have a search through the Juliabloggers archives to see if anything stands out there.

view this post on Zulip Brian Chen (Apr 10 2023 at 18:36):

Also the docs are pretty thin, yeah

view this post on Zulip Dale Black (Apr 11 2023 at 20:48):

Yeah, I found something from Emma Bourdou on medium but not a ton that I have seen

view this post on Zulip Sundar R (Apr 11 2023 at 20:56):

I remember the Introduction to metaprogramming in Julia workshop from JuliaCon 2021 being pretty good

view this post on Zulip Dale Black (Apr 11 2023 at 22:23):

Ooooh thank you!

view this post on Zulip Dale Black (Apr 18 2023 at 07:12):

Alright, I am still playing with this. I think I have come up with a pretty simple approach that seems to mimic React/JSX style templating with pure Julia code instead of strings. Do y'all see anything that is a big no-no, in this approach?

function process_exprs(macro_symbol, exprs)
    str = string("<", macro_symbol, " ")
    for e in exprs
        if typeof(e) == Expr
            if e.head == :call && e.args[1] == :(=>)
                str *= string(e.args[2].value, "=", e.args[3])
                str *= " >"
            end
            if e.head == :tuple
                for arg in e.args
                    str *= string(arg.args[2].value, "=", arg.args[3], " ")
                end
                str *= ">"
            end
        end
        if typeof(e) == String
            if last(str) == '>'
                str *= string(e)
            else
                str *= string(">", e)
            end
        end
        if typeof(e) == Expr
            if e.head == :macrocall
                str *= process_exprs(split(string(e.args[1]), "@")[2], e.args)
            end
        end
    end
    str *= string("</", macro_symbol, ">")
    return str
end

macro p(exprs...)
    str = process_exprs("p", exprs)
    return :(@htl("""
            $($(str))
    """))
end

macro span(exprs...)
    str = process_exprs("span", exprs)
    return :(@htl("""
            $($(str))
    """))
end

image.png

view this post on Zulip Dale Black (Apr 18 2023 at 07:13):

image.png

view this post on Zulip Dale Black (Apr 18 2023 at 07:18):

Also, I am having trouble with this approach when it comes to HTML element attributes. Right now, all the attributes are being converted to a string which should work for classes, but attributes that expect javascript code or booleans for example are now messed up. Any idea how I could modify this part

        if typeof(e) == Expr
            if e.head == :call && e.args[1] == :(=>)
                str *= string(e.args[2].value, "=", e.args[3])
                str *= " >"
            end
            if e.head == :tuple
                for arg in e.args
                    str *= string(arg.args[2].value, "=", arg.args[3], " ")
                end
                str *= ">"
            end
        end

to account for this error?

e.g.

macro button(exprs...)
    str = process_exprs("button", exprs)
    return :(@htl("""
            $($(str))
    """))
end

image.png
image.png


Last updated: Nov 06 2024 at 04:40 UTC