Broadcasting over combinations of inputs

I’m looking for a design pattern that will let me broadcast a function over two input lists, returning a 2D array of the output of the function for all possible pairs from the two lists.

In other words if my input arrays are:

x = [1, 2, 3]
y = ["a", "b"]

I’d like a succinct way to calculate

[ f(1, "a") f(2, "a") f(3, "a";
  f(1, "b") f(2, "b") f(3, "b") ]

for an arbitrary function f() of two inputs.

I’ve played with Iterators.product a little, but I’m not sure it does exactly what I’d like.

julia> (+).(collect(Iterators.product(1:4, [1, 2])))
ERROR: MethodError: no method matching +(::Tuple{Int64,Int64})

julia> (+).(collect(Iterators.product(1:4, [1, 2]))…)
(20, 12)

I should add that I’m looking for a performant way to do this for my application, so anything that makes additional copies of the data won’t work.

Are there any neat Julia tricks folks know of to do this without a for loop?

Try

f.(permutedims(x), y)

or

[f(x,y) for y in y, x in x]

I think both should be pretty much optimal performance.

3 Likes

I know you specifically asked about two inputs, but just as a general note, you can combine your idea of Iterators.product and @marius311’s use of comprehensions for any number of arguments:

[f(args...) for args in Iterators.product(x, y, z, a, b, c)]

would give you the 6-dimensional cube over all combinations of arguments.

3 Likes

Thank you both! This is exactly what I was looking for.

1 Like

I’d like to point out to anyone referencing this that replacing

f.(permutedims(x), y)

with

f.(x', y)

does not work. It errors when x is a Vector of length one. It is trying to take the Adjoint (transpose) of the element itself in that case. I’m not sure if this is a bug or is intentional.

Its intentional, adjoint is recursive like the mathematical construct, while permutedims is not (that was why I used the latter). I think this has some discussion of why, if I remember right. Fwiw, I don’t think vector length matters, e.g. ["a","b"]' is also an error.