Pass through function name to different type

I am making a type that holds another type as one of its fields, like so

struct A
    a::Float64
end

f(a::A, x::Real) = A.a * x
# plus whole other bunch of functions with similar arguments
g(a::A, x::Real) = ...
h(a::A, x::Real) = ...

struct B
    a1::A
    a2::A
end

Now I want the functions that operate on B to use the A objects inside, depending on the value of x, like so

function f(b::B, x::Real)

    if x >= 0.0
        return f(b.a1, x)
    else
        return f(b.a2, x)
    end

end

However, as listed above there are a bunch of functions that operate on A with x as an input, and it seems cumbersome to copy such a similar function signature many times. So is there a way to pass through the function as well? Something like this (obviously this doesn’t work):

function (function_name)(b::B, x::Real) where {function_name}

    if x >= 0.0
        return function_name(b.a1, x)
    else
        return function_name(b.a2, x)
    end

end

This example above I would only have to write once, and it would work for f, g, h, and any such function that is defined on A. Is something like this possible in Julia?

I am not sure if I completely understood you problem, but you can pass functions as any other parameter:

julia> f(a,b) = min(a,b)
f (generic function with 1 method)

julia> g(a,b) = max(a,b)
g (generic function with 1 method)

julia> h(x,y,f) = f(x,y)
h (generic function with 1 method)

julia> h(1,2,f)
1

julia> h(1,2,g)
2


It sounds like your type B is a container for type A objects, and applying a function f to B should apply f to each A element. Possibly the cleanest way to do so is to use broadcasting, so you can write f.(b,x).

Thanks for the replies, I’ll try to clarify a bit.

So I’m trying to find out how to avoid having to write the following code for every function that acts on A to also work on B (where again, depending on the value of x, a different A object is selected for the function evaluation):

function f(b::B, x::Real)

    if x >= 0.0
        return f(b.a1, x)
    else
        return f(b.a2, x)
    end

end

function g(b::B, x::Real)

    if x >= 0.0
        return g(b.a1, x)
    else
        return g(b.a2, x)
    end

end

function h(b::B, x::Real)

    if x >= 0.0
        return h(b.a1, x)
    else
        return h(b.a2, x)
    end

end

This code is all so similar, that I figured there would be a way to only write this logic once and have it be applied to every function that acts on A (something along the lines of what I wrote in the last code block in my original question).

I think this is a bit different than broadcasting, as I don’t want f (and other functions) to simply work on each A object inside of B but rather use the value of x to select which A object to use.

So, one alternative is to pass the function as a parameter:

function f(b::B, x::Real,func)

    if x >= 0.0
        return func(b.a1, x)
    else
        return func(b.a2, x)
    end

end

func may be f, g, h, etc.

Write a macro that takes a list of function names and writes all this code for you.

1 Like

@lmiq: thanks for your reply. I should note that the functions f, g, h, etc are already used throughout the code base, and your suggestion would require changing a bunch of code (wherever f, g, etc. are called). (That’s my bad, I should’ve made that clear)

@dlakelan: that is a great pointer!

Just to make sure I’m interpreting your answer correctly. You’re suggesting this?

for op = (:h, :g, :h)
       eval(
              quote
                     function $op(b::B, x::Real)
                            if x >= 0.0
                                   return $op(b.a1, x)
                            else
                                   return $op(b.a2, x)
                            end
                     end
              end
       )
end

Are there any problems with that approach in terms of dispatch, type-stability or performance? (or does it simply just generate the code and that’s it?)

1 Like

Nope! It’s a perfectly reasonable thing to do.

Yup, exactly. The only downside is that it can make your code harder to read, especially since your text editor or IDE may not be able to figure out where the functions are defined.

4 Likes

Thanks @rdeits and @dlakelan, this solved my problem!

(I gave @dlakelan the solution as that answer pointed me in the right direction, hope that’s ok. If I could assign the solution to you both, I would)

1 Like

Meta programming is Julia’s secret weapon

2 Likes

You can write this a bit shorter with

@eval begin 
   ... 
end

replacing eval(quote .. end)

2 Likes