Dear Julia Community.
I have a struct that contains both Bool flags and arrays.
I want to be able to change the value of some of the Bool flags, but I would like to avoid having a mutable struct, which I believe can create problems like type instability (where the type of the elements could change at runtime, since I need to define it with abstract types for generality)
Now, This is currently a mutable struct, since I would like to change the value of initialized, however, I would like to have it immutable, as the Array{T} is generic and could be reallocated at runtime, so keeping it mutable I believe this could create unwanted type instability. I also want to avoid being more specific here (e.g. using a concrete type for the arrays), as based on some condition in the initialization, the type and ranks of the Array could change during the initialization.
Indeed, a working solution is to use a 1-length Bool array for initialized, but it is not really elegant. Does it exists an automatic wrapper on something like this (i.e., like a pointer to a single element)?
My solution is kind of hard but if I had your problem, that’s what I would have done.
First of all I would have made a completely immutable struct with parametric types for arrays:
struct StochasticSettings{T} # T <: Array
remove_scha_forces :: Bool
clean_start_gradient_centroids :: Bool
clean_start_gradient_fcs :: Bool
initialized :: Bool
original_force :: T
original_fc_gradient :: T
end
Then I would define accessors functions
settings = setinitialized(old_settings, true) # the arrays will just be copied
This method should be done only if performance are critical. If not the solutions provided by the others are 100% fine
This is akin to an XY problem: you are trying to solve the wrong problem here, because the premise of the question has a fundamental misunderstanding.
Mutability vs immutability has nothing to do with type instabilities. If you have abstractly typed fields, it is type-unstable whether or not the struct is mutable.
The way to get both concrete types and generality is to define parametric structs, as you have attempted here with {T}. This can be done for both immutable and mutable types.
No. A parametric type like StochasticSettings{T} is not a single type, it is a family of types. e.g. when you initialize it with Vector{Float64}, you get a StochasticSettings{Float64} instance where the fields can only be Array{Float64}.
However, Array{Float64} is still abstractly typed, because it doesn’t include the dimensionality. To make it concrete, while remaining mutable, I would change it to Vector{T} in your definition (assuming you want only 1d arrays). You could make it even more generic with e.g.
mutable struct StochasticSettings{T <: AbstractArray}
remove_scha_forces :: Bool
clean_start_gradient_centroids :: Bool
clean_start_gradient_fcs :: Bool
initialized :: Bool
original_force :: T
original_fc_gradient :: T
end
which allows any type of array of any type of element and any dimensionality (replace with AbstractVector if you only want 1d arrays) … but the type is fixed for any single StochasticSettings{T} instance.
It’s really critical to understand this in order to get good performance in Julia.
Superficially, an immutable object’s fields’ runtime types don’t change because its fields don’t directly change, but if you constrain the field with an abstract declared type like Array{T} (an iterated union via unspecified N), then that’s what the compiler’s type inference has to work with, not the runtime value’s type. To use a simpler example, Some{Number}(1) and Some{Number}(1.0) both provide the compiler with the same immutable type Some{Number} regardless of the field’s type at runtime, typeof(something(s)). The mutability or immutability of StochasticSettings doesn’t actually matter here, your specified generality is the true cause.
It might not even help if you parameterize StochasticSettings to have concrete declared types because variables or expressions bound to instances of varying concrete StochasticSettings types for generality are also type unstable. The upside is a function call taking such instances as inputs can be internally type-stable even if the call site is type-unstable; this is called a function barrier in the Manual’s Performance Tips. You won’t even have to change StochasticSettings to leverage function barriers if you are mostly passing the original_force and original_fc_gradient arrays into performance critical function calls. A little bit of well-isolated type instability can be well worth the generality; to put some numbers on it, if 99% of your runtime is spent on internally type-stable function calls, then optimizing away the type instabilities and runtime dispatches in the other 1% of caller functions would only speed up the program by <(100/99-1)=1.01%.