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

It’s not done by the compiler, but you could always do:

a = [1, 2]
b = [3, 4]
c = [5, 6]
"""
    g!(a, b, c)

Warning: mutates the second argument
"""
g!(a, b, c) = b[2] = 1
g!(a, b, c)

and then you would know that whenever you saw a function with a ! in the name you could check which are mutated by writing ?g! in the REPL to check the docstring.

6 Likes

This should be solved by language constructs like most of the modern languages and not comments that some people adds them some not.

Unless you go the Fortran route and disallow aliasing, you can’t really make guarantees here.

2 Likes

But in C++ you cannot actually tell either. You know that b and c can be changed, but not that they are.

In julia

g(a::Int, b::Vector, c::Vector) 

you can similarly tell that b and c can be changed, but not whether they are.

I am not certain what counts as a modern language. Are there many modern dynamic languages with this feature?

8 Likes

Assuming the function author follows the Julia style guide, a should be mutated: Style Guide · The Julia Language.

2 Likes

But it could be that more than one argument that is mutated.

And then there are cases like rand! and map! where multiple conventions collide.

1 Like

you can similarly tell that b and c can be changed, but not whether they are .

In C++, for example, if they are not const they WILL be changed. I assume in many other languages too. In Julia is not clear if they are intended to be mutable or const nor enfocerced by the language or compiler. The person who makes the call cant guess what happens looking at the function call with its data specially if the name has an exclamation sign.

I see this discussion going nowhere.
I think your point is clear, but also the answer: it isn’t currently possible.

I also believe that if this isn’t possible, there is a reason “blocking” it.

What would be interesting instead would be to have a comment on what is blocking to have this language feature and if there are plans (in Julia 2??) to remove this bloccage. …

13 Likes

Well, this is it, though: the language doesn’t let us express (im)mutability of function arguments. In Rust there’s special syntax to say “this argument is mutable”, but Julia doesn’t have this.

The compiler can’t help you with this, so, unfortunately, you have to rely on comments and meaningful variable names.

2 Likes

Excuse me, but how do you know that? Doesn’t the code actually have to perform some sort of mutating operation on the variable? The act of not putting const in the signature surely does not change the argument all by itself.

8 Likes

I don’t fully agree: You can tell if an argument is mutable or immutable by inspecting its type. So saying “this argument is mutable (or immutable)” is possible. But you cannot take any arbitrary mutable object and force it to be immutable. That might be useful, perhaps, having an annotation that guarantees that mutable object are not mutated.

1 Like

I got out the manual and after a few tries, managed to write the macro I referred to earlier:

"""
    @immutable x 
    @immutable x y z 

Declare function argument(s) immutable, and generate an error if an attempt is made to mutate it/them.

Invisibly wraps the named argument(s) in a `ReadOnlyArray` wrapper, preventing accidental modifications 
to the arguments' contents.  If the argument is other than a mutable `AbstractArray` the macro is a no-op.

## Example

    function f!(x,y,z)
        @immutable y z
        # Code which mutates x and uses values in y and z
    end
"""
macro immutable(xs...)
    local q = Expr[]
    local t
    for x in xs
        push!(q, 
        quote
            if isa($(esc(x)), AbstractArray) && ismutable($(esc(x)))
                t = $(esc(x))
                $(esc(x)) = ReadOnlyArray(t)
            end
        end)
    end
    return Expr(:block, q...)
end

A small demo:

julia> using ReadOnlyArrays

julia> function f!(x, y, z)
           @immutable x z
           @show typeof.((x,y,z))
           y[2] = 2
       end
f! (generic function with 1 method)

julia> x = (1,2,3,4);

julia> y = rand(4);

julia> z = rand(3);

julia> f!(x, y, z)
typeof.((x, y, z)) = (NTuple{4, Int64}, Vector{Float64}, ReadOnlyArray{Float64, 1, Vector{Float64}})
2

julia> y
4-element Vector{Float64}:
 0.5389943545876524
 2.0
 0.4961355278094852
 0.10676683774485696

Note that within the function the type of the tuple x wasn’t altered by the macro. Only z being a mutable AbstractArray was wrapped in a ReadOnlyArray.

I know this isn’t the complete answer to the OP’s post, but it would allow one to annotate and enforce immutability of function arguments. It may be worth a small package or perhaps a PR to ReadOnlyArrays.

10 Likes

No… it is not exactly like that, a const object in C++ can be changed, as a Julia one can (but is a little harder to do it) because it can have a pointer to some memory that can be changed even if the object itself is const. So, in the end, this is only better than Julia in the sense that most sensible programmers will annotate a parameter with const if it does not change state in any way, but not-so-sensible programmers can forget to add const to unchanging parameters, and hazardous programmers can mutate the state of const objects. In the end you are just relying on the prudence of previous programmers.

This is a problem to be solved by documentation.

Why?

  1. No language does that automatically, they always rely on the user annotating the parameter (or using a different object type) to guarantee constness.
  2. To suggest Julia do that, you need to understand Julia’s model, and have a good proposal how do you add that to the language. Julia’s model has no good way to implement that without breaking other parts of the language (or possibly being a performance hindrance). “Most modern languages” as you mention have a model in which the variable is a memory position, in Julia it is a binding, one very mainstream language that have a model that gets close to Julia is Java, and, surprise, surprise, Java also do not have a way to do what you want. The recommendation would be basically the same as here, create a immutable wrapper or use an immutable object.
6 Likes

Thank you for the example.

Is again, a ReadOnlyArray my arrays are not so. Are big and mutable.

Dan, from your comment it sounds like you aren’t appreciating exactly how ReadOnlyArrays can be used for your purposes. Please consider the following example function, where I intend to mutate the first argument to be the reversed cumulative sum of the second argument. The second argument is annotated with @immutable to ensure it is not accidentally mutated in this function:

function revcumsum!(a::Vector, b::AbstractVector)
    @immutable b  # Disallow mutations to second argument
    length(a) == length(b) || throw(ArgumentError("arguments must have same length"))
    a .= b
    cumsum!(a, b)
    reverse!(a)
    return a
end

Now we exercise the function on an array of one million elements. Note that both arrays are mutable outside of the function:

julia> b = collect(1:1_000_000);

julia> a = similar(b);

julia> revcumsum!(a, b);

julia> b[1:5]
5-element Vector{Int64}:
 1
 2
 3
 4
 5

julia> a[1:5]  # contains the reversal of the cumulative sum of b
5-element Vector{Int64}:
 500000500000
 499999500000
 499998500001
 499997500003
 499996500006

julia> b[1]  # Still retains original value
1

julia> b[1] = 0  # I am mutating b 
0

julia> b[1:5]  # We see that the mutation occurred as requested
5-element Vector{Int64}:
 0
 2
 3
 4
 5

The point is that within the function, and only within the function, b becomes immutable. Also, we see that the function does not perform any memory allocation:

julia> using BenchMarkTools

julia> @btime revcumsum!($a, $b);
  1.390 ms (0 allocations: 0 bytes)

And if we make a similar function that omits the @immutable statement:

julia> function revcumsum2!(a::Vector, b::AbstractVector)
           length(a) == length(b) || throw(ArgumentError("arguments must have same length"))
           a .= b
           cumsum!(a, b)
           reverse!(a)
           return a
       end
revcumsum2! (generic function with 1 method)

julia> @btime revcumsum2!($a, $b);
  1.408 ms (0 allocations: 0 bytes)

the execution time is essentially identical to the original function that includes the @immutable statement. The execution time cost is negligible.

So to summarize, the point of @immutable is to allow you to annotate and disallow mutation of mutable arrays (of any size) within functions that you define. That way the function caller does not have to do anything special. The presence of the @immutable statement (typically and preferentially) on the second line of the function permits a quick inspection of the function to reveal which arguments could or could not be mutated.

That is the primary use case that I was thinking of. If I understand your wishes correctly, I believe that this goes at least partway (i.e. for functions that you yourself write) towards the goal you seek.

5 Likes

tyvm for your time to write this example! @imutable should be on Julia’s library.

It solves some of the problems pointed by me but is not a professional solution for large scale development:

  • Objects with mutable fields are not protected.
  • The ‘!’ decorator is still optional for mutable functions and this should be enforced even if I deeply dislike this solution.

Sad … quite very sad and crippling for large scale software development. This togheter with inability to create standalone executable and inability to run scripts fast makes it useful just to replace eventually R, Matlab or Mathematica and never become a big language. No wonder the rate of adoption is so slow and Go and Rust, comparable old languages, are flying when compared with Julia.

Well, based on this discussion, it seems that other languages cannot do it either, or only partially :person_shrugging:

Standalone executables are under development and will be available in the not too distant future, and latency is actively worked on and has improved drastically over the last few years.

5 Likes

Boy, that alone would not be a small feat!

11 Likes

C, C++, Rust, C# are some languages that are doing it even Python tries hard to solve it somehow because is so important.