struct Car
color::String
model::String
gears::Int
end
and I have another struct
struct CarGroup
cars::Vector{Car}
end
I would like all the functions called on Car to be applied to CarGroup in this way:
function color(c::Car)
return c.color
end
function color(cg::CarGroup)
return map(cg.cars) do c
color(c)
end
end
Is this thing even possible with standard language? I imagine that I can define a macro that does and add it to all the functions of interest… but I would much prefer some magic function of the type:
function f(cg::CarGroup, args...; kwargs...)
map(c->f(c, args...;kwargs...), cg,cars)
end
No. The syntax for it does exist, but we’re deliberately prevented from defining one method for arbitrary callables because it silently interferes with other functions’ dispatch e.g. we really don’t want print(::CarGroup) calls to be affected:
julia> function (f::Function)(cg::CarGroup, args...; kwargs...)
map(c->f(c, args...;kwargs...), cg,cars)
end
ERROR: cannot add methods to builtin function `Function`
Stacktrace:
[1] top-level scope
@ REPL[6]:1
julia> function (f::Any)(cg::CarGroup, args...; kwargs...)
map(c->f(c, args...;kwargs...), cg,cars)
end
ERROR: cannot add methods to builtin function `Any`
Stacktrace:
[1] top-level scope
@ REPL[7]:1
julia> function (f::F)(cg::CarGroup, args...; kwargs...) where F
map(c->f(c, args...;kwargs...), cg,cars)
end
ERROR: function type in method definition is not a type
Stacktrace:
[1] top-level scope
@ REPL[5]:1
Best we can do is metaprogramming for select callables (see karei’s @eval loop), or embrace a higher order function that takes input callables. map(color, cg.cars) is already fairly terse, the only possible improvement is a custom higher order function that accesses the cars field for us like carmap(color, cg), which doesn’t save much writing.
Also note that it generally doesn’t really make sense to forward “all” functions in this manner.
For example, a recolor(car::Car)::Car function generalized to CarGroup should probably return CarGroup back, not a Vector{Car} like map would do.
Custom broadcasting calls e.g. color.(x::CarGroup) are different from sharing direct function calls on scalars and containers color(x::Union{Car, CarGroup}). Worth mentioning that broadcasting calls can be shared instead if custom broadcasting is also implemented for Car as a 0-dimensional scalar like strings.
You may be able to create a macro that does what you want. I’m out of practice with macro writing, so just regard this as a proof concept. I’m pretty sure I should be escaping symbols.
julia> struct Car
color::String
model::String
gears::Int
end
julia> color(c::Car) = c.color
color (generic function with 2 methods)
julia> struct CarGroup
cars::Vector{Car}
end
julia> macro mapforward(from::Symbol, to::Symbol)
quote
for method in InteractiveUtils.methodswith($from)
n = method.name
q = quote
global $n
function $n(cg::CarGroup, args...; kwargs...)
map(c->$n(c, args...; kwargs...), cg.cars)
end
end
eval(q)
end
end
end
@mapforward (macro with 1 method)
julia> @mapforward(Car, CarGroup)
julia> cg = CarGroup([
Car("white","legend", 5),
Car("red", "integra", 3)
])
CarGroup(Car[Car("white", "legend", 5), Car("red", "integra", 3)])
julia> color(cg)
2-element Vector{String}:
"white"
"red"
Iteration Interface
Rather than jumping to overload broadcasting, it may be sufficient to implement iteration.
Thanks for your clear explanation. I think the @eval loop is quite a clean solution, this way I can clearly define which functions are expected to broadcast within the group and not incur in unexpected behaviours from other functions!
yeah, the map is an example. In the actual use case, I want to dispatch a set of functions to a group without collecting the objects (in-place functions !) (juliaSNN).
Thanks a lot for this solution, it works quite well this way!
It’s also nice that I can dispatch the function to the group in different ways depending on the function.
modifiers = [:paint!, :start!]
properties = [:color, :year]
for f in modifiers
@eval $f(cg::CarGroup, args...; kwargs...) = begin
map(c -> $f(c, args...; kwargs...), cg.cars)
cg
end
end
for f in properties
@eval $f(cg::CarGroup, args...; kwargs...) =
map(c -> $f(c, args...; kwargs...), cg.cars)
end