StaticArrays.jl icon indicating copy to clipboard operation
StaticArrays.jl copied to clipboard

Compact show for SArray and MArray

Open jishnub opened this issue 4 years ago • 8 comments

Along the lines of https://github.com/JuliaLang/julia/pull/40722, this changes the way show(io, ::SArray) and show(io, ::MArray) displays the contents. The type information printed as a prefix distinguishes these from other dense arrays. Also makes the output valid as a constructor.

After this PR:

julia> show(SVector{2, Int64}((1, 2)))
SVector{2, Int64}((1, 2))

julia> show(MVector{2, Int64}((1, 2)))
MVector{2, Int64}((1, 2))

Edit: Closes #692

jishnub avatar May 07 '21 09:05 jishnub

Hah I was going to have a go at this too, https://github.com/JuliaArrays/FillArrays.jl/pull/144 is the other one...

But I think they can be more compact, at least sometimes. Why not print SA[1, 2, 3]? Where I got lost was in how Base decides whether to print a type in front... I see now there are forms SA_F32[1, 2] and SA{Int32}[1, 2] which would be ideal.

julia> (rand(Int32.(1:99),999),)
(Int32[60, 26, 91, 10, 70, 33, 25, 64, 44, 64  …  20, 15, 21, 6, 39, 67, 18, 18, 84, 47],)

Less sure about the M case, but perhaps MVector(1, 2) is nicer?

mcabbott avatar May 08 '21 02:05 mcabbott

Did we consider SVector{2}([1, 2])? If the eltype isn't obvious it can go on the inner array, like SVector{2}(Any[1, 2]), so effectively reusing the logic applied to Vector in a composable way.

andyferris avatar May 10 '21 10:05 andyferris

Here's a quick attempt to hook into the machinery... @less Base.typeinfo_prefix(stdout, [1,2,3]) is where it decides what prefix to print on other arrays, thus:

julia> function Base.typeinfo_prefix(io::IO, X::SArray)
           typeinfo = get(io, :typeinfo, Any)::Type
           if !(X isa typeinfo)
               typeinfo = Any
           end
           # what the context already knows about the eltype of X:
           eltype_ctx = Base.typeinfo_eltype(typeinfo)
           eltype_X = eltype(X)

               # Types hard-coded here are those which are created by default for a given syntax
               if eltype_X == eltype_ctx
                   "SA", false
               elseif eltype_X == Float32  # added special case
                   "SA_F32", false
               elseif !isempty(X) && Base.typeinfo_implicit(eltype_X)
                   "SA", true
               # elseif print_without_params(eltype_X)  # not sure what this does!
               #    sprint(show_type_name, unwrap_unionall(eltype_X).name), false # Print "Array" rather than "Array{T,N}"
               else
                   string("SA{", eltype_X, "}"), false
               end
       end

julia> (SA[1,2,3], SA{Int32}[1 2; 3 4], SA[1f0, 2f0])
(SA[1, 2, 3], SA{Int32}[1 2; 3 4], SA_F32[1.0, 2.0])

julia> (SA{Int}[], SA{Float64}[], SA[])  # empty
(SA{Int64}[], SA{Float64}[], SA{Union{}}[])

julia> (SVector{2}(Any[1, 2]), SA{Any}[1,2], SVector{3}(Any[1, :a, nothing]))  # first really does have eltype Int
(SA[1, 2], SA{Any}[1, 2], SA{Any}[1, :a, nothing])

julia> repr(@SMatrix rand(10,10); context=(:limit => true, :compact => true))
"SA[0.262792 0.33571 … 0.432945 0.18245; 0.535286 0.995272 … 0.881322 0.00517454; … ; 0.908566 0.738296 … 0.0315535 0.46813; 0.602212 0.843338 … 0.209644 0.31736]"

julia> [SA[0f0,i] for i in 1:3]  # not sure if this should omit eltype? Omit everything? When :compact => true?
3-element Vector{SVector{2, Float32}}:
 SA[0.0, 1.0]
 SA[0.0, 2.0]
 SA[0.0, 3.0]

julia> ([SA[1f0,2f0], SA[3f0,4f0]], [SA[1f0,2f0], SA[3f0,4f0,5f0]]) # arrays of arrays
(SVector{2, Float32}[[1.0, 2.0], [3.0, 4.0]], SArray{S, Float32, 1} where S<:Tuple[SA[1.0, 2.0], SA[3.0, 4.0, 5.0]])

mcabbott avatar May 10 '21 13:05 mcabbott

I'm wondering if SA is would be confusing for people who aren't direct StaticArrays.jl users. We are used by a lot of downstream packages and general Julia users may receive static arrays (I think I saw some from Plots.jl or somewhere like that recently) and be presented (by show) with SA[1,2,3] and therefore expect the element type to be SA.

I realize it is shorter and convenient for developers, but am unconvinced that it's the best choice for show or repr.

andyferris avatar May 11 '21 01:05 andyferris

Much as I like SA, it is a bit of a syntax trick.

You could still minimise down to SVector(1.0, 2.0, 3.0) when the eltype is obvious. But SMatrix{2,2}(1,2,3,4) is confusing as it's [1 3; 2 4]. If you wrap an array, I'm a bit surprised that SVector{2}(Any[1, 2]) doesn't work, I suppose it's like (Any[1,2]...,), so at least for abstract types I guess you'd have to write it in the curly brackets.

mcabbott avatar May 11 '21 02:05 mcabbott

I'm a bit surprised that SVector{2}(Any[1, 2]) doesn't work

Oh, so am I! Even convert is broken, and neither match Base.

julia> SVector{2}(Any[1,2])
2-element SVector{2, Int64} with indices SOneTo(2):
 1
 2

julia> convert(SVector{2}, Any[1,2])
2-element SVector{2, Int64} with indices SOneTo(2):
 1
 2

julia> Vector(Any[1,2])
2-element Vector{Any}:
 1
 2

julia> convert(Vector, Any[1,2])
2-element Vector{Any}:
 1
 2

We should definitely this behavior - so don't feel afraid to implement show this way.

andyferris avatar May 11 '21 02:05 andyferris

Would you ever want an abstract type though? Any[1] you can write 2.5 into, maybe you could want that for an MArray? SVector{2}(Any[1,"β"]) does preserve it, of course.

mcabbott avatar May 11 '21 02:05 mcabbott

In my opinion, it's important to mimic Base as closely as possible, to help people write generic code and avoid surprises when using StaticArrays.

While this is more important for MArray due to setindex!, there's also the common issue of arrays with eltype Union{T, missing} and making life easy for inference in these cases, or conforming to some storage container which you want store and retrieve such values (like with a Vector{SVector{3, Union{Float64, Missing}}} - individual elements could be entirely Float64 or entirely Missing or a mixture).

andyferris avatar May 11 '21 23:05 andyferris