Why Julia values types are inmutable?

In C#, you have structs, that are value types. Value types are types that are passed by value, and can be efficently inlined in stack for example. The more similar thing that I can see in Julia are inmutable types. As I understand it, in C# sometimes you want to use structs for perfomance reasons, and in Julia, you want to use inmutable types for the same reasons: avoid boxing mainly.

So, my question is: why the most similar thing in Julia to C#'s value types is inmutable too? Why not only allow pass by value types and take inmutability as a orthogonal thing?

4 Likes

I think the basic reason is that these aren’t fully orthogonal. Small rows are often the ones you want to be passed by value, but copying then it’s basically free, since they are fixed size and small. Furthermore, specifying immutability allowed the computer to do all sorts of other trucks, most importantly, not creating the struct at all.

1 Like

I may be wrong, a committer or steward will probably have a more accurate answer, but basically, it is in the design of the language that you do not decided how the objects are passed to methods. If the object is immutable, it may be passed by value or reference, it is irrelevant, because it never changes your code is correct independent of what the compiler decides. If you want to pass a mutable object to a function, it will be passed by reference (unless somehow the compiler is able to prove it is not changed, maybe inlining the pure method in the caller point).

It is a design decision that permeates all the language.

3 Likes

Worth noting the C# lore is that mutable value types are evil: Mutating Readonly Structs | Microsoft Docs

7 Likes

Only allowing pass by value would be quite annoying when you pass large things, like arrays, around. Then you’d have to instead pass a Ref(my_array), and unwrap it in the function. Similarly, only having pass by reference would not be good - it would be inefficient for small types like integers.
As it works now, whether Julia passes by value or by reference is an implementation detail, and is not supposed to be distinguishable by the programmer. That is a good thing, because it’s one less thing to worry about - the compiler will just do the right thing and you never have to worry about how Julia is passing things around.

As for decoupling mutability from how an argument is passed - how on Earth would you pass a mutable type by value? Suppose function A passes a mutable type X to function B, and function B then mutates it, before returning back to function A. Unless X actually contains a reference to the X in function A, how will function A know it has been mutated?

3 Likes

This is the the case for other languages as well. Julia always pass by reference which for immutable types is equivalent to pass by values. I assume you are talking about passing in register or memory and that has always been an implementation in all languages that compiles to native code.

1 Like

This is a really good question with a kind of unexpectedly philosophical answer!

If you have mutable value and reference types, then you end up with a bifurcated type system that has two fundamentally different kinds of values with incompatible semantics. In the following example, imagine T is some kind of mutable struct with a field field::Int but we don’t know if it’s a hypothetical mutable value type or a normal mutable reference type:

function mutate_maybe!(x::T, v::Int)
    x.field = v
end

x = T(1)
mutate_maybe!(x, 2)
println(x.field)

Does this code print 1 or 2? In other words: are mutations to a value visible outside of the function in which the mutation occurs or not?

The answer is that you have no idea: it depends on whether T is passed by value or reference. This is what I mean by “incompatible semantics”. This kind of thing is bad enough in a language like C# with static typing—at least you know from the types in the program which case you’re dealing with. Now imagine that in a language like Julia where type annotations are optional! I could have left the types off of the signature of mutate_maybe! and it would mutate some kinds of values and do nothing to other kinds of values. The very semantics of the language are unclear without knowing whether you’re dealing with a value type or a reference type. Not great, Bob.

Note that this also makes function call boundaries rather bizarrely significant: you can’t just inline the operations of a function if the arguments are passed by value since you have to make sure that mutations that happened across a function boundary have no effect while also making sure that mutations that didn’t happen across a function boundary do have an effect. If I automatically or manually inline the logic of a function, it changes the meaning of those operations! Oops.

If, on the other hand, we instead disallow mutating some types, then this problem completely vanishes: all values have the same semantics, but we’re allowed to pass the immutable ones by value or reference as we see fit because there’s no way to tell the difference!

Moreover, this is a feature for many kinds of objects. Imagine if you could mutate the value of an integer? This was actually possible due to a bug in an early Fortran compiler. You could change the value of 2 to be 3 and after that anywhere anyone used the value 2 they got a 3 instead. :grimacing:

The intuitive wrongness of that Fortran compiler bug is getting at the fundamental fact that numbers are a value type: if you increment the number 2, you do not get the same number with a different value, you get a different number. It fundamentally does not make sense to mutate a number. The fact that Julia allows user-defined types to be immutable in the same way is a powerful feature: you can make your own types that have this same number-like property that if you change them, they are different thing, not the same thing with a different value.

Bottom line: value types should not be mutable. The fundamental question about a type is “if I change the value of this thing, is that a different thing or the same thing with different content?” If the answer is that it’s a different thing, then you have a value type and it should be immutable. If the answer is that it’s the same thing with a different value, then that object must have identity independent of its contents, so it’s a reference type and may be mutable. The very concept of a mutable value type is muddled and ill conceived.

31 Likes