# A macro for parametrized functions

I am contemplating writing a macro `@parameterized` that turns

``````@parameterized function ControlField(
t; # variable
ΔT₁, ΔT₂, ΔT₃, ϕ₁=0.0, ϕ₂=0.0, ϕ₃=0.0, E₀₁, E₀₂, E₀₃, # parameters
a::Float64=100.0  # constants (deteced by having type annotation)
)
_tanhfield(t; E₀=E₀₃, t₁=(ΔT₁+ΔT₂), t₂=(ΔT₁+ΔT₂+ΔT₃), a=E.a) * cos(ϕ₃)
end
``````

into

``````struct ControlField <: ParameterizedFunction
parameters::ComponentVector{Float64,Vector{Float64},Tuple{Axis{(ΔT₁=1, ΔT₂=2, ΔT₃=3, ϕ₁=4, ϕ₂=5, ϕ₃=6, E₀₁=7, E₀₂=8, E₀₃=9)}}}
a::Float64
end

# Maybe also a nice keyword-argument constructor here.

function (E::ControlField)(t)
@unpack E₀₃, ΔT₁, ΔT₂, ΔT₃, ϕ₃ = E.parameters
_tanhfield(t; E₀=E₀₃, t₁=(ΔT₁+ΔT₂), t₂=(ΔT₁+ΔT₂+ΔT₃), a=E.a) * sin(ϕ₃)
end
``````

Question 1: That seems like it should be doable, right? (Anybody getting hooked to write part of that macro for me? ) The `@unpack` in the output would have to be handled via `macroexpand`, right? Any other useful tips for going about this?

Question 2: Is my understanding correct that any code using that macro would need to import the names used in the expanded version:

``````using ComponentArrays: ComponentVector, Axis
using UnPack: @unpack # not needed if I'm using `macroexpand`, I guess
using QuantumPropagators.Controls: ParameterizedFunction
``````

Or is there some way where these names only need to be available in the place where the macro is being defined?

I think it should not.
I view macros as a shortcut to typing more. As such, they should just return what they saved the user from typing, which is your second code block and that contains a “raw” `@unpack`.

`@macroexpand` and `@macroexpand1` are your friends for developing

Yes, same idea as above. If the user would have typed it out, they would have also needed the `using`s to make it run. I guess you could also include the `using` statements in the expanded macro expression, but I never thought about its pros and cons and I always require the usings to be added by the user (which is always myself ).

With regards to skipping `using UnPack`. I guess that could work, because IIRC `@unpack` falls back to `getproperty`. However, I personally would regard that as an implementation detail and not rely on `macroexpand` to remove all ties to `UnPack`. In theory it could be that in the future `@unpack` expands to something like `Unpack.mygetproperty` and then it breaks. I was wrong, `@unpack` expands to `Unpack.unpack`, so `macroexpand` won’t work, see post below.

1 Like

Are you saying it is possible to have a macro call inside another macro?

(Any example for that in the wild?)

``````using UnPack

macro unpack_with_message(msg, ex)
return esc(quote
println(\$msg)
@unpack \$ex
end)
end

a = (; x1=1, x2=2, x3=3)

@unpack_with_message "unpacking something" x1 = a

display(@macroexpand1 @unpack_with_message "unpacking something" x1 = a)

display(@macroexpand @unpack_with_message "unpacking something" x1 = a)
``````

yields

``````julia> include("mwe_goerz.jl")
unpacking something
quote
#= /home/flo/wd/scratch/julia/mwe_goerz.jl:5 =#
println("unpacking something")
#= /home/flo/wd/scratch/julia/mwe_goerz.jl:6 =#
#= /home/flo/wd/scratch/julia/mwe_goerz.jl:6 =# @unpack x1 = a
end
quote
#= /home/flo/wd/scratch/julia/mwe_goerz.jl:5 =#
println("unpacking something")
#= /home/flo/wd/scratch/julia/mwe_goerz.jl:6 =#
begin
#= /home/flo/.julia/packages/UnPack/EkESO/src/UnPack.jl:100 =#
local var"##240" = a
#= /home/flo/.julia/packages/UnPack/EkESO/src/UnPack.jl:101 =#
begin
x1 = (UnPack).unpack(var"##240", Val{:x1}())
end
#= /home/flo/.julia/packages/UnPack/EkESO/src/UnPack.jl:102 =#
var"##240"
end
end
``````

So I was actually wrong, and `UnPack` does not use `getproperty`, but instead `UnPack.unpack`. (its actually documented GitHub - mauro3/UnPack.jl: `@pack!` and `@unpack` macros). So I would not really work with just `macroexpand`, I think.

Sorry, can’t think of one right now, but I use it in my own projects quite frequently.

1 Like

Another gem is MacroTools.jl, in particular `prewalk(rmlines, ex)` to remove those line blocks when developing.

2 Likes

This can be done more straightforwardly with `MacroTools.striplines(ex)`.

1 Like

To question 1: Inserting another macro call into the expansion does not pose any problems and does not require any special handling. When the newly passed expression gets smaller, you can even use the same macro in its expansion recursively:

``````macro or(exprs...)
if isempty(exprs)
:(true)
else
:(let v = \$(exprs[1])
if v
v
else
@or \$(exprs[2:end]...)
end
end)
end
end
``````

It’s very rare that you need to call `macroexpand` inside macro code and in any case, probably on the expression that what passed as an argument, i.e., to look inside a macro it was handed. I would not bother about that at the moment (and it won’t be necessary for your macro anyways).

2 Likes

Here is a quick and dirty attempt:

``````module CoolMacro

using MacroTools
using Parameters
using ComponentArrays

abstract type ParameterizedFunction end  # Should import this ...

# Some helper functions creating the expansion
function split_args(kwexprs)
params = []
consts = []
for expr in kwexprs
# Note: You might not want to ignore val here!
if @capture(expr, name_::T_ = val_) || @capture(expr, name_::T_ = val_)
push!(consts, name => T)
elseif @capture(expr, name_ = val_) || @capture(expr, name_)
push!(params, name)
end
end
(params, consts)
end

function expand_struct(name, params, consts)
# Note: You might want to parameterize the eltype!
axistype = Expr(:tuple, (:(\$x = \$i) for (i, x) in enumerate(params))...)
paramtype = :(ComponentVector{Float64,Vector{Float64},Tuple{Axis{\$axistype}}})
:(struct \$name <: ParameterizedFunction
parameters::\$paramtype
\$((:(\$x::\$(esc(T))) for (x, T) in consts)...)
end)
end

function expand_call(name, arg, params, body)
# Note: I don't bother to unpack only the required parameters!
:(function (E::\$(esc(name)))(\$arg)
@unpack \$(Expr(:tuple, params...)) = getfield(E, :parameters)
\$(esc(body...))
end)
end

# The final macro
macro parameterized(fundef)
@capture(fundef, function name_(arg_; kwargs__) body__ end) || error("Function definition with specific format expected!")
params, consts = split_args(kwargs)
struct_def = expand_struct(name, params, consts)
call_def = expand_call(name, arg, params, body)
:(begin
\$struct_def
\$call_def
end)
end

end # module
``````

You can check the expansion via:

``````julia> @macroexpand1 CoolMacro.@parameterized function ControlField(
t; # variable
ΔT₁, ΔT₂, ΔT₃, ϕ₁=0.0, ϕ₂=0.0, ϕ₃=0.0, E₀₁, E₀₂, E₀₃, # parameters
a::Float64=100.0  # constants (deteced by having type annotation)
)
_tanhfield(t; E₀=E₀₃, t₁=(ΔT₁+ΔT₂), t₂=(ΔT₁+ΔT₂+ΔT₃), a=E.a) * cos(ϕ₃)
end
``````

To question 2: If you are careful in using `esc` the definitions in the expansion should refer to the place where the macro has been defined, i.e., module `CoolMacro`, and not where it’s used. Thus, the user should not need to include `Parameters` for the macro to be able to use `@unpack` … not quite sure, if including it is necessary to run the function created by the macro though.

In any case, your desired macro is quite large and requires some care. You certainly want to read and understand the section on macro hygiene.

2 Likes

Wow, thanks everyone! These are some great tips and starting points!