Explicit denotation of variability and immutability

Parts of this proposal are fundamentally incompatible with how Julia works – in particular all the stuff about “type casting” makes sense in C++ but does not make sense in Julia.

In C++, dynamic and static types are separate things. The dynamic type of a value is its “actual type” while the static type of an expression referring to a value is the type of the expression – i.e. what the compiler thinks the type of a value is in a particular region of code. The static type should be a supertype of the dynamic type, but in general, these are different types and can lead to different behaviors. In non-virtual dispatch, for example, a method is chosen based on the static type of something, rather the actual “dynamic” type of the object.

In Julia, a value has only one unchangeable type – it’s actual type (i.e. its dynamic type in C++ lingo). Expressions don’t have types, only values do, and the type that the compiler thinks an expression has is just an implementation detail that generally doesn’t affect the behavior of code. (There are a few corner cases where we allow type inference to leak into visible behavior, but it’s avoided as much as possible.) The “static type” of something is simply not a concept in Julia, which simplifies the mental model of the langage tremendously as compared to C++. Moreover, the type of a value cannot ever change in Julia – an object has only one permanent actual type.

In this context, the proposed notion of casting between mutable and immutable variants for the same objects just doesn’t make sense. Whether a value can be mutated or not is a property of its type and that type cannot change. If you want to make a mutable object immutable, you can wrap it in an immutable wrapper. But that doesn’t buy you much since you can always just reach in and get the mutable internal object. Moreover, local immutability doesn’t really buy you much – most of the benefits of immutability disappear if some other code can still mutate something. You still have to guard against the possibility of mutation everywhere. That means you need to heap allocate data; you can still get data races. Julia does have immutable arrays, but the real benefit comes from knowing that no one can mutate them, not that some code cannot mutate them while other code still might.

Interestingly, making a mutable wrapper around an immutable object is actually useful and efficient, even though at first blush it seems to violate immutability. The key is that semantically, the mutating API works by replacing the whole immutable object with a new modified immutable value upon mutation. Since the wrapped object is immutable, however, we can make sure that the wrapper has the only reference to the immutable object that it wraps: immutability guarantees that we can make copies whenever we want without affecting semantics, so we can copy an immutable when it is wrapped and after that we know that the wrapper has the only reference. Therefore, mutation of the wrapped object can be implemented via actual mutation without violating the semantic immutability of the wrapped object. A bunch of design and implementation work has gone into supporting this better in Julia, but that effort has been tabled until after we get Julia 1.0 out the door.

13 Likes