Function `apply`, that calls a function with a tuple of arguments?

Julia makes it very easy to map/broadcast a function over a collection of argument tuples. But I don’t think it makes it so easy to map/broadcast multiple functions over one tuple of arguments.

We can do this:

f.((1, 2, 3)) == (f(1), f(2), f(3))

Is there a way to do this?

(f, g, h).(1) == (f(1), g(1), h(1))

Obviously this syntax can’t be made to work; it errors with MethodError: objects of type Tuple{...} are not callable. But there should be some “apply” function (or is there, and I just can’t find it). Something like this would make sense to me:

apply(f::Function, args::Tuple) = f(args...)
@assert apply.((f, g, h), Ref(args)) == (f(args), g(args), h(args))
1 Like

You can use the pipe operator for this:

julia> f, g, h = sin, cos, tan
(sin, cos, tan)

julia> 1 .|> (f, g, h)
(0.8414709848078965, 0.5403023058681398, 1.5574077246549023)
7 Likes

Like it, but I then needed horizontal orientation (matrix) for vector input

julia> [1 2 3] .|> (sin, cos, tan)
3×3 Matrix{Float64}:
 0.841471   0.909297   0.14112
 0.540302  -0.416147  -0.989992
 1.55741   -2.18504   -0.142547

As alternative one could use a comprehension:

julia> apply(funs, x) = [f.(x) for f in funs]
julia> apply((sin, cos, tan), [1,2,3])
3-element Vector{Vector{Float64}}:
 [0.8414709848078965, 0.9092974268256817, 0.1411200080598672]
 [0.5403023058681398, -0.4161468365471424, -0.9899924966004454]
 [1.5574077246549023, -2.185039863261519, -0.1425465430742778]

we must have a previous discussion or even github issue on this I swear

What do you mean? You can do

1.7.0> [1,2,3] .|> (sin, cos, tan)
3-element Vector{Float64}:
  0.8414709848078965
 -0.4161468365471424
 -0.1425465430742778

or

1.7.0> (1,2,3) .|> (sin, cos, tan)
(0.8414709848078965, -0.4161468365471424, -0.1425465430742778)

these are both columns, what if they want the result of broadcast to be a row?

What do you mean? Isn’t it obvious how to get that?

That’s not the same and so depends on what you want.
In the example, we have 3 functions and 3 input values.
I expect 3x3 output values

Yes, but you seemed to indicate that there was a problem.

But you don’t need horizontal orientation for vector input, you need it for matrix output.

how?

julia> [1,2,3] .|> [sin, cos, tan]
3-element Vector{Float64}:
  0.8414709848078965
 -0.4161468365471424
 -0.1425465430742778

julia> [1,2,3] .|> permutedims([sin, cos, tan])
3×3 Matrix{Float64}:
 0.841471   0.540302   1.55741
 0.909297  -0.416147  -2.18504
 0.14112   -0.989992  -0.142547

this doesn’t work, we want a row, not a matrix

A row is a matrix. But anyway:

1.7.0> [1 2 3] .|> [sin cos tan]
1×3 Matrix{Float64}:
 0.841471  -0.416147  -0.142547

1.7.0> ([1, 2, 3] .|> [sin, cos, tan])'
1×3 adjoint(::Vector{Float64}) with eltype Float64:
 0.841471  -0.416147  -0.142547

I’m sure there are more ways.

I mean, how is this different from any other examples of broadcasting?

Although apply or funcall are not as useful in Julia as in some lisps, I sometimes miss them.

An example (completely different from the OP) is that I sometimes wish we could write function barriers like so:

arr = Any[1, 2, 3]  # elements of type unknown to the compiler

apply(arr[1]) do x
    # This anonymous function will get compiled & specialized for typeof(x)
    for _ in 1:1_000_000
        do_something_with(x)
    end
end

You can easily define your own “apply” function — the syntax f <| x is good for this:

julia> f <| x = f(x)
<| (generic function with 1 method)

julia> (sin, cos, tan) .<| [1, 2, 3]
3-element Array{Float64,1}:
  0.8414709848078965
 -0.4161468365471424
 -0.1425465430742778

julia> (sin, cos, tan) .<| π
(1.2246467991473532e-16, -1.0, -1.2246467991473532e-16)
1 Like

I’d support this being in Base. In fact I find this a more natural (if a tiny bit more verbose) syntax for function calling than parentheses. I think it already meshes well with the rest of Julia. Broadcasting currently looks like f.([1,2,3]) or map(f, [1,2,3]) (not quite the same as broadcasting, but sometimes equivalent). In comparison,

  1. The relationship between args |> f (left associative) and f <| args (right associative) is intuitive.
  2. If you know about both + and <|, then it’s easy to extrapolate from [a,b,c] .+ [1,2,3], to [f,g,h] .<| [1,2,3], and then to [f g h] .<| [1,2,3] (for a matrix of all pairs).
  3. There would be a uniform syntax for choosing the direction (LTR or RTL) in which function calls are written.