Getting a reference to, and not a copy of, a field value

The code y = object.x binds a copy of object.x to y. How do I make bind y to object.x itself?

The reason for my question is this: To improve readability, I want to do something like this:

function foo(cache)

    X = cache.X
    y = cache.y
   
    # lots of stuff involving X and y

end

The problem is that X or y is quite big and I don’t actually want to copy it (and I would like changes to X to be reflected in changes to cache.X) and do not want to clutter my code everywhere with “cache”.

2 Likes

Your premise is wrong — in Julia assignment is just binding. X and cache.X are just two names for the same value.

Your code should do what you want. You may want to post a larger sample that reproduces the aspect that’s surprising you.

2 Likes

At the beginning yes, but not for long if the OP does X = ... later in the code, if that’s what the OP meant by “changing” X. If cache.X is mutable and the “changes” are of the form X.field = value, then this will be automatically reflected in cache.X.field. The gist is if one wants to modify the value that cache.X is binded to, it’s not possible via X, but if the changes are to some deeper bindings in the structure of the data object that cache.X is binded to, e.g. cache.X.field = new_value, then one can alias cache.X and work with the new shorter name instead. So this pattern is not uncommon:

x = cache.x; y = cache.y;
...all sorts of acrobatics with x and y involving x = ... and y = ...
cache.x = x; cache.y = y; # to change the data objects that cache.x and cache.y are binded to only at the end
2 Likes

Here is why I say that y = object.x binds y to a copy of object.x:

julia> mutable struct Foo
   x::Int
   end

julia> object = Foo(1)
julia> y = object.x
1

I now change object.x:

julia> object.x = 2
2

Buty is still bound to 1:

julia> y

1

This behaviour is not what I naively expected. And because of it, my strategy to declutter code allocates more memory, no?

1 Like

No, it doesn’t allocate more memory.

For example:

julia> a = rand(10000);

julia> b = a;

julia> b[1] = 2.0;

julia> a[1]
2.0

No copy was made from b = a, in fact, assignment never makes a copy. We can see:

julia> @time b = a;
  0.000002 seconds (4 allocations: 160 bytes)

The 4 allocations here is just from the global scope, the bytes allocated is much fewer than would be needed to copy the array. If we want a copy, we need to specify it:

julia> @time b = copy(a);
  0.000064 seconds (6 allocations: 78.359 KiB)

When you write

object.x = 2

you are not mutating the value that object.x binds to, you are simply rebinding object.x to 2. The value object.x binds to a number and is immutable so there is no way to mutate that value.

This is a good read: Values vs. Bindings: The Map is Not the Territory · John Myles White

NB: The post above only considers heap allocations which is likely what OP was interested in.

1 Like

I think the key issue here may be that you have a mental model of integers as mutable boxes and assume that assignment like this puts a different value in the box. That is not the case: integers are immutable and this assignment causes object.x to point at a different “box” containing the value 2; it does not mutate anything. The binding y still points at the “box” containing the value 1, which has not and cannot be changed because it is immutable.

4 Likes

To directly answer the original question, there is no general way to make the syntax x = ... have the same effect as a.b = .... However, if a.b refers to a mutable object, then doing x = a.b followed by mutating x has the same effect as mutating a.b.

Note that a.b = ... is a mutating operation on a. Something like push!(a.b, y) or a.b[1] = 0 would be a mutating operation on a.b.

6 Likes

Yes, I see that my initial premise was incorrect. My apologies for not checking this more carefully first, and my thanks for the explanations.

struct Foo
    X::Vector{Int}
end

julia> object = Foo([7, 7, 7])
julia> object.X
3-element Array{Int64,1}:
 7
 7
 7

julia> Y = object.X
julia> (object.X)[1] = 8

julia> object.X
3-element Array{Int64,1}:
 8
 7
 7

Y is indeed bound to the same object:

julia> Y
3-element Array{Int64,1}:
 8
 7
 7

I think those of us that come from C++ have a tendency to get super paranoid about copying and have a hard time trusting the compiler/garbage-collector to manage large objects sanely by default. Recently I created a whole package meant for managing large datasets entirely with pointers, only to realize that what I was trying to do works perfectly fine if done in the most naive way. I’ve come to the opinion that the best approach is to never worry about this sort of thing and to pick up the pieces later in the unlikely event that something goes horribly wrong. That approach will probably save a huge amount of wasted effort.

9 Likes

Out of curiosity, is there any future possibility (or possible already??) that it becomes possible to make
an alias to a variable of value (immutable) type, e.g. an integer variable? I mean something like
ref nmol = systemFoo.numMolecules, where numMolecules is an integer variable
and nmol is an alias to it. Then, I can write a simpler code using nmol for brevity.

It is of course possible to create a binding with a shorter name like foo = systemFoo first,
but I feel it more convenient to be able to make an alias directly to numMolecules.

(In C++, I guess this is possible by auto& nmol = systemFoo.numMolecules. In Fortran, one can do
a similar thing like associate( nmol => systemFoo%numMolecules ) on the fly (with no declaration of
nmol beforehand).

Absolutely NOT. The simplicity of the semantic of assignment is very essential to the language. making a[] = 1 do that will be possible.

5 Likes

Revisiting the same question in a more convoluted example:

mutable struct Foo{T}
    x::T
end

struct FooContainer{T}
    x::Foo{T}
end

struct FooFooContainer{T}
    x::FooContainer{T}
end

f = Foo(randn(10))
fc = FooContainer(f)
fcc = FooFooContainer(fc)


@allocated bar = f # 0

@allocated bar = fc.x # 0

@allocated bar = fcc.x # not 0!

How do I get the last line to be non-allocating, i.e. aliasing?

1 Like

That is only because of f, fc, fcc and bar are global variables and bar is not type-stable

let f = Foo(randn(10)), fc = FooContainer(f), fcc = FooFooContainer(fc)
    @allocated bar = fcc.x
end

returns 0

4 Likes