Is it good practice to have a variable binding change type in Julia programming?

Is the following good practice and why?

x = [rand(3,3) for i in 1:10]
x = sum(x)

The first assignment binds x to Array{Matrix} and the second to Matrix. The type of the object x is bind to changes in the code. Code is for illustration only and of course I can do x=sum(rand(3,3) for i in 1:10) but often due to readability I want to break the logic.

Many thanks.

I don’t think it’s good practice because for a reader who finds x later in the code, they might skim the code for the origin of that variable and find only line 1, but not line 2. As that could be confusing I try to avoid variable rebinding. Also, if one name can describe two different things, it’s probably not a good name.

2 Likes

That’s called type instability and inhibits generation of efficient code. Performance Tips Β· The Julia Language

4 Likes

I was not expecting this, but actually this does can introduce problems (is this a bug?):

julia> function f()
         x = 1
         x = [ x for i in 1:2 ]
         return x
       end
f (generic function with 1 method)

julia> @code_warntype f()
Variables
  #self#::Core.Const(f)
  #5::var"#5#6"
  x@_3::Core.Box
  x@_4::Union{}

Body::Any
1 ─       (x@_3 = Core.Box())
β”‚         Core.setfield!(x@_3, :contents, 1)
β”‚         (#5 = %new(Main.:(var"#5#6"), x@_3))
β”‚   %4  = #5::var"#5#6"
β”‚   %5  = (1:2)::Core.Const(1:2)
β”‚   %6  = Base.Generator(%4, %5)::Core.PartialStruct(Base.Generator{UnitRange{Int64}, var"#5#6"}, Any[var"#5#6", Core.Const(1:2)])
β”‚   %7  = Base.collect(%6)::Vector{_A} where _A
β”‚         Core.setfield!(x@_3, :contents, %7)
β”‚   %9  = Core.isdefined(x@_3, :contents)::Bool
└──       goto #3 if not %9
2 ─       goto #4
3 ─       Core.NewvarNode(:(x@_4))
└──       x@_4
4 β”„ %14 = Core.getfield(x@_3, :contents)::Any
└──       return %14


while:

julia> function f()
         x = 1
         y = [ x for i in 1:2 ]
         return y
       end
f (generic function with 1 method)

julia> @code_warntype f()
Variables
  #self#::Core.Const(f)
  #7::var"#7#8"{Int64}
  y::Vector{Int64}
  x::Int64

Body::Vector{Int64}
1 ─      (x = 1)
β”‚   %2 = Main.:(var"#7#8")::Core.Const(var"#7#8")
β”‚   %3 = Core.typeof(x::Core.Const(1))::Core.Const(Int64)
β”‚   %4 = Core.apply_type(%2, %3)::Core.Const(var"#7#8"{Int64})
β”‚        (#7 = %new(%4, x::Core.Const(1)))
β”‚   %6 = #7::Core.Const(var"#7#8"{Int64}(1))::Core.Const(var"#7#8"{Int64}(1))
β”‚   %7 = (1:2)::Core.Const(1:2)
β”‚   %8 = Base.Generator(%6, %7)::Core.Const(Base.Generator{UnitRange{Int64}, var"#7#8"{Int64}}(var"#7#8"{Int64}(1), 1:2))
β”‚        (y = Base.collect(%8))
└──      return y

I am not sure if I agree or understand this in this way. My impression is that, in the code above, the types should be perfectly inferreable in both cases, and the variable label should be irrelevant.

1 Like

Yes, I expect that is true in this example, but I would err on the side of avoiding changing the type of a variable so you don’t need to analyze those lower level details to see whether everything is inferred stably.

3 Likes

Thanks for your answer; they are all good.

Indeed, initially I thought that it was only not recommendable for code readability, but it can make more harm than that (maybe this instability is something related to the garbage collection?).