Function forwarding for clashing function names

I have general question regarding composition and function forwarding. Let’s assume we have a struct car which is composed of a motor and a horn:

struct car

Both of it’s components implement a function change! which allows us to modify internals:

change!(m::Motor; num_pistons=4) = println("change number of pistons") # dummy function
change!(h::Horn; decibel=90) = println("change volume of tooting") # dummy function

Now my natural intention is to try to forward this behaviour to the car itself. For example, the call
change!(x::car, num_pistons=6, decibel=100) should result in change!(x.motor, num_pistons=6) and change!(x.horn, decibel=100). However, additionally I want to have the following behaviour:

  • keyword arguments are optional, so that we can choose to modify only a subset of (car) components
  • if two components have the same keyword argument implemented, use it in both change! functions
  • if one keyword argument can not be resolved, give an error
  • low runtime costs

Is there an elegant way of achieving this? Is this design itself questionable and should be done differently (maybe by instead implementing change_motor! and change_horn! for a car)?

One option:

function change!(c::car; kwargs...)
  motorkws = (:num_pistons,)
  hornkws = (:decibels,)
  kwkeys = keys(kwargs)
  unused = setdiff(kwkeys,motorkws,hornkws)
  isempty(unused) || error("will not use keywords ",unused)
  kw = intersect(kwkeys,motorkws)
  isempty(kw) || change!(c.m;kwargs[kw]...)
  kw = intersect(kwkeys,hornkws)
  isempty(kw) || change!(c.h;kwargs[kw]...)
  return c # or return nothing, etc
julia> struct Motor end; struct Horn end; struct car; m::Motor; h::Horn; end

julia> c = car(Motor(),Horn())
car(Motor(), Horn())

julia> change!(c; num_pistons=99)
change number of pistons
car(Motor(), Horn())

julia> change!(c; num_pistons=99, magic=false)
ERROR: will not use keywords [:magic]

But the intersect and setdiff operations here, as well as the kwargs[kw] calls resulting from them, will allocate so leave some performance on the table. One could get much more involved with that handling to do better (and perhaps there’s already a package to help), but you’ll have to decide whether that’s worth the programming effort.

Modest performance issues aside, I find this tedious to program. Also, I worry whether it’s such a good idea to have shared keywords distributed everywhere. If you make a cost keyword, for example, should that really be applied to every component in the car?

I think that your change_motor! idea is more clear and won’t require all the programming work that will be needed for a single change!(::car,...) mega-method. It’s a simple matter of method forwarding

change_motor!(c::car; kwargs...) = change_motor!(c.m; kwargs...)

When do I use variations on your original pattern here (which isn’t often), they tend to look like this lazy option that gives up some behavioral niceties:

change!(m::Motor; num_pistons=4, _...) = println("change number of pistons")
change!(h::Horn; decibel=90, _...) = println("change volume of tooting")

function change!(c::car; kwargs...)
  return c

This will call every function regardless of whether any relevant keywords are present. It will quietly ignore any extra or mis-spelled keywords.

1 Like

Thanks a lot! Since my application is built for the end-user and should give errors, I can’t really use your lazy option. What do you think about:

function change!(x::car, field::Symbol; kwargs...)
    y = getfield(x, field)
    change!(y; kwargs...)

I would use getproperty rather than getfield, as properties are considered more user-facing than fields (getproperty defaults to getfield in any case), but otherwise that’s reasonable.

I’ll remark that having the user call change!(x, :m; kwargs...) isn’t really much different from just having them call change!(x.m; kwargs...), nor is it necessarily easier than having them call change_motor!(x; kwargs...). I’d suggest you consider all those options and choose whichever seems “nicest”.

Personally, I use the change_motor! pattern most frequently. An advantage is that it’s the easiest one to change (without needing your user to change their code) if you change the representation of car later. For example, perhaps you later decide that the “motor” should be contained within a “drivetrain” substructure. The getfield approach won’t work (unless you do the transformations on the target field before calling it, at which point it’s basically getproperty), getproperty can work unchanged if you redefine getproperty (which is a little tedious but definitely doable), but for change_motor! you can make the tiny change from it using x.m to x.drivetrain.m and it’s good to go. Or, for even more extensibility, you can change it to call change_motor!(x.drivetrain; kwargs...) and add a change_motor(::Drivetrain; kwargs...) method to support that.

1 Like