Why are returned types different in comprehension over an empty range if called from a function vs top level?

julia> using ApproxFun

julia> rs = Jacobi(5,4);

julia> d = domain(rs);

julia> Sf = JacobiWeight(0,2,ConstantSpace(d));

julia> using ApproxFun.ApproxFunSingularities: jacobiweight

julia> using ApproxFun.ApproxFunBase: ConcreteMultiplication

julia> min(rs.b, Sf.β)-1:-1:1 # empty range
-1:-1:0

julia> [ConcreteMultiplication(jacobiweight(1,0,d), Jacobi(rs.b-i,rs.a,d)) for i in min(rs.b, Sf.β)-1:-1:1]
Any[]

julia> ((rs,Sf,d) -> [ConcreteMultiplication(jacobiweight(1,0,d), Jacobi(rs.b-i,rs.a,d)) for i in min(rs.b, Sf.β)-1:-1:1])(rs,Sf,d)
ConcreteMultiplication{JacobiWeight{ConstantSpace{ChebyshevInterval{Float64}, Float64}, ChebyshevInterval{Float64}, Float64, Int64}, Jacobi{ChebyshevInterval{Float64}, Float64, Int64}, Float64}[]

julia> VERSION
v"1.9.3"

Why are the types different? The second call is identical to the first, except this time this is placed in a function. My impression was that this might help with type-inference, but won’t change the actual types that are returned.

Edit: On second thought, perhaps the inferred type (using @default_eltype) is used to allocate the container, which is a bit unfortunate

julia> G =(ConcreteMultiplication(jacobiweight(1,0,d), Jacobi(rs.b-i,rs.a,d)) for i in min(rs.b, Sf.β)-1:-1:1)
Base.Generator{StepRange{Int64, Int64}, var"#15#16"}(var"#15#16"(), -1:-1:0)

julia> Base.@default_eltype(G)
Any

julia> ((rs,Sf,d) -> (G = (ConcreteMultiplication(jacobiweight(1,0,d), Jacobi(rs.b-i,rs.a,d)) for i in min(rs.b, Sf.β)-1:-1:1); Base.@default_eltype(G)))(rs,Sf,d)
ConcreteMultiplication{JacobiWeight{ConstantSpace{ChebyshevInterval{Float64}, Float64}, ChebyshevInterval{Float64}, Float64, Int64}, Jacobi{ChebyshevInterval{Float64}, Float64, Int64}, Float64}
1 Like

Comprehensions create closures, which capture variables. Creating limited scopes, such as a let block or a small function help to avoid making them mutable Any boxes that can’t infer.

You can try a let block around the comprehension as well to see if it also infers instead of Any[]. I expect it to.

My idea was to copy-paste existing codes to the REPL for quick testing, but looks like that won’t work

Yes, that’s unfortunately common when closures are involved.
Although for me, normally it is that code works in the REPL, but not inside the scope of an @testset, as the latter causes some variable defined after the closure to get captured, producing incorrect results (while later definitions of course can’t be captured in the REPL).