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
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")
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
)
I'm not sure if there are easier examples, but check out how stdlib macros like @code_warntype
and @info
handle this
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
Thanks! I don't understand those fully but I can look more into it!
Definitely don't eval
in a macro!
Hmm, would you be able to briefly explain why?
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
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
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
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.
Empirical studies of eval usage
http://janvitek.org/pubs/oopsla12b.pdf
http://janvitek.org/pubs/oopsla21a.pdf
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.
Also the docs are pretty thin, yeah
Yeah, I found something from Emma Bourdou on medium but not a ton that I have seen
I remember the Introduction to metaprogramming in Julia workshop from JuliaCon 2021 being pretty good
Ooooh thank you!
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
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
Last updated: Dec 28 2024 at 04:38 UTC