Make function paramaters immutable by default

proposal

#1

Right now Julia has a convention that every function that mutates its parameters is marked with ! but it has several downsides :

  1. It is just a convention - we rely on programmer’s good will and we all know what does that mean. (Especially when deadlines appear.)
  2. It doesn’t tell you which argument is being mutated so you will have to specify those mutable arguments explicitly in documentation instead. (Again another convention…)

On top of that making mutability explicit looks even more promising when you consider that :

  1. It enables further optimizations.
  2. It makes code less prone to error. It is easier to reason about code.
  3. It is generally believed to be the right style: Experienced C programmers mark parameters with const whenever they can. Rust does that and even Julia itself recently moved from mutable structs to immutable structs.

Although it is not a big change for compiler it is totally not backward compatible so the final decision should be made before 1.0 comes out…


#2

Function parameters are immutable, but underlying objects aren’t. It’s the same as passing constant pointers in C - you cannot modify a pointer to reference a different object, but you still can modify the object it already points to.

If you want to guarantee that a function will not modify contents of the object, you have to pass immutable objects, so it’s not really related to the mutability of function parameters.


#3

You are wrong, it is possible to pass a reference to mutable object into a function and guarantee the object won’t be mutated.

See constant pointer vs pointer to constant.

    1. 2017 1:40 PM, 1:40 PM, Andrei Zhabinski julialang@discoursemail.com napsal/a:

#4

There’s no such guarantee in C. It is NOT UB to mutate a pointer to constant int in a C/C++ function.

It is.


#5

⁣What do you mean bu UB?

    1. 2017 2:55 PM, 2:55 PM, Yichao Yu julialang@discoursemail.com napsal/a:

#6

Undefined behavior.


#7

That’s why I told “constant pointers” and not “pointer to constant”.

Constantness is not a property of objects in C, but only a compilation-time constraint. If compilation of a caller happens in a different context than that of callee (e.g. when using a shared library), the compiler doesn’t have access to the code of called function and can’t guarantee that the passed object won’t be modified. Given that Julia uses dynamic compilation, tracking parameter constantness becomes even harder.

Also in C++ there are “legal” means of removing constantness of a variable, const_cast is the first the comes to mind.

If your primary interest is in additional optimizations, take a look at discussions on pure functions (e.g. #414). If we can infer whether a function is pure, we can automatically apply many optimizations without changing the semantics of the language.


#8

Why? Julia already has parametric types. If “constantness” were like a parameter of every type, then julia would already have all the machinery in place to track it, no?


#9

Because that’s not what this thread is about, the constantness is already a property of every type and this thread want to make it a property of a variable, which is impossible to keep track of.


#10

I still don’t follow. The type is a property of a variable, so if constantness can be inferred from the type then it should be straightforward to tell if a variable is constant?

Maybe I did not express myself clearly. Yes mutability is a property of every type. But it is not a type parameter. If I want an immutable version of an existing mutable type, then I have to define it from scratch. What I meant was to have the mutability flag accessible somehow. Function arguments would be of the immutable version of their types unless otherwise specified, and promotion from mutable to immutable would be automatic and essentially a no-op. (Conversion in the other direction would necessitate copying, of course.)


#11

In dynamically-typed languages like Julia type is a property of an object, not a variable.

Can you provide an example when you need it?


#12

In my scenario, I would need it basically whenever I wanted to call a function without an exclamation point in its name.

For example, getindex would accept Array{:immutable, T, N} so if I had a mutable array, I’d have to convert it to immutable. (Of course, julia would do that for me, so getindex would appear to work exactly as before. Only if some bug made it try to modify the array would there be a difference.)

Conversely, setindex! would only accept Array{:mutable, T, N}, and in this direction there would be no auto-conversion.


#13

So why not just use immutable arrays? There’s even a package for it.


#14

We should not do this along with the huge performance cost due to repeated conversion.


#15

Array and getindex was just an example. I’m talking about any type and any function that is not explicitly declared to modify its inputs.


#16

Would there have to be a performance penalty, though?

If the mutable and immutable versions of objects have the exact same memory layout, then the conversion from mutable to immutable should be a no-op. So I can only think of two cases when there would be a performance hit:

  1. Conversion from immutable to mutable will require copying. But this should only happen in the places where the copy operation was necessary anyway, such as a function which does not wish to modify its input but still use an in-place algorithm.

  2. When extra methods need to be compiled because a function is called with both immutable and mutable inputs. But this should basically never happen. Functions that don’t modify their inputs should only have methods for the immutable type (and handle the mutable type via auto-promotion). And functions that do modify their inputs cannot be called with the immutable type.


#17

Yes. A lot.

No you have to make a copy or you mutate the type of an object.

No. If you don’t make a copy you can’t enforce this.


#18

So I again ask for examples. I see 3 possible situations:

  1. You never modify an object - in this case immutable / struct are what you need, Julia can apply particular optimizations functions with them.
  2. You modify an object quite often - in this case type / mutable struct are what you need,
  3. You modify an object just in a few functions:
    3.1. If the object should get modified in a caller, you don’t have any choice but to use mutable structs anyway.
    3.2. If the object needs to be modified only inside a callee, you can use immutable structs and create a local mutable copy when needed. Yet, I can hardly think of any such example.

The point is that there’s probably an easy way to achieve what you want using features that are already in the language, without breaking any existing code.


#19

They don’t. Immutables are value-types. Mutable types are not. There’s a huge difference there.


#20

OK. Here’s an example: Let’s say I have an object that I modify quite often, but I only do it in functions that I’ve written myself. I also want to call functions written by my friend (in order to visualize my object or whatever), but I need to be sure that these functions don’t modify my object. He tells me he’s not doing any ccalls or unsafe_* (or those parts of the code are checked really carefully) but he can’t guarantee that he’s not unintentionally modifying my object, because he’s also calling functions written by other people. These people say exactly the same thing. So now I have to make a copy of my object before each call to a function that I didn’t write myself… but my object is a huge neural network that requires 60 % of my RAM for each copy.

Fair enough. I should not have used the term “immutable”. Let’s say there’s a third category called “constant”. Objects in this category are passed by reference like mutables, but modifying their fields is forbidden like immuatbles. Reading the fields of a “constant” always yields an immutable or another “constant”. The “constantness” applies to whole objects, including nested objects. In a similar way, Union types cannot contain both “constant” and mutable types simultaneously. The “constantnesss” flag applies to the whole union.

The way I imagine it, the compiler should be able to keep track of the “constantness” of every object via the type inference engine. It wold still emit the same code as for immutables, as long as no modification is done to the object.

“Constantness” would be a convenience feature, not a safety feature. There would be no way to prevent malicious code from modifying “constant” objects, e.g. via ccall.