Return tuple of user chosen functions, metaprogramming?

Hi,

I have a problem where, after som expensive calculations, an anonymous function is created. The anonymous function needs to return a tuple of the results depending on the users input. Here is an example of what I wish to achieve.

function run(funs::Array{Symbol,1})
    #f1,f2,f3 are expensive and only available at run time
    f1 = x->minimum(x)
    f2 = x->maximum(x)
    f3 = x->sum(x)
    
    return function (x)
        ...
    end
end
julia> foo = run([:f1, :f3])
julia> foo([1 2 3])
(6,1)

To me it seems like a problem which could (should?) be solved by metaprogramming; however, my attempts have been hacky at best. I’m sure there’s an elegant Julian way of doing it!

Something like this:


julia> function foo(funcs)

         return x -> [f(x) for f in funcs]

       end
foo (generic function with 2 methods)

julia> foo(funcs...) = foo(funcs)
foo (generic function with 2 methods)

julia> g = foo(f1, f2)
#9 (generic function with 1 method)

julia> g([3, 4, 5])
2-element Array{Int64,1}:
  5
 12

Or if you want them to return a tuple:

julia> foo(funcs...) = x -> map(f -> f(x), funcs)
foo (generic function with 1 method)

julia> foo(sum, minimum, maximum)(rand(10))
(5.8259370715920795, 0.3263348387493512, 0.8915579235083304)

Thanks, returning a tuple is what I need!

Do you have an idea of how I could get the interface to work? The application where I’m using this is nested in an expensive function which generates f1,f2,f3 in the example and both the number of functions and their order can vary based on the user input.

function run(funs::Array{Symbol,1})
    #f1,f2,f3 are expensive and only available at run time
    f1 = x->minimum(x)
    f2 = x->maximum(x)
    f3 = x->sum(x)
    
    return function (x)
        ...
    end
end

The following seems to work pretty well for the problem as you’ve posed it:

julia> function run(names::Vector{Symbol})
         library = Dict(
           :f1 => x -> minimum(x),
           :f2 => x -> maximum(x),
           :f3 => x -> sum(x)
         )
         functions_to_call = Tuple([library[s] for s in names])
         return function (x)
           call.(functions_to_call, Ref(x))
         end
       end
run (generic function with 1 method)

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

julia> f = run([:f1, :f2])
#38 (generic function with 1 method)

julia> f([1,2,3])
(1, 3)

Some notes:

  • I’m assembling a Dict mapping names to functions, rather than trying to look them up by the names of the variables they’re bound to. I wouldn’t recommend trying to access things inside some other function by their variable names: variable names don’t really exist in the compiled code, so looking things up by variable name isn’t a great idea.
  • I construct a Tuple of functions to call instead of just a list so that Julia can correctly infer the number of outputs of the resulting function (a tuple’s length is part of its type, which is not true for an Array). That means that Julia can correctly infer the output type of f() in the above example:
julia> @code_warntype f([1,2,3])
Body::Tuple{Int64,Int64}
9 1 ─ %1  = %new(Base.RefValue{Array{Int64,1}}, x)::Base.RefValue{Array{Int64,1}}                            β”‚β•»β•·β•·              Type
  β”‚   %2  = (Base.getfield)(%1, :x)::Array{Int64,1}                                                          β”‚β”‚β•»β•·β•·β•·β•·β•·β•·β•·β•·β•·       copy
  β”‚   %3  = Base.identity::typeof(identity)                                                                  β”‚β”‚β”‚β•»β•·β•·β•·β•·β•·           tuplebroadcast
  β”‚   %4  = Base.min::typeof(min)                                                                            ││││┃│││││││││       ntuple
  β”‚   %5  = invoke Base._mapreduce(%3::typeof(identity), %4::typeof(min), $(QuoteNode(IndexLinear()))::IndexLinear, %2::Array{Int64,1})::Int64
  β”‚   %6  = (Base.getfield)(%1, :x)::Array{Int64,1}                                                          β”‚β”‚β”‚β”‚β”‚β”‚β•»β•·β•·β•·β•·β•·           _broadcast_getindex
  β”‚   %7  = Base.identity::typeof(identity)                                                                  β”‚β”‚β”‚β”‚β”‚β”‚β”‚β•»β•·β•·β•·β•·β•·           _broadcast_getindex_evalf
  β”‚   %8  = Base.max::typeof(max)                                                                            ││││││││┃│││││           call
  β”‚   %9  = invoke Base._mapreduce(%7::typeof(identity), %8::typeof(max), $(QuoteNode(IndexLinear()))::IndexLinear, %6::Array{Int64,1})::Int64
  β”‚   %10 = (Core.tuple)(%5, %9)::Tuple{Int64,Int64}                                                         β”‚β”‚β”‚β”‚β”‚            
  └──       return %10   

and that in turn means that you should be able to use the resulting f efficiently:

julia> using BenchmarkTools

julia> @btime $f($([1,2,3]))
  7.089 ns (0 allocations: 0 bytes)
(1, 3)
3 Likes

Thanks for the help, this was precisely what I was looking for! Glad to see that it can be done with such good performance as well.