Overwriting matrix elements of a different type

Suppose I have this matrix:

M = [1 2 3; 4 5 6];

The type of M is Int64, but now I want to update one of the values to a non-integer:

M[1] = pi

Of course, the above just produces this error:

ERROR: MethodError: no method matching Int64(::Irrational{:π})
Closest candidates are:
  (::Type{T})(::T) where T<:Number at boot.jl:772
  (::Type{T})(::AbstractChar) where T<:Union{Int32, Int64} at char.jl:51
  (::Type{T})(::AbstractChar) where T<:Union{AbstractChar, Number} at char.jl:50
  ...
Stacktrace:
 [1] convert(#unused#::Type{Int64}, x::Irrational{:π})
   @ Base ./number.jl:7
 [2] setindex!(A::Matrix{Int64}, x::Irrational{:π}, i1::Int64)
   @ Base ./array.jl:966
 [3] top-level scope
   @ REPL[21]:1

So what’s the recommended method of updating elements in a matrix that may be of a different type than the matrix itself?

1 Like

It’s not possible unless the matrix’s type is supertype. There’s lots of reasons it wouldn’t work:

  1. Different types might require different amounts of memory.
  2. Knowing the type of each element now isn’t a property of the array, but of the element, so every element has to carry a type tag.

If you plan to use arrays this way, probably best to use Matrix{Any}.

Well, one way to accomplish the goal is to convert M manually and then update the elements, like

M = [1 2 3; 4 5 6];
M = convert(Matrix{Float64},M);
M[1] = pi; 

Is there a more elegant solution?

Be careful that those Ms are not the same matrix, meaning that mutations in the later won’t be reflected in the previous M, if for instance it was an input of a function, intending to mutate it.

You have just assigned the M label to two different matrices.

Whoa whoa whoa, so I now have two M matrices? I thought I effectively deleted the first one when I converted it to Float64 and called it by the same name.

The Julia workspace only shows one M, so where’d the first one go?

You only have one matrix, Leandro’s point was that if you do this in a function, the input would not be modified as you are allocating a new matrix when you change the type:

julia> function change_element(m)
           m = float.(m)
           m[1, 1] = π
           m
       end
change_element (generic function with 1 method)

julia> x = [1 2; 3 4]
2×2 Matrix{Int64}:
 1  2
 3  4

julia> change_element(x)
2×2 Matrix{Float64}:
 3.14159  2.0
 3.0      4.0

julia> x
2×2 Matrix{Int64}:
 1  2
 3  4
2 Likes

Setfield.jl can do this:

julia> using Setfield

julia> x = [1 2; 3 4]
2×2 Matrix{Int64}:
 1  2
 3  4

julia> @set! x[1] = pi;

julia> x
2×2 Matrix{Float64}:
 3.14159  2.0
 3.0      4.0

Note that this is just makes a copy of the matrix for you promoted to Float64 before setting the element, so depending on your usecase, using a Matrix{Real} or something like that could be better.

What you did is to assign the label M to a new matrix. The previous matrix, if not referenced by any other label, can’t be accessed anymore, and will be garbage-collected when GC runs.

If you do have another label/reference to that matrix, it is still there, and is not mutated:

julia> a = [1 2; 3 4]
2×2 Matrix{Int64}:
 1  2
 3  4

julia> m = a;

julia> m = convert(Matrix{Float64}, m);

julia> m[1,1] = π
π = 3.1415926535897...

julia> m
2×2 Matrix{Float64}:
 3.14159  2.0
 3.0      4.0

julia> a
2×2 Matrix{Int64}:
 1  2
 3  4

(note also that Setfield is not doing anything different from this). The point is: you cannot mutate the field of a matrix to a Float64 whose memory layout is such to store only Int64. You have to create a new matrix, or set the matrix to support both types from start, either using Matrix{Real}[] or Matrix{Uniont{Int,Float64}}, or similar, but that comes with an important performance penalty, which might be relevant or not depending on what you are doing.

Not sure whether or not this is elegant, but it does reuse the same matrix:

function int2float(a::Array{Int})
    maxa = 2^53
    for i in eachindex(a)
        abs(a[i]) > maxa && error("a[", i, "] = ", a[i], " not exactly representable by Float64")
        a[i] = reinterpret(Int, float(a[i]))
    end
    return reinterpret(Float64, a)
end

It’s used like this:

julia> x = [1 2 3; 4 5 6]
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> y = int2float(x)
2×3 reinterpret(Float64, ::Matrix{Int64}):
 1.0  2.0  3.0
 4.0  5.0  6.0

julia> y[1] = pi; y
2×3 reinterpret(Float64, ::Matrix{Int64}):
 3.14159  2.0  3.0
 4.0      5.0  6.0

I legit think OP might just be looking for this:

julia> M = Float64[1 2 3; 4 5 6];

julia> M[1] = pi;

julia> M
2×3 Matrix{Float64}:
 3.14159  2.0  3.0
 4.0      5.0  6.0

i.e. the solution is to initialize the Matrix with a more appropriate type (technically, the one that promotes to

julia> promote_type(Int64, Float64, Irrational)
Float64

)

6 Likes

This is actually quite a fun solution, but I think it requires a bit of explanation.

I also agree with @jling that it’s probably an XY-problem.

Thanks! Here’s the explanation: 2^53 is the largest integer for which it and all smaller integers can be exactly represented by the 52-bit mantissa used in a Float64, as discussed in this post. So, after this test, the function first replaces each integer element of a by another integer whose interpretation as a Float64 would produce the same value, then returns an interpretation of the matrix as those Float64 values. However, as discussed in the above link, there are some integers greater than 2^53 which can also be exactly represented by Float64s, and these are not allowed by the function I posted previously. The version below works for any integer exactly representable in Float64 and is probably a little clearer. Warning: with either version of the function, the original Int array is modified so that it can’t be usefully referenced through its original binding.

function int2float(a::Array{Int})
    for i in eachindex(a)
        aif = float(a[i])
        a[i] == aif || error("a[", i, "] = ", a[i], " not exactly representable by Float64")
        a[i] = reinterpret(Int, aif)
    end
    return reinterpret(Float64, a)
end
1 Like

You can also use the function maxintfloat:

julia> maxintfloat(Float64)
9.007199254740992e15

julia> maxintfloat(Float64) == 2^53
true

But I tend to think this is a distraction—if you want to store a floating-point value in any element of the array, then you are probably willing to deal with roundoff errors if you are going to do any calculation at all with the elements. Presumably @chadagreen should have used a floating-point matrix to start with.

For people coming from Matlab, the Matlab arrays are floating-point by default, and Matlab will silently allocate complex-number storage for you if you assign a complex number, and will silently grow the array if you assign past the end of the array. (For people coming from Python, the default Python lists are equivalent to Any[...] arrays in Julia.). Julia arrays, in contrast, default to the type of the elements you initialize them with (unless you explicitly specify another type), and Julia will never convert the whole array to another type or silently resize it without you explicitly requesting it. So, you should think a bit more carefully in Julia about what type of elements you want when the array is created.

Julia is pretty powerful — you can play all kinds of games to optimize code, re-use storage (even using storage from an array of Int64 to store an array of Float64, as in the int2float function above), and so forth. But in most cases the simpler techniques suffice, and on Julia Discourse we can sometimes confuse people by immediately showing off all the tricks.

5 Likes