Julia's assignment behavior differs from Fortran?

I have been there. Fortran is so simple that we have to learn even about what they are talking about in these answers. Mutable? Immutable? Labels? x[i]=1 is a function call? It is fun if you take it the easy way.

2 Likes

I’m not a programmer, I’m a physicist writing a program.

So I’m hoping to use the program like a walking stick for support, then the stick turns into a Swiss army knife, and everyone says that “those are better than sticks!”. Which may be true, but…

3 Likes

Counterpoint, having

y = [1, 2]
x = y

not perform a copy is super helpful and imo pretty simple. It means that you can write a program that re-names variables as freely as you like, with mutation, and not worry about the overhead of copying variables. Its a really good convenience.

4 Likes

Yeah, from a performance standpoint, setting up x as just a name pointing to y is better than copying over the entire contents of y into x. But, I wonder, why ever rename a variable at all? If you have the name y already, what’s the use of having x as another name for the same thing? Now you just have a redundant new name x that gets changed by commands that don’t mention x in them at all.

1 Like

It only gets changed if it is mutable. It is not a risk if you are using immutable objects like numbers. Also, the meaning of some data may change inside a function scope. You may have a current and old variables in which data flows to current and then to old, you may have a data and a buffer variables with the same kind of object and then you make changes to the buffer and make it the new data, and then store the old data as the new buffer. These are all cases I had in my real code and that benefited for this behaviour.

Agreed. The point is if the advantages are worth the efforts.

Is plotting? Automatic differentiation? Automatic access to all blas without having to deal with compilation flags? Easy parallelization? Distribution of the code to every platform easily? Easy access to all the famous Optimization packages?

1 Like

I go by Matt, Matthew, and (very very rarely) Dr. Bauman. All three names refer to the same thing — myself. You tell Matt to do something, and my mom would describe it as Matthew doing something, and someone who is trying to be overly formal and doesn’t really know me might describe it as Dr. Bauman doing something. Yes, this is “spooky action” where telling one name to change something about itself actually affects all three names at once without referencing all three names — but we’re very used to it!

This is the key — the name is not the thing. Mutations happen to the thing. The name is just how we describe the thing to one another — and depending upon the context different names might be useful.

12 Likes

I find this discussion very interesting. I have the impression that it is hard for someone that has not programmed in Fortran for a long time to understand the issues we Fortran programmers have (not with Julia in particular, with other languages in general, probably). The thing is that Fortran is so restricted in its purposes that a lot of the choices one has in other languages are at least not default behaviors in Fortran, and the default behavior is most generally a good one for numerical computations. That is why one can be a completely ignorant in most aspects of computer programming and still produce good quality and fast Fortran programs. And that is why Fortran is still around.

When you, experts, answer our questions, many times you simply do not imagine how ignorant we are about even the terminology you use, even if we are (and I could give examples of that) the developers of important numerical packages that you use in your every day life.

4 Likes

I am still trying to understand what mutable means, I googled it and still couldn’t quite get it.

BTW, my last large-scale numerical simulation programming effort (before the one I’m doing this year) used a shiny new programming language you may have heard of, called Fortran 90. (It was rough adapting over from Fortran 77, but I got used to it.)

(Actually, that doesn’t count the stuff I was doing in Mathematica. But Mathematica doesn’t really let you look under the hood at all, which is why I’m here.)

1 Like

Maybe this will help. Note that y1 is just a pointer to the M object. So is y2, consequently when you modify the inside of y2 using the name y2, since the name y1 points to the same thing, y1 also looks like its modified.

In contrast, you can’t modify x2.x because its not a mutable struct. The “two names for the same object” logic doesn’t work.

julia> mutable struct M
       x
       end

julia> struct P 
       x 
       end

julia> y1 = M(1);

julia> y2 = y1
M(1)

julia> y2.x = 5;

julia> y1
M(5)

julia> x1 = P(1)
P(1)

julia> x2 = x1;

julia> x2.x = 5
ERROR: setfield! immutable struct of type P cannot be changed

Ok thanks for the example pdeffebach, I will check it out.

Hunh? It definitely still works. You still have two names for the same object — but the object cannot change.

Mixing up mutation and naming is part of why I think this is so confusing to newcomers. They’re totally independent concepts. Naming works exactly the same, regardless of mutability.

8 Likes

Yes for sure. By this logic I only mean only the thinking "if I change this variable it shows up in y1"

This reminds of something I read about Python years ago.

Someone compared a python object to a cat that travels between two houses. In one house the cat is called Tigger and in the other it’s called Fluffy. The cat doesn’t know its name.

10 Likes

I may have read this wrong, but I think some part of the confusion may be an expectation that code looks and works more like math. Let’s say this little bit here was just math on paper:

xNew = xOld + 1
xOld = xNew

This would indeed imply an impossible equation xOld = xOld + 1. But = and xOld = xOld + 1 don’t mean that in Julia (or Python, as mentioned earlier). I don’t know Fortran so I can’t say for sure, but maybe Fortran was designed to look and work a lot more like math.
A lot of these concepts cropping up (assignment, references, mutability) may be confusing at first but are good to learn. And people here are very willing to help out.

In Fortran you would have declared xOld and xNew at the beggining of the code giving the sensation that it each is a variable with a specific position in memory, without any ambiguity. Therefore,

xNew = xOld

copies the value of xOld into xNew.

Now that I am immersed in this world of labels, etc, I understand that what we think is going on might not be the actual thing the program does when it is compiled and executed (the positions in memory of xNew might not exist in practice), but that is not relevant for the user.

Concerning the mutable vs. immutable thing, from the perspective of a previous Fortran user: In Fortran everything seems to have its place in memory, as I said, and everything seems to be mutable, although that might not be true in practice. Thus, there is an abstraction layer there that must be overcome. I hope what I say in what follows is not too wrong.

One learns in this process that values can “exist” in different ways. A variable may occupy a place in one type of memory, the type of memory that we understood that existed, which is called the “heap”. The variables in the heap have an address to the position in memory they occupy and, thus, the value that they assume can be changed, by modifying the content of that position in the memory. This is where mutable variables are stored.

In Fortran, from a user perspective, everything seems to be in the “heap” (although that might not be true, the compiler will decide that), in such a way that one can program as if every variable had an address in memory and its value can be modified by modifying the content of that position in the memory. Thus, everything seems to be mutable in Fortran. Additionally, labels are assigned forever to the same positions in memory.

Now we learn that some variables might exist in other types of memory, the “stack” and (I guess) the processor cache. These types of memory are much faster than the “heap” to work with, and if a variable can be assigned to these places your code will be faster. However, the values occupying these types of memory positions do not have an address in the usual sense. You cannot change the value associated to that position in memory because that value in that position in memory is not persistent, that is, it will be discarded as soon as possible. Even if the value will be used later, it might be that it is copied somewhere else in the stack without our knowledge if that results to be the best strategy for performance. We do not control where these values are stored and, then, we cannot assign different values for these variables, because this actually does not make sense, they are only values stored somewhere in a fast access memory.

Thus we learn that in a loop like:

s = 0.
for i in 1:3
   x = 2*i
   s = s + x
end

x might not be allocated in memory at all. It might occupy a temporary place in the fast stack memory or, even, only in the processor cache. In general we don’t know what is going on with x, and we should not care about that, the people that implemented the compilers are much smarter than us and implemented most likely the best choice. Perhaps it will be stored in the slow “heap” memory, with an address, particularly if it was a huge array instead of a simple scalar, but it doesn’t mater. (in this case probably it is just inlined, but the idea is the same)

A Fortran user is suprised that a loop like that does not allocate anything. We learn that everything has its place in memory, even the counter of the loop, so that code should at least allocate some values. Yet, now we discover that these allocations “do not count”, because are fast allocations in these non-addressed types of memory.

But we have to learn that for the compiler have freedom to choose what to do with x, the content of x cannot change. Thus, it must be immutable. In the loop above, it doesn’t even make sense calling x the same variable at each loop iteration. It is just a temporary value assigned to some fast memory position that will be used and discarded.

Therefore, if we write

x = 1 
x = 2

the two x are probably just two completely independent values stored in these fast memories. Both the “first x” and the “second x” are immutable. Actually what is immutable is the Integer values 1 and 2, and x is only a temporary label to one or other of this values. The first x will be discarded when convenient without we knowing where it was stored, if it was stored at all.

Yet, if we write

x = Vector{Int}(undef,1)
x[1] = 1
x[1] = 2

we are assuming that for some reason you want to access the same position in memory repeatedly, and this must be stored in the slower heap memory, where things have real addresses. This x is a mutable object, you can actually change the content associated with the position it occupies in memory explicitly.

Later we learn that vectors can also be immutable (why not? If a number can be stored in these fast memories, why not a bunch of numbers?). And we can use StaticArrays where small vectors behave the same as any other immutable value, like a number. This means that:

julia> function f()
         s = 0.
         for i in 1:1000
           x = SVector{3,Float64}(i, sqrt(i), i^2)
           for j in 1:3
             s = s + x[j]
           end
         end
         s
       end
f (generic function with 1 method)

julia> f()
3.343550974558874e8

julia> @allocated f()
0

Wait, that function that generates 1000 vectors of dimension 3 does not allocate anything? Yet it doesn’t, because these static arrays are immutable, so they only exist in the fast memory positions which are temporary. Knowing this allows a bunch of code optimizations which are very cool, and a very pretty syntax if you are dealing with particle simulations. For instance, you can do:

julia> x = [ SVector{3,Float64}(1,1,1) for i in 1:3 ]; # positions of 3 particles

julia> function update_positions!(x)
         for i in 1:length(x)
           y = 2*x[i] # whatever needed
           x[i] = y 
         end
       end
update_positions! (generic function with 1 method)

julia> update_positions!(x)

julia> x
10-element Array{SArray{Tuple{3},Float64,1,3},1}:
 [2.0, 2.0, 2.0]
 [2.0, 2.0, 2.0]
 [2.0, 2.0, 2.0]

julia> @allocated update_positions!(x)
0

The update_positions! function is mutating the elements of x (x is mutable), but the elements of x are themselves immutable static vectors. This is, the line y = 2*x[i] is just creating a new static vector, and x[i] = y is not actually modifying the values of the positions in memory of the elements of x[i] as we would think (or it might be, that is just not your problem), and all that does not involve any access to the slow heap memory of the computer. Thus you can deal with a vector as fast you can deal with a scalar.

The possibilities to improve the performance of a numerical code increase. I have been able to write faster codes in Julia than in Fortran now, but that depends on some adaptation with that new way of thinking and with the new possibilities involved.

10 Likes

Just to add one thing. There is nothing mysterious about StaticArrays. They are convenient immutable structures, which you could have defined yourself, with the same allocation results:

julia> struct P
         x :: Float64
         y :: Float64
         z :: Float64
       end

julia> function update_positions!(x)
         for i in 1:length(x)
           y = P( 2*x[i].x, 2*x[i].y, 2*x[i].z )
           x[i] = y   
         end
       end
update_positions! (generic function with 1 method)

julia> x = [ P(1.0,1.0,1.0) for i in 1:100 ];

julia> update_positions!(x);

julia> @allocated update_positions!(x)
0

3 Likes

The fact that x = , x .= and x[i] = mean totally different things is quite unintuitive, and I remember being confused by this when I started julia. This doesn’t look really covered in the docs, at least not at a place a beginner is going to look at. Might be a good idea to add a small section on arrays? The docs on “noteworthy difference from matlab says” “Julia arrays are not copied when assigned to another variable. After A = B , changing elements of B will modify A as well.” which is good but might be a bit more explicit? Also should there be a “noteworthy differences from fortran”?

6 Likes

I think I understand what is going on under the hood based on this discussion. Can someone confirm that my comments are accurate?

x=[2]  # x points to memory location m1
y=x    # y points to memory location m1
x=[3]  # x points to memory location m2, y still points to m1
z=x    # z points to memory location m2
x[1]=4 # m2 changes value in place, affecting all variables that point there
julia> println(x,y,z)
[4][2][4]
6 Likes

There is an attempt to build that list here: Noteworthy differences from Fortran

1 Like