Creating function that broadcast function over multiple keyword arguments

Dear all

I come from the R community, and try to switch to Julia )
Assume I have a function f1 (fictive function below)

f1 = function(x, y ; a = 1.5, b = 5)
coef = a * sum(x’y) + b
(coef = coef, mod = 1)
end

where x, y are vectors, e.g.

x = [1, 10, -2] ; y = [.5, 7, 22.3] ;

and that I want to replicate this function over vectors (instead of single elements)
of parameters a and b, for instance

a = [1, 2] ; b = [.5, -.5] ;

Using map, I can do

map((a, b) → f1(x, y ; a = a, b = b), a, b)

or with another syntax
(Julia: Passing keyword arguments to function through the map() function - Stack Overflow)

map(a, b) do a, b
f1(x, y ; a = a, b = b)
end

I can also build the following meta function ‘metaf’ (using the second syntax
since in real I have successive steps in the map)

metaf = function(x, y, fun ; a, b)
map(a, b) do a, b
fun(x, y ; a = a, b = b)
end
end

metaf(x, y, f1 ; a = a, b = b)

What I would like is such a metaf function but being generic for any function ‘fun’ that can have
its own arguments (different from ‘a’ and ‘b’ of function ‘f1’ above).
.
Something like

metaf(x, y, fun = f2 ; gamma, alpha, theta)
metaf(x, y, fun = OtherFun ; C)

etc.

Is it possible to do this with map in the previous metaf function?

Or may be map is not the efficient way?

I tried to incorporate things like ‘args…’ in metaf but without success

Thanks for any ideas

I think you can do

f1.(Ref(x),Ref(y),a=a,b=b) # note the dot

In which case the function call will be broadcasted on a and b only

Edit: no, it does not work for keyword parameters.

Something on these lines could work (sorry I am not really able to test anything now)

f(x,args...) = args[1][1]*x .+ args[2][1]

x = [ 1, 2 ]
a = [ 2, 4, 6 ]
b = [ 1, 1, 2 ]

@show f(x,1,2)

@show f.(Ref(x),a,b)




Quick question:

julia> map(+, [1,2], [3,4,5,6,7])
2-element Vector{Int64}:
 4
 6

is this what you intend with a and b, applying fun on direct pairs of values, or do you want to apply fun on every combination of elements in a and b?

Edit: both are totally possible but neither looks particularly elegant. The problem is, that keyword arguments get passed as NamedTuples (or a pairs view thereof) and are also expected in this form by fun (NamedTuple or Dict{Symbol,_}). So you essentially want to iterate the content of each element of the named tuple while preserving the name. Not too hard but looks unwieldly:

transform_kwargs(; kwargs...) = [map(x->Pair(arg.first, x), arg.second) for arg in kwargs]
transform_kwargs(kwargs) = [map(x->Pair(arg.first, x), arg.second) for arg in kwargs]

julia> transform_kwargs(a = [1,2], b = [:x, :y, :z], c = 2)
3-element Vector{Any}:
     [:a => 1, :a => 2]
     [:b => :x, :b => :y, :b => :z]
 :c => 2
# note that this breaks down for non-iterable arguments like transform_kwargs(say = :nooooo)
# so probably not the most reasonable implementation, sorry ^^

(please correct me if there’s a function for that already, couldn’t think of one)

Your metafun gets easy then (solved for all pairs of arguments here):

function metaf(x, y, fun; kwargs...)
    combos = Iterators.product(transform_kwargs(kwargs)...)
    return [fun(x, y; keywords...) for keywords in combos]
end
julia> metaf([1,2,3], [4,5,6], 
(x,y;alpha, beta, theta)->(theta .* y ./ exp.(alpha .+ beta .* x)); 
alpha = [-1, 0, 1], beta = collect(1.:0.1:2.), theta = [1.] )
3×11×1 Array{Vector{Float64}, 3}:
[:, :, 1] =
 [4.0, 1.8394, 0.812012]         [3.61935, 1.50597, 0.601553]     …  [1.47152, 0.248935, 0.0404277]
 [1.47152, 0.676676, 0.298722]   [1.33148, 0.554016, 0.221299]       [0.541341, 0.0915782, 0.0148725]
 [0.541341, 0.248935, 0.109894]  [0.489826, 0.203811, 0.0814114]     [0.199148, 0.0336897, 0.00547129]

But this looks rather opaque, so you can probably come up with a clearer and more explicit solution.

Editedit: if you put fun as a first argument to metaf, this enables do-block syntax which makes it a lot more readable.

Thanks for quoting me this syntax wuth f. and @show… I will study it.
But I think

is a constraint to me, since here it requires to know that the function f has two arguments, while f1, f2 etc. can have different numbers of arguments and I would like to automatize. Thanks anyway

Yes, direct pairs

Many thanks for all your powerfull code. I will work on it in more details and if needed will come back to you

Another solution:

function metaf(x, y, f; args...)
    names = [a.first for a in args]
    values = [a.second for a in args]
    map(values...) do v...
        f(x, y; Pair.(names, v)...)
    end
end

or even shorter:

function metaf(x, y, f; args...)
    map(values(args)...) do v...
        f(x, y; Pair.(keys(args), v)...)
    end
end

Example:

x = 1;
y = 2;
a = [1,2,3];
b = [4,5,6];

function myf(x, y; a, b)
    coef = a * sum(x'y) + b
    (coef = coef, mod = 1)
end

julia> metaf(x, y, myf; a, b)
3-element Vector{NamedTuple{(:coef, :mod), Tuple{Int64, Int64}}}:
 (coef = 6, mod = 1)
 (coef = 9, mod = 1)
 (coef = 12, mod = 1)
2 Likes

This is amazing! (@sudete) It seems being exactly what I had in mind when I posted my questions. Many thanks!

. also thanks again to others for the other ideas )

1 Like

@sudete well done, does the right thing and is much prettier than my version!

1 Like