Why I can't set what parameters are mutable explicitly?

Do you have any information about this?

1 Like

Is it just me or has this been worded very confusingly? It’s not that OP wants an argument to be specified as immutable (an instance of an immutable type), rather the argument would be specified as not being mutated in the method. I’m rather baffled that the discussion is about method definitions because I could just look at the method definition and see if the argument is mutated in a few telltale ways:

  1. arg[i] = value does setindex!
  2. arg.afield = value does setproperty!
  3. somefunction!(arg, barg, carg) uses the ! convention to indicate that a method does mutation, so I’ll check that method to see if arg might be mutated.

I’ve read about a similar request but about function calls, and iirc, people figured out that any mark indicating “this argument isn’t mutated” in a function call could only be enforced if the method definition is inspected, which requires the dispatch to be known. So people just decided to write comments or name mutated variables with ! indicators.

2 Likes

They have Final that somehow works sometimes. Is not enforced by language or interpreter only with some various degrees of success. I hate this sort of plumbing in languages when Treating such an important problem in a language is so important: The side effects of a call.

I would like to see something like this in Julia:

function g(a!, b, c!)

It will help clarify a call, is in Julia spirit and should work in version 1.x too. But NOT as syntactic sugar enforced by complier!

https://mypy.readthedocs.io/en/stable/final_attrs.html

Also, expanding the use of const to arguments won’t help. Instances are not const, variables are. So all const does is prevent the variable from being reassigned. It however does nothing to prevent the instance from being mutated; the instance’s type is either immutable or mutable. ReadOnlyArray is an immutable wrapper of a possibly mutable AbstractArray, and it forwards to all wrapped array’s methods except the mutating setindex!; if you pass a ReadOnlyArray into a function call, you can guarantee that no mutation happens, just throwing an error if the function tries.

Here’s an example rewriting the OP’s code into the global scope where const does work.

julia> const v = [1, 2]
2-element Vector{Int64}:
 1
 2

julia> v[2] = 3   # mutation of the instance
3

julia> v   # mutation worked
2-element Vector{Int64}:
 1
 3

julia> v = 3   # reassignment of variable failed
ERROR: invalid redefinition of constant v

mypy’s Final seems to be analogous to const: “Final names are variables or attributes that should not be reassigned after initialization.”

1 Like

All arguments should be const by default.

Even if they were (and we would need some nonconst keyword to allow some variables to be reassigned), it would still not prevent mutation. My example above clearly demonstrates mutation of an instance assigned to a const variable.

4 Likes

Re-assignment would anyway not be visible outside of the called function, so the usefulness is unclear.

But, you are right: it is mutability which is the issue here, not whether bindings are const.

2 Likes

This would be very good if are enforced by the compiler not optional things.

It is very important that a class that has mutable fields or vector to not be changed inside a call by default or no side effects please by default! I would preffer this to be the default behaviour and to use ‘!’ to flag mutable variables. Something like:

g(a!, b!, c)
swap(a!, b!)

not g!(a, b, c) how it is now.

This must be enforced by the compiler.

You’re talking about something I remember from that thread. g(a!, b!, c) is actually a fairly unusable syntax, and it’s apparent if we look at a definition and a call:

function g(a!, b!, c)
   # mutates a! and b!, c is not mutated
end

# many lines later, or even in a different file
function f(x, y, z)
  g(x, y, z)
end

When we call g, we can’t tell if it mutates anything at all. The ! method convention is only necessary at the call, not the definition. We wouldn’t need it at the definition to see if anything is mutated, we could just inspect the method body.

Well okay, let g be g! then, let’s re-examine your idea:

function g!(a!, b!, c) # already legal to write, just not enforced
   # mutates a! and b!, c is not mutated
end

# many lines later, or even in a different file
function f(x, y!, z!)
  g!(x, y!, z!)   # mutates x and y!, z! is not mutated
end

Oh no, just because I didn’t put a ! in x’s name doesn’t mean I can force g! to not mutate a! = x. The body of g! doesn’t even know what the names x, y!, z! are, their instances were already assigned to a!, b!, c at the header. Variable names are fickle, if we need an instance to never mutate we need an immutable type.

This does happen. The compiler notices that an immutable type does not implement a mutating method, so it makes code that throws a MethodError. Thing is, compilation of a method only happens upon a function call that dispatches to it, and I think that is far too late for your liking.

2 Likes

Note that in Julia this kind of guarantee is particularly hard, because a user can overload an inner method that is called by a “non-mutating” function, making it mutating. I think you would have to combine that with giving up on generics at the same time:

julia> add_one(x) = x + 1
       function nomutationhere(x)
           return add_one(x)
       end
nomutationhere (generic function with 1 method)

julia> struct A
           x::Vector{Int}
       end

julia> a = A([0,0])
A([0, 0])

julia> add_one(a::A) = a.x[1] += 1
add_one (generic function with 2 methods)

julia> nomutationhere(a)
1

julia> a
A([1, 0])

I mean, when someone writes the nonmutationhere function, should he/she assume that nobody will never come up with a use for that function for a different type of object for which that function does mutate the object?

This kind of flexibility and composability clearly has its tradeoffs, but it does not seem clear to me if there is anything that could improve on this. I myself have some packages where I define const foo = foo!, for instance because the foo is mutating or not depending on the type of variable the user wants to use, and the alternative would be to have almost identical functions for each situation (and having both names allows the user to stick to the convention of mutability).

1 Like

I don’t much understand what you want to say. A call to a functions needs to enforce declaration of the side effect. How it is used is irrelevant.

Let’s take a simple example:

#using v1.8
f1(x)= x[1] = 2
f2!(x)= x[1] = 3
f3!(x!)= x[1] = 4
f4(x!)= x[1] = 5

v = [1]
f1(v) #wow, silently alter my vector!
f2!(v)
f3!(v) #error
f4(v) #error

I don’t find this behaviour too good.

The OP is not looking for an instance that never mutates. He wants a mutable instance with the assurance that a called function will not mutate it.

3 Likes

For flat arrays this is exactly the f(ReadOnlyArray(a)) solution proposed above.
Maybe, something more general for arbitrarily nested mutable structs, would also be useful…

3 Likes

Is not only for flat arrays is for any aother type that passes by ref.

Yes, I’ve noted that as well when I first commented. However, I believe that to be infeasible. A[i] = value and A.y = value are both syntactic sugar for method calls, specifically setindex! and setproperty!. If we somehow mark the variable A to not be mutated in a method, the compiler has to figure out whether methods like f!(A, B, C) and h!(a, A) mutate A’s possibly mutable instance and throw an error if any do. Hypothetically possible but it would take a lot of work (compilation takes longer) and there’s just no system even resembling that in Julia.

OP used a C++ example void g(int a, int& b, int& c) and someone else said it is analogous to g(a::Int, b::Ref{Int}, c::Ref{Int}). Well, not exactly, C++ and Julia treat variables and arguments very differently, so even that example is making an imperfect analogy between Julia’s instances and the C++ variables. In Julia, a = 0 has an immutable Int instance, so the only way a’s value can change is reassigning a different instance e.g. a = 2 or a += 1. In C++, you can change int a’s value without reassigning it at all:

#include <iostream>

void g(int& a2) {
    a2 += 1;
}

int main()
{
    int a = 0;
    std::cout<<a;
    g(a); //not reassigning a
    std::cout<<a;
    return 0;
}

// Equivalent in Julia requires a = Ref(0)

C++ is just one of those languages where a variable and its value are more…tied together. In languages like Julia and Python, variables are just nametags you slap onto an instance, and you can put on or take off as many nametags as you want. The closest analog of const C++ variables in Julia is instantiating immutable types. Any mutable struct type can have a struct version; Arrays aren’t mutable structs but ReadOnlyArrays is the struct version. The const in Julia (and the Final in mypy) is just not at all relevant, despite sharing a name.

1 Like

Those errors are because you forgot to rename x in the method body to match the argument x!. That’s why all the errors say “UndefVarError: x not defined”.

Also, by your proposal, shouldn’t all the mutated x be named x! instead, e.g. x![1] = 2? That way, you could accomplish what you want to a limited degree with “UndefVarError: x! not defined”.

3 Likes

Enforcing immutability would be useful even without static analysis - most Julia errors are runtime anyway. But this is very challenging to make work in the general case.
For example, this should error:

function f(x)
     a = first(x).a
     ...
     g(a[2])
     ...
end

function g(b)
    empty!(b[2])
end

f(Immutable(x))
1 Like

Still doesn’t work normal.
image

Well, you didn’t assign a global variable v!…as the error tells you.

1 Like