How to design with immutable and also mutable type?

suppose I have a parametric type as follows:

struct A{T <: AbstractVector{Float32} }
    data::T
end

setdata!(a::A, x) = (a.data .= x)

here T could be Vector{Float32} or a view etc.

however, I would like to use SVector (in StaticArrays) also. But as I need to mutate the contents of data over time, I need to define another struct which is mutable like:

mutable struct B{T <: AbstractVector{Float32} }
    data::T
end

setdata!(b::B, x) = (b.data = x)

so, my question is: is there a more beautiful way to do this? maintaining a immutable type along with another mutable type seems uglyā€¦ and the behaviors of A and B are basically identical, except for setdata!().

another question is: if I just use (the mutable) B for all cases, would it be stupid because of the performance penalty caused by mutable types?

thanks.

Look at ArrayInterface.jl on detecting mutability. Iā€™ll write an example once Iā€™m not on a phone.

1 Like

Can I ask why you donā€™t want to make A mutable? I mean I guess if data is a Vector{Float32}, at a low level you will have 3(?) pointers to get at the data instead of 2. Not sure what the CPU overhead on that would be, but it should be minuscule.

That said, you might be able to do something like:

struct A{T}
    data::T
end

mutable struct B{T}
    data::T
end

A(d::T) where T <: SVector = A(B(d))

getdata(a::A{T}) where T = a.data
getdata(a::A{T}) where T <: B = a.data.data

Granted any changes you want to make to the data will need to use the getdata function.

1 Like

thanks. looking forward to your examples.

I assume that a mutable struct is much slower than a immutable one, but Iā€™m not sure.

A(B(d)) not work. A and B have many (identical) fields other than data (not shown for simplicity).

Wait, do you have some instances where you donā€™t need to update data and thatā€™s why you have two types in the OP? Or will you for sure need to change the data for every instance of A?

If you will always need to change values within data then you donā€™t want SVector. You may want to use MVector or just a Vector if youā€™re going to change sizes.

1 Like

yes sure, I need to change data for every instance.

I donā€™t want to use MVector because:

  1. itā€™s much slower than SVector
  2. everytime Iā€™m changing all contents of data, not at a particular index. Effectively I will put a new SVector into data

I think I get it. So you want to be able to swap out the SVector but you also want the type to not be mutable because all the other fields stay the same and donā€™t change.

I think what you want to do is something more like:

struct TypeWithManyFields end

struct SVectorAndFields
    fields::TypeWithManyFields
    data::SVector
end

Then make a new instance of SVectorAndFields like so:

old_data = SVectorAndFields(TypeWithManyFields(), SVector(1))

new_data = SVectorAndFields(new_data.fields, SVector(2))

noā€¦ itā€™s not the pointā€¦

let me try to clarify:

  1. I want to have a custom type containing a data field which can be a mutable vector (e.g. Vector) or an immutable vector (e.g. SVector).
  2. the whole content of data has to be updated over time. For mutable vector, it could be done by obj.data .= v, i.e. the contents of data are overwritten by those in v. However, for immutable vector, because its content could not be modified, the field data has to be replaced like obj.data = v.
  3. now, for obj.data .= v we can use a normal (immutable) struct. But to accommodate obj.data = v, we need a mutable struct instead.

the problem is how to get the best from both worlds (i.e. using a fast, immutable struct for mutable vector & using a mutable struct for immutable vector) without defining two custom types.

or, maybe two types are indeed needed, then the question becomes how to abstract / factor these two types into a single type as their behaviors are basically identical.

FYI, GitHub - JuliaFolds/BangBang.jl: Immutables as mutables, mutables as immutables. is aiming at providing a unified API for immutable and mutable objects. In particular, something like BangBang.@set!! x.data[1] = 0 should work for x :: A{<:Vector} and x :: B{<:SVector}. It even works for x :: A{<:SVector} so you donā€™t need to define the struct B. This ā€œmagicā€ is implemented in https://github.com/jw3126/Setfield.jl

4 Likes

Iā€™m still not sure I understand exactly what youā€™re trying to do. If you want mutable fields then this will allow you to have mutable fields with SVector.

mutable struct MyFields
    field1
    field2
    field3
end

struct MyVectorFields
    fields::TypeWithManyFields
    data::SVector
end

You said that data will need to be changed for every instance and the fields are going to change too. If they are all changing at the same time then just create a new instance every time they are all changed. In which case this would be fine:

struct MyVectorFields
    field1
    field2
    field3
    data::SVector
end

If youā€™re concerned with the first example because you are changing data a lot more frequently than the rest of your fields and donā€™t want the overhead of constructing a new instance (which should be small since itā€™s only two fields), then data probably shouldnā€™t be part of the rest of the data.

Itā€™s difficult to know exactly where the problem lies with any of these solutions without a more concrete example of what you are trying to do.

1 Like

inspired by your suggestions, I realized that the solution is quite simple: instead of calling

setdata!(obj, x)

I should call

obj = setdata!(obj, x)

thatā€™s it! (despite that this calling convention is not idiotmatc)

### normal & fast immutable struct
struct A{T <: AbstractVector}
    data::T
    others::Int
end

### traits
abstract type IsMutableTrait end
struct Mutable <: IsMutableTrait end
struct Immutable <: IsMutableTrait end
ismutable(::Type{<:SVector}) = Immutable()
ismutable(::Type{<:AbstractVector}) = Mutable()

function setdata!(obj::A{T}, x::T)::A{T} where {T}
    setdata!(ismutable(T), obj, x)
end

function setdata!(::Mutable, obj::A{T}, x::T)::A{T} where {T}
    obj.data .= x
    return obj    # *** returns itself ***
end

function setdata!(::Immutable, obj::A{T}, x::T)::A{T} where {T}
    newobj = A(x, obj.others)
    return newobj    # *** returns a new object ***
end

v = A([1, 2], 0)
sv = A(SVector(1, 2), 0)

julia> v = setdata!(v, [3, 4])
A{Array{Int64,1}}([3, 4], 0)

julia> sv = setdata!(sv, SVector(3, 4) )
A{SArray{Tuple{2},Int64,1,2}}([3, 4], 0)

in this way, I got the following advantages:

  1. only a single (fast, normal, immutable) struct is needed; no need to maintain two structs
  2. updating a mutable data could be done in-place (by .=)
  3. a new object is needed to be created only during updates of the immutable data

I think what you just came up with is a nice API. Thatā€™s how Iā€™d do if I were to do this in a self-contained module.

Itā€™s just a nit-picking, but I think the API

is somewhat non-conventional. Usually in Julia, for API f!(dest, args...), I think youā€™d expect dest to be guaranteed to be mutated. However, your setdata! sometimes donā€™t. I think itā€™s OK for an internal function. Base has something like this; e.g., grow_to!. But if you are going to expose it as a public API, maybe itā€™s confusing?

(BTW, thatā€™s why Iā€™m using a strange !! suffix in BangBang.jl. But I donā€™t think itā€™s a conventional suffix.)

I wouldnā€™t. I would expect that it allows dest to be mutated, but thatā€™s an option.

FWIW, I would be fine with just ! for this case.

I donā€™t think that it is common for callers to depend on the contents of an argument being actually mutated. Consider eg

using LinearAlgebra
A = Matrix(Diagonal(ones(2)))
B = cholesky(A)
rdiv!(A, B)

A conforming program couldnā€™t even tell if A was mutated here, since it is == to the original.

yeahā€¦ itā€™s not idiomatic. And itā€™s the very reason I spent so much time stuck in the problem!!!

maybe I should better rename setdata!() as something like setdataĪ”() to remind myself (and others) of this strange usage.

just for completeness:

### normal & fast immutable struct
struct A{T <: AbstractVector}
    data::T
    others::Int
end

### traits
abstract type IsMutableTrait end
struct Mutable <: IsMutableTrait end
struct Immutable <: IsMutableTrait end
ismutable(::Type{<:SVector}) = Immutable()
ismutable(::Type{<:AbstractVector}) = Mutable()

function setdataĪ”(obj::A{T}, x::T)::A{T} where {T}
    setdataĪ”(ismutable(T), obj, x)
end

function setdataĪ”(::Mutable, obj::A{T}, x::T)::A{T} where {T}
    obj.data .= x
    return obj
end

function setdataĪ”(::Immutable, obj::A{T}, x::T)::A{T} where {T}
    newobj = A(x, obj.others)
    return newobj
end

v = A([1, 2], 0)
sv = A(SVector(1, 2), 0)

v = setdataĪ”(v, [3, 4])
sv = setdataĪ”(sv, SVector(3, 4) )

julia> v = setdataĪ”(v, [3, 4])
A{Array{Int64,1}}([3, 4], 0)

julia> sv = setdataĪ”(sv, SVector(3, 4) )
A{SArray{Tuple{2},Int64,1,2}}([3, 4], 0)

Functions like pop!, splice!, take!(channel), put!, etc. depend on mutation.

Iā€™m not sure what you want to point out with rdiv! example. Itā€™s docstring says

Compute A / B in-place and overwriting A to store the result.

Iā€™d argue that rdiv! method implementation that does not mutate A has a bug.

So do you think itā€™s OK to add APIs like push!(::Tuple, _) and delete!(::NamedTuple, ::Symbol) in Base? What about push!(::StaticVector, _)? I personally find them strange but if Iā€™m a minority Iā€™m happy to use such APIs as they are very useful.

Consider the hypothetical

my_rdiv!(A::AbstractMatrix, J::UniformScaling) = my_rdiv!(A, J.Ī»)

function my_rdiv!(A::AbstractMatrix, Ī»::Real)
    Ī» == 1 ? A : LinearAlgebra.rdiv!(A, Ī»)
end

It may or may not mutate A. I am not proposing it as something elegant or efficient, but I think it would be conforming.

No. The method should do what the contract prescribes, and I would expect that after

push!(collection, item)
@assert item āˆˆ collection

holds.

My point is very simple: all I am saying that if the method actually does what it promises, whether it mutates a given argument is irrelevant (as long as that wasnā€™t promised). Eg if the recommended way to use a method is

result2 = buffered_calculation!(result, arguments)

then the method could have result === result2 and overwrite it (eg for Array) or make a new one (SArray). As long as this is only conditional on the type, the compiler can deal with it just fine.

1 Like

So you are just saying that when ā€œdo nothingā€ is the correct answer, the caller should expect the argument is not mutated (e.g., sort!([1, 2, 3])). I agree, of course. I guess you missed my initial point. It was that itā€™s not possible to define (say) push! semantics on immutable corrections like tuples; hence the weird suffix.

To be fair, I think you are missing my point too: all I am saying is that my interpretation is that ! allows a specific argument to be mutated, but does not require it per se.

For push!, the requirement for mutating the collection comes from the fact that it should contain item.