I found myself needing a Scalar
type for broadcasting purposes, to trick broadcast
into _not_ trying to broadcast something, even though it is a matrix. An initial (bad) implementation is here.
With https://github.com/JuliaLang/julia/pull/16986 close to merging, I wonder if such a feature would be desirable in Base
.
@TotalVerb, can you give an example of where you needed this?
Wouldn't storing something in a 0-dimensional array do the trick?
After #16986 you might also wrap the array with a tuple as pointed out here https://github.com/JuliaLang/julia/issues/17411#issuecomment-236414603.
That's a good point; this Scalar
type is just a one-element tuple. The only difference is that I envision Scalar(1) .+ 1
to be 2
, whereas (1,) .+ 1
to be (2,)
. That is to say, Scalar
would be a zero-dimensional tuple, instead of a one-dimensional tuple with one element.
@stevengj The current implementation of Scalar
is basically just a 0-dimensional array, but it's a little slower than necessary because of the extra allocation.
@stevengj An example of where I needed this is to support broadcasting for Basket
, which are a collection of currencies. It is useful to support both broadcasting over the individual instruments within the collection, and to support broadcasting the collections as if they were instruments in their own right.
Another example is given in the README for Scalars.jl.
(An aside: I don't think you need to define show(io, "text/plain", x)
if you just want it to call show(io, x)
, as the former calls the latter by default.)
@stevengj It's to override the method inherited from AbstractArray
.
Oh, right.
A simpler solution, that wouldn't require us to define any new types, is for broadcast
to treat Ref(x)
as a 0-dimensional array of x
. This seems like something we should do anyway, because that's effectively what Ref
is, and it seems relatively easy to add once #16986 is merged.
Related: https://github.com/JuliaLang/julia/issues/18271#issuecomment-245276731, wherein I proposed using Ref(x)
with the syntax &x
as a means of not dropping a scalar dimension when slicing or reducing. Of course, that entails considering Ref
to be a 1-dimensional container, whereas this entails considering it to be a 0-dimensional container.
I'm worried that the compiler may not be able to eliminate the allocation of a Ref.
@StefanKarpinski, since you access a ref with ref[]
, it doesn't make sense to me to treat it as anything other than a 0-dimensional array. (Of course, getindex
can always implement whatever method it wants for a Ref
argument if you want magic handling of &x
.)
@TotalVerb, in the rare cases where the heap allocation of a Ref
object would matter compared to the cost of a broadcast
over arrays of arrays, presumably in some kind of tight inner loop, couldn't you simply allocate the Ref
outside the loop and mutate it as needed?
Put another way, I tend to think that Ref
should be treated as a 0-dimensional array in broadcast
_anyway_, regardless of whether we want a Scalar
type too. My inclination would be to add Ref
support in broadcast
first, and then see if the performance really is a practical annoyance before we add another type.
@stevengj I think that's a very sane approach.
FYI I'm working on a _StaticArrays_ implementation over at JuliaArrays/StaticArrays.jl#50 (it made sense there since this is basically an immutable version of Array{T,0}
and won't have that associated allocation penalty).
I'd really like this before 1.0; I'll try to make a PR soon. Ref
and (x,)
do not work for many use cases because they still take priority over Nullable
. The Scalar
above also implicitly arrayifies the argument.
julia> Nullable(4) .+ (5,)
ERROR: MethodError: no method matching +(::Nullable{Int64}, ::Int64)
Closest candidates are:
+(::Any, ::Any, ::Any, ::Any...) at operators.jl:424
+(::Complex{Bool}, ::Real) at complex.jl:239
+(::Char, ::Integer) at char.jl:40
...
Stacktrace:
[1] _ntuple at ./tuple.jl:135 [inlined]
[2] ntuple(::Base.Broadcast.##3#4{Base.#+,Tuple{Nullable{Int64},Tuple{Int64}}}, ::Type{Val{1}}) at ./tuple.jl:128
[3] broadcast(::Function, ::Nullable{Int64}, ::Tuple{Int64}) at ./broadcast.jl:434
Aside from being a problem with Nullable
already, other broadcast types presumably also might want the ability to force arguments to not be treated as an array.
Also, while Ref
wrapper solution from #18965 does work without Nullable
, it seems to me that it is quite slower than it needs to be... Comparing two ways to do e.g. ((1,1),(2,2),(3,3)) .== &(3,3)
:
julia> f1(x, y) = x .== Ref(y)
f1 (generic function with 1 method)
julia> function f2(x, y)
r = BitArray(length(x))
for (i,e) in enumerate(x)
r[i] = e==y
end
return r
end
f2 (generic function with 1 method)
julia> @btime f1(((1,1),(2,2),(3,3)), (3,3))
399.085 ns (4 allocations: 4.34 KiB)
3-element BitArray{1}:
false
false
true
julia> @btime f2(((1,1),(2,2),(3,3)), (3,3))
34.815 ns (2 allocations: 128 bytes)
3-element BitArray{1}:
false
false
true
Confusion about Ref
and Scalar
has cropped up a bit on Discourse. See:
https://discourse.julialang.org/t/how-to-select-multiple-items-using-dataframesmeta/16231/2
Just echoing that it would be nice if we had a Scalar
function which worked like Ref
with the benefit of having a more intuitive name. I understand this is a 1.x possibility.
Note that one option we've discussed, without having a new type, is to define &x
as sugar for a ref (#27608), which would make it easier to escape from broadcasting.
Here's a minimal implementation of a Scalar
type. I think there's an argument to be made that such a thing would be preferable to Ref
since it's immutable and will be elided by the compiler more easily.
Is there still interest in putting such a thing Base? I could try opening a PR if people are interested.
Most helpful comment
Here's a minimal implementation of a
Scalar
type. I think there's an argument to be made that such a thing would be preferable toRef
since it's immutable and will be elided by the compiler more easily.Is there still interest in putting such a thing Base? I could try opening a PR if people are interested.