Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tuple of callables should be callable #32682

Closed
jessebett opened this issue Jul 25, 2019 · 4 comments
Closed

Tuple of callables should be callable #32682

jessebett opened this issue Jul 25, 2019 · 4 comments

Comments

@jessebett
Copy link
Contributor

I would like to propose allowing tuples of functions to be be callable and have the syntax:

(f,g)(x) = (f(x),g(x))

At the moment this is disallowed with an error, e.g.:

x = collect(1:10);
(minimum,maximum)(x)
julia> ERROR: MethodError: 
objects of type Tuple{typeof(minimum),typeof(maximum)} are not callable
Stacktrace:
 [1] top-level scope at REPL[7]:1

Another use for this would be to unpack the keys and values of a dictionary in a single line:
k,v = (keys, values)(mydict::Dict)

  1. I do not know where callable is specified. Searching the docs for callable or in the help ?callable does not give anything concrete about how to allow something to be callable.

  2. How should broadcasting work? I think a reasonable proposal is (f,g).(x) == (f.(x), g.(x)). Is this ambiguous or not desirable for a reason I cannot foresee?

  3. Since f and g would be accessing the same input, and likely be doing similar operations, this introduces a performance issues. It makes it really easy to write non-performant code. My assumption was that there would be a lot of duplicated work if e.g. f and g both look through x separately, they should know to share common work.

This was motivated by the (minimum,maximum)(x) example. There is an existing function for this, extrema which should return the same result faster:

using BenchmarkTools

(T::Tuple{typeof(minimum),typeof(maximum)})(x) = (minimum(x),maximum(x))

x = rand(10^3);
@btime (minimum,maximum)($x)
@btime extrema($x)

On Julia-1.1:

julia> @btime (minimum,maximum)($x)
  4.593 μs (0 allocations: 0 bytes)
(0.0010074908627801804, 0.9977385773243437)

julia> @btime extrema($x)
  2.649 μs (0 allocations: 0 bytes)
(0.0010074908627801804, 0.9977385773243437)

So you can see that this callable tuple is twice as slow. EXCEPT!

On Julia-1.3:

julia> @btime (minimum,maximum)($x)
  1.835 μs (0 allocations: 0 bytes)
(1.8361594563476302e-5, 0.9999551648541347)

julia> @btime extrema($x)
  2.651 μs (0 allocations: 0 bytes)
(1.8361594563476302e-5, 0.9999551648541347)

Ummm. So now the tuple (minimum,maximum) is faster than extrema? I don't understand this. Some threading magic?

Anyway I think this is a general problem, is the compiler smart enough to let f and g know about the shared work between f and g and use them together?

I was going to propose explicitly dispatching to more efficient joint methods, e.g. (T::Tuple{typeof(minimum),typeof(maximum)})(x) = extrema(x) but not only is this ugly, very specific, and hopefully could be automatically made efficient by the compiler. It is also, as demonstrated above, less efficient than (minimum(x),maximum(x)) currently!

  1. I don't know what to do in the case that f mutates x, or if we should do anything. If it is literally the case that (f,g)(x) = (f(x),g(x)) then I guess the order of these two matter. But if its possible to do these two functions in parallel, it might be a problem. I also don't know how that might affect shared work.

I would like to help implement this. I think it looks like setting up methods that look like

(T::Tuple{f::Callable,g::Callable})(x) = (f(x),g(x))

But as I said at the top, I don't know what decides if a type is callable.

@jessebett
Copy link
Contributor Author

So this looks like an issue with extrema actually. (minimum,maximum)(x) is twice the sum of the times for minimum(x) and maximum(x). extrema is 4 times as long.

julia> @btime (minimum,maximum)($x)
  1.837 μs (0 allocations: 0 bytes)
(0.00045425045471247927, 0.9998863001976936)

julia> @btime extrema($x)
  2.654 μs (0 allocations: 0 bytes)
(0.00045425045471247927, 0.9998863001976936)

julia> @btime minimum($x)
  915.886 ns (0 allocations: 0 bytes)
0.00045425045471247927

julia> @btime maximum($x)
  917.722 ns (0 allocations: 0 bytes)
0.9998863001976936

@jessebett
Copy link
Contributor Author

Apparently this is due to #31442

@andyferris
Copy link
Member

Just brainstorming... what if broadcast also broadcasted the function argument?

E.g. (f,g).(x) = (f(x), g(x)) when x is "scalar".

(I realize this is a bit annoying when you want to do minimum or maximum over x being a collection where you would need to wrap x for example like (f,g).((x,))).

@fredrikekre
Copy link
Member

Duplicate of #22129

@fredrikekre fredrikekre marked this as a duplicate of #22129 Jul 26, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants