-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
Broadcasting tuple functions #22129
Comments
Personally I am a fan of proposal B, not least because the other features it would introduce ( |
Currently you can do julia> (t::Tuple{F,G}){F,G}(x) = (t[1](x), t[2](x))
julia> (sin, cos)(0)
(0.0,1.0)
julia> (sin, cos).([-1, 0, 1]) # broadcast((sin, cos), [-1,0,1])
3-element Array{Tuple{Float64,Float64},1}:
(-0.841471,0.540302)
(0.0,1.0)
(0.841471,0.540302) |
Right, but that's not the result you'd want. The point of this is to return a tuple of arrays instead of an array of tuples. |
Oh, right, I missed the intention of the OP. So the example I gave above is probably a reason not to choose the Proposal A approach. |
I have to confess I kind of like the idea of tuples being callable by calling each of their components on the given arguments. But I suspect others may not care for that behavior and would prefer that an operator have the effect of "tupling" functions, i.e. funprod(f::Function, g::Function) = (args...)->(f(args...), g(args...) Part of the reason this is appealing is that there doesn't seem to be anything else that calling a tuple could sensibly mean and the function which returns the tuple of the result of calling each element of the tuple on a given set of arguments is the correct product in the category of functions. |
I agree there should be an efficient and convenient solution for this problem. Some function return multiple values. |
I like Proposal A, |
It would be great to have a syntax which can "unzip" tuples from a single function, eg like |
I'm not convinced you need a "syntax" (i.e. language changes) for this. For example, It would be great if someone worked on a "TupleCast" package for this sort of thing. It's a fair amount of work only because |
Of course, if "tuplecasting" is sufficiently successful, it might get inducted into Base, but it's better to experiment with this sort of thing outside of Base first where it won't get set in stone too quickly. |
I would argue against this for I also think this is just kinda niche. If one wanted to broadcast, defining the scalar operation of calling things, then broadcastinmg that would do fine, no?
|
It would be kind of pleasant though, if |
I love this. I intuitively tried the tuple version, found it didn’t work, and found this thread. Here’s a more generic implementation: > funprod(functions...) = (args...)->map(x->x(args...), functions)
> funprod(sin,cos,tan)(0)
(0.0, 1.0, 0.0) I like the idea of this being equivalent to |
this is broadcasting function tuples, not tuple functions |
I woulda thought |
is this the place where pipe finally shines?
edit, thanks @giordano . So we want |
I don't think so, since the point of the feature requested is to broadcast tuples/arrays in tuple functions: julia> (2, 3) .|> (sin, cos)
(0.9092974268256817, -0.9899924966004454) which is hardly what people want here |
It's not obvious to me what |
It seems obvious to me that it would do |
IMHO, as it is now, the behavior of Broadcast with several outputs is quite useless. The generated array of tuples breaks somehow the logic and always needs allocations. Consider a simple elemental function with the signature:
If I want to evaluate this function in-place in a vectorized manner for two arrays
However, |
The fact that ( x .|> f ) !== ( f.(x) ) when [sin cos].([1,2,3,4,5]) I would also prefer |
I'm in complete agreement with #22129 (comment). This fact that you can't return a tuple of arrays causes many headaches for functions that compute on tensors. It's a major weak point in the language and I hope that it gets addressed. In general, I try to avoid returning tuples because of the way broadcasting on functions returning tuples currently works. But it often results in awkward library design. The alternative is to return the array of tuples (AoT), reallocate, and pay the performance penalty. Is there some intermediate, best-practice workaround for this? |
There is StructArrays, with which @piever showed me this: using StructArrays
function unzip_broadcast(f, args...)
bc = Broadcast.instantiate(Broadcast.broadcasted(f, args...))
StructArrays.components(StructArray(bc))
end
unzip_broadcast((x, y) -> (x, x/y), [1,2,3], [4 5])
# ([1 1; 2 2; 3 3], [0.25 0.2; 0.5 0.4; 0.75 0.6]) |
so broadcasting lazystack over unzip_broadcast achieves what is desired: I can generate a tuple of arrays from broadcasting over a function with multiple return values and then stack each array of the tuple. However, this is completely non-obvious and these helper packages are sufficiently sophisticated such that I don't understand the performance implications. But the code is less verbose. I also feel there is a gap in Julia where manipulating tensors should be more straightforward. Thanks again for the help! |
@prittjam I had made this a while back (before 1.0). https://github.com/spalato/Destruct.jl It worked in 1.0. It presumably still works. It's not optimal as it does work from the array of tuples. However, it is reasonably effective, preserves array shape, works with mixed types. Has no dependencies outside Base. The function has 10 lines of code. |
I can't say I love the idea of making It seems like most of the desire for this functionality would be satisfied by a variant of |
Any updates on how best to make broadcasting on a function that returns a Tuple to give a Tuple of Arrays instead an Array of Tuples? |
there are a lot of ways. one might be
|
@bclyons12: I am not aware of any solution that avoids interim allocations (and results in separate objects, not a view). I think that this should be implemented first in a l package though, following the techniques of |
Probably the most straightforward solution is to use StructArrays. It's a very popular and stable package. julia> A = StructArray(x=[1,2], y=[3,4])
julia> f(a) = (u=a.x+a.y, v=a.x-a.y)
julia> B = f.(A)
2-element StructArray ...
# actual arrays, not views
julia> B.u
2-element Vector{Int64}:
4
6 |
After reading several recent questions about
divrem
andsincos
(not in Base) broadcasting on arrays, I think the current situation could be improved. To summarize, the issue arises when trying to computesin.(A), cos.(A)
(whereA
is a vector) in an efficient way. (minmax
,extrema
,divrem
,fldmod
,fldmod1
,reim
, are similar)The approach with
tmp = sincos.(A); first.(tmp), last.(tmp)
is unsatisfactory because it requires an intermediate allocation.The approach with
sin.(A), cos.(A)
is unsatisfactory because there may be efficiency gains from computingsin
andcos
at once. I suspect some of the desire for an unzip function is caused by this problem, which has no convenient solution at the moment, aside from writing out the loop. Thinking about this problem, I have a few proposals.Proposal A
It may make sense to allow
(sin, cos).(A)
or
broadcast((sin, cos), A)
which returns
sin.(A), cos.(A)
but perhaps computed more efficiently. (In other words, an "unzipped broadcast".)However this raises the question about whether
sincos(x)
anddivrem(x, y)
themselves might better be called(sin, cos)(x)
...Proposal B
If this syntax is too revolutionary, an alternative is to offer
unzip
(#13942), along with a way to make the inner broadcast lazy. That is, perhaps we could havewhich expands to
where
Broadcast
is a minimal iterable object which callsbroadcast
uponcollect
, butunzip
on it can be specialized to avoid the intermediate allocation.Here I'm proposing
@.
as an idiom for a lazy broadcast; this has a reasonably nice parallel to@[...]
for lazy indexing (i.e. viewing). This approach has the advantage that lazy broadcasts have themselves been a commonly requested feature.(Now,
unzip
might itself be reasonably expected to be lazy, so we might wish to requirecollect(unzip(sincos@.(A)))
.)Proposal C
Ideally
sin.(A), cos.(A)
would be as efficient as computingsincos.(A)
. It may be possible to intercept this pattern in lowering, and lower the construction of tuples that are broadcast over the same symbol arguments toBase._tuple_broadcast((sin, cos), A)
which by default falls back tobroadcast(sin, A), broadcast(cos, A)
, but allows specialization. I don't particularly like this kind of solution, because it does break referential transparency.The text was updated successfully, but these errors were encountered: