mutable fields in Julia struct
Asked Answered
H

3

22

I couldn't find an answer in both stackoverflow and the Julia docs to the following "design problem":

Let's say I want to define the following object

struct Person
birthplace::String
age::Int
end

Since Person is immutable, I'm happy that nobody can change the birthplace of any Person created, nonetheless, this also implies that when time passes, I cannot change their age either...

On the other hand, if I define the type Person as

mutable struct Person
birthplace::String
age::Int
end

I can now make them age, but I don't have the safety I had before on the birthplace, anyone can access it and change it.

The workaround I found so far is the following

struct Person
birthplace::String
age::Vector{Int}
end

where obviously age is a 1-element Vector.
I find this solution quite ugly and definitely suboptimal as I have to access the age with the square brackets every time.

Is there any other, more elegant, way to have both immutable and mutable fields in an object?

Maybe the problem is that I am missing the true value of having either everything mutable or immutable within a struct. If that's the case, could you explain me that?

Heterogamete answered 23/1, 2018 at 12:5 Comment(4)
from the way you phrase the question I assume you don't care for a incrementage function that creates a new object with the right age? e.g. incrementage(p::Person) = Person(p.birthplace, p.age+1);Replevin
Exactly, I don't want to create a new object. The idea is a an object that fetches from the web some info and updates some of its fields.. Since it's gonna poll every 10 seconds or so, creating a new object every time is not what I am looking for.. But thanks anyway!Heterogamete
you could go down a route with a bespoke inner constructor giving you complete control over what is and is not visible / mutable, like here: https://mcmap.net/q/408832/-how-to-create-a-quot-single-dispatch-object-oriented-class-quot-in-julia-that-behaves-like-a-standard-java-class-with-public-private-fields-and-methods/4183191 (disclaimer: shameless plug of own question)Replevin
Nice! I am actually coming from C++, thus I am used to that "type" of objects. But it is definitely not in the philosophy of the language, thus I'll not go down that road.Heterogamete
F
21

For this particular example it seems better to store the birthdate rather than the age, since the birthdate is also immutable, and it is simple enough to calculate the age from that information, but perhaps this is just a toy example.


I find this solution quite ugly and definitely suboptimal as I have to access the age with the square brackets every time.

Usually you would define a getter, i.e. something like age(p::Person) = p.age[1] that you use instead of accessing the field directly. With this you avoid the "ugliness" with the brackets.

In this case, where we only want to store a single value, it is also possible to use a Ref (or possibly a 0-dimensional Array), something like:

struct Person
    birthplace::String
    age::Base.RefValue{Int}
end
Person(b::String, age::Int) = Person(b, Ref(age))
age(p::Person) = p.age[]

with usage:

julia> p = Person("earth", 20)
Person("earth", 20)

julia> age(p)
20
Forgave answered 23/1, 2018 at 12:53 Comment(4)
Indeed, it is a toy example... In general I tend to use "get" functions like the one you said only for fields accessible for the end-user, while deep down in the code (where the user will never have to look at) I access fields directly. Also, I don't know if there is a performance difference between accessing directly the field and accessing it through a function..Heterogamete
You can have getters only for internal use also, and there is no performance difference.Forgave
There is no performance difference because a small function which is type stable will just inline the function, so the compiler will essentially get rid of the function call and paste in the field access, making the getter a high-level zero-cost abstraction.Tergiversate
Instead of Array{Int,0} it's probably better to use a Ref. At least when I last checked there is a performance difference.Tergiversate
U
10

You've received some interesting answers, and for the "toy example" case, I like the solution of storing the birth-date. But for more general cases, I can think of another approach that might be useful. Define Age as its own mutable struct, and Person as an immutable struct. That is:

julia> mutable struct Age ; age::Int ; end

julia> struct Person ; birthplace::String ; age::Age ; end

julia> x = Person("Sydney", Age(10))
Person("Sydney", Age(10))

julia> x.age.age = 11
11

julia> x
Person("Sydney", Age(11))

julia> x.birthplace = "Melbourne"
ERROR: type Person is immutable

julia> x.age = Age(12)
ERROR: type Person is immutable

Note that I can't alter either field of Person, but I can alter the age by directly accessing the age field in the mutable struct Age. You could define an accessor function for this, ie:

set_age!(x::Person, newage::Int) = (x.age.age = newage)

julia> set_age!(x, 12)
12

julia> x
Person("Sydney", Age(12))

There is nothing wrong with the Vector solution discussed in another answer. It is essentially accomplishing the same thing, since array elements are mutable. But I think the above solution is neater.

Unveiling answered 23/1, 2018 at 20:59 Comment(6)
I was about to propose something along the same lines, but at the end of the day it's still rather unsightly. One could make an inner constructor for Person that takes an Int rather than an Age, so that at least you can do Person("Melbourne", 11). Also you could make Age types callable so you could at least do p.age() rather than p.age.age ... but at the end of the day it's not something that will be seamlessly used in Int expressions unless one goes down the line of conversions as well (which is probably not worth the effort). Then again, neither is the "vector of 1" approach.Replevin
@TasosPapastylianou Yes, agreed it still all looks a little messy, but if the criterion is a composite of mutable and immutable objects, I don't really see a cleaner way. At least, as you say, most of the ugliness can be hidden with some intuitive accessor functions. If your object has lots of fields, you could probably even metaprogram to avoid code bloat.Unveiling
I still prefer the option with Vector or Ref, as I don't have to define a new struct. In particular, considering that I would be creating struct Age just to make it a field in the struct Person and not using it anywhere else, it seems to me a bit silly. At the moment, since in general in the structs I'm creating I have more than one field that I would like to have mutable, I try to combine them in containers (usually Dict) in a logical way.Heterogamete
@Batta Yes, this one really comes down to personal preference. I personally am happy to create a struct for one-off use cases. Sometimes such behaviour can be quite useful for type checking. For example, in finance, you could make both prices and volumes of type Float64, or they could be of type Price and Volume (just wrappers on Float64), but now if your functions expect Price and Volume, you will never accidentally confuse the two in your code. Useful trick for production-level code where accidents like this cost lots of money :-)Unveiling
@ColinTBowers, your last comment is the most enlightening thing I've read all week about typing. I don't know why I hadn't picked it up before. In addition to Price v. Quantity, you could have kg<:Quantity v. ton<:Quantity or Dollar:<Price v. Euro:<Price, or even Endogenous v. Exogenous, that sort of thing, no? Awesome!Provitamin
@Provitamin Oh for sure. I work a lot with equity data. Some things that exist in my own code base include: abstract type Currency ; end, struct AUD <: Currency ; end, and so on, or abstract type Exchange ; end, struct NYSE <: Exchange ; end, struct ASX <: Exchange ; end, and so on. The exchange one becomes super useful when working with times, for example, since all exchanges have different operating hours, so I can write functions that return market open and closing times and dispatch based on different exchanges.Unveiling
S
6

In Julia 1.8, you can use

mutable struct Person
       age::Int
       const birthplace::String
end

Cf. https://docs.julialang.org/en/v1.8-dev/manual/types/#Composite-Types

Studner answered 28/2, 2022 at 21:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.