Reduce + hcat is type unstable

I have observed the following type instability when using reduce + hcat as follows (see examples below). Is this intended? Could it be related to issue#46331?

More context: I am using this syntax, as recommended here., when converting a Vector of scalar or arrays into a higher-dimensional array (e.g., a 128-long vector of 2x3-sized matrices is converted into a 2x3x128 array).

julia> reduce(hcat, ones(4))
1×4 Matrix{Float64}:
 1.0  1.0  1.0  1.0

julia> reduce(hcat, ones(1))
1.0

julia> @code_warntype reduce(hcat, ones(4))
MethodInstance for reduce(::typeof(hcat), ::Vector{Float64})
  from reduce(op, A::AbstractArray; kw...) @ Base reducedim.jl:406
Arguments
  #self#::Core.Const(reduce)
  op::Core.Const(hcat)
  A::Vector{Float64}
Body::Union{Float64, Matrix{Float64}}
1 ─ %1 = Core.NamedTuple()::Core.Const(NamedTuple())
│   %2 = Base.pairs(%1)::Core.Const(Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}())
│   %3 = Base.:(var"#reduce#802")(%2, #self#, op, A)::Union{Float64, Matrix{Float64}}
└──      return %3

Namely, it seems that reduce(hcat, v) returns a Matrix if v has more than 1 element, but a scalar if v has one element.

Note that, in contrast, using hcat + splatting yields a consistent behavior

julia> hcat(ones(4)...)
1×4 Matrix{Float64}:
 1.0  1.0  1.0  1.0

julia> hcat(ones(1)...)
1×1 Matrix{Float64}:
 1.0

julia> @code_warntype hcat(ones(1)...)
MethodInstance for hcat(::Float64)
  from hcat(X::T...) where T<:Number @ Base abstractarray.jl:1609
Static Parameters
  T = Float64
Arguments
  #self#::Core.Const(hcat)
  X::Tuple{Float64}
Locals
  @_3::Union{Nothing, Tuple{Int64, Int64}}
  @_4::Int64
  @_5::Union{Nothing, Tuple{Int64, Int64}}
  j::Int64
  i::Int64
Body::Matrix{Float64}
# 
# skipping rest of output
# 

I think you want stack.

The earlier magic methods of reduce(hcat, xs) are quite specific, and your example isn’t hitting them. It is just doing hcat(1.0, 1.0), hcat(hcat(1.0, 1.0), 1.0) and so on. Combining elements zero times returns just 1.0.

julia> @which reduce(hcat, ones(4))
reduce(op, A::AbstractArray; kw...)
     @ Base reducedim.jl:406

julia> @which reduce(hcat, eachcol(ones(3,4)))
reduce(::typeof(hcat), A::AbstractVector{<:AbstractVecOrMat})
     @ Base abstractarray.jl:1721

julia> reduce(hcat, ones(1))
1.0

julia> reduce(hcat, ones(2))
1×2 Matrix{Float64}:
 1.0  1.0
1 Like

Thanks! I just independently found out about stack indeed :slight_smile:

Quick note: stack does not seem to work well with Vector{String} input:

julia> stack(["hello", "world!"])
ERROR: MethodError: no method matching size(::String)

Stacktrace:
 [1] axes(A::String)
   @ Base ./abstractarray.jl:98
 [2] _stack_size_check
   @ ./abstractarray.jl:2870 [inlined]
 [3] _typed_stack(::Colon, ::Type{Char}, ::Type{String}, A::Vector{String}, Aax::Tuple{Base.OneTo{Int64}})
   @ Base ./abstractarray.jl:2802
 [4] _typed_stack
   @ ./abstractarray.jl:2793 [inlined]
 [5] _stack
   @ ./abstractarray.jl:2783 [inlined]
 [6] _stack
   @ ./abstractarray.jl:2775 [inlined]
 [7] #stack#178
   @ ./abstractarray.jl:2743 [inlined]
 [8] stack(iter::Vector{String})
   @ Base ./abstractarray.jl:2743
 [9] top-level scope
   @ REPL[7]:1

That’s a bug, I think, but a bug in the error-printing function. stack claims to accept any iterator of iterators, but all the inner ones must have the same size.

julia> stack(["hello", "world"])
5×2 Matrix{Char}:
 'h'  'w'
 'e'  'o'
 'l'  'r'
 'l'  'l'
 'o'  'd'

julia> stack(["hello", "world!"])
ERROR: MethodError: no method matching size(::String)

julia> stack(["hello", "world!"] .|> collect)
ERROR: DimensionMismatch: stack expects uniform slices, got axes(x) == (1:6,) while first had (1:5,)
Stacktrace:
 [1] _stack_size_check
   @ ./abstractarray.jl:2895 [inlined]

julia> @eval Base @inline function _stack_size_check(x, ax1::Tuple)
           if _iterator_axes(x) != ax1
               uax1 = map(UnitRange, ax1)
               uaxN = map(UnitRange, _iterator_axes(x))  # this line had axes(x)
               throw(DimensionMismatch(
                   LazyString("stack expects uniform slices, got axes(x) == ", uaxN, " while first had ", uax1)))
           end
       end
_stack_size_check (generic function with 1 method)

julia> stack(["hello", "world!"])
ERROR: DimensionMismatch: stack expects uniform slices, got axes(x) == (1:6,) while first had (1:5,)
1 Like