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

Closest thing I could think of is !ismutable(arg) || throw(error("arg must be immutable.")). Not sure if it’ll help your example, though, that looks like restricting variables again.

1 Like

This runs into the problem that, e.g., Set is immutable, while String is mutable, even though the observed behaviours are the opposite.

2 Likes

I correct it.

It’s possible to protect the fields of a mutable struct via similar technique. Here’s a prototype.

import Base: propertynames, getproperty, setproperty!

struct ReadOnlyStruct{T}
    parent::T
end
const ROS = ReadOnlyStruct
propertynames(ros::ROS{T}) where T = propertynames(getfield(ros, :parent))
getproperty(ros::ROS{T}, name::Symbol) where T = getproperty(getfield(ros, :parent), name)
setproperty!(ros::ROS, name::Symbol, x) = error("Cannot set property of ReadOnlyStruct")

mutable struct Foo
    x::Int
end
f!(a::Foo, b::ROS{Foo}) = a.x = 3
g!(a::Foo, b::ROS{Foo}) = b.x = 3 # violates read only contract

Demonstration:

julia> foo = Foo(5)
Foo(5)

julia> bar = Foo(6)
Foo(6)

julia> foo.x = 3
3

julia> foo
Foo(3)

julia> f!(foo, bar |> ROS)
3

julia> g!(foo, bar |> ROS)
ERROR: Cannot set property of ReadOnlyStruct
Stacktrace:
 [1] error(s::String)
   @ Base ./error.jl:33
 [2] setproperty!(ros::ReadOnlyStruct{Foo}, name::Symbol, x::Int64)
   @ Main ./REPL[9]:1
 [3] g!(a::Foo, b::ReadOnlyStruct{Foo})
   @ Main ./REPL[23]:1

julia> propertynames(foo)
(:x,)

julia> propertynames(foo_ro)
(:x,)

julia> hasproperty(foo, :x)
true

julia> hasproperty(foo_ro, :x)
true

For convenience only, one could then also define

f!(a::Foo, b::Foo) = f!(a, ROS(b))
g!(a::Foo, b::Foo) = g!(a, ROS(b))

julia> f!(foo, bar)
3

julia> g!(foo, bar)
ERROR: Cannot set property of ReadOnlyStruct
...

Someone could probably combine this into the @immutable macro.

An important concept here is that

  • assigning to a property (a_struct.property = value) is just a call to setproperty!(a_struct, :property, value)
  • assigning to an index (array[idx] = value) is just a call to setindex!(array, idx, value)
julia> @code_lowered f!(foo, bar|> ROS)
CodeInfo(
1 ─     Base.setproperty!(a, :x, 3)
└──     return 3
)

julia> @code_lowered g!(foo, bar|> ROS)
CodeInfo(
1 ─     Base.setproperty!(b, :x, 3)
└──     return 3
)

julia> h!(v) = v[1] = 5
h! (generic function with 1 method)

julia> @code_lowered h!(1:3)
CodeInfo(
1 ─     Base.setindex!(v, 5, 1)
└──     return 5
)

Making the array or struct virtually “immutable” is thus just a matter of overriding the “set” methods for the wrapper type. Essentially what we are doing here is leveraging the power of multiple dispatch to implement a form of immutable arguments. This is wonderful since this avoids having to add additional keywords to Julia.

Earlier you suggested that the macro, @immutable should be part of Base Julia or the standard library. I disagree since this functionality can exist as an outside package. Julia has a minimalist design philosophy to its Base and standard libraries. If functionality can exist outside the standard libraries, then it should. A free standing package is much easier to iterate and evolve than if it were a standard library. There are many examples of packages that were once part of Base or a standard library which have since be spun out for this reason.

While more extensive language changes may not be possible now for Julia 1.x, it’s possible that they could be part of Julia 2.x or another major version iteration. Still the preference is to prototype new functionality outside in a package first to establish what needs to be done and how it may work.

10 Likes

Wow tyvm for your time!

As was said before a couple of times:

Not the many ways of using a function is the problem, the lack of an enforced way of describing the side effects a function can have looking at its definition is.

Nothing in Julia can stop a determined user from thwarting one’s attempts to simulate an object as immutable:

julia> instanceFoo = Foo(0)
Foo(0)

julia> immutableFoo = ReadOnlyStruct(instanceFoo)
ReadOnlyStruct{Foo}(Foo(0))

julia> getfield(immutableFoo,:parent).x = 1
1

julia> immutableFoo
ReadOnlyStruct{Foo}(Foo(1))

julia> instanceFoo
Foo(1)

It just isn’t supported by the language and its semantics. I have grossly violated the ReadOnlyStruct’s internals here, but the point stands. Personally, I consider the ability to do this a feature, although one that needs to be exercised with extraordinary caution and only in very rare cases.

But it seems that the original poster’s needs and wants in this regard are not supported by Julia, nor will they be in the near or medium future, and they will perhaps be better-served by a different language.

3 Likes

BTW, also in C++ the const does not guarantee that r will remain unchanged. Change

to

void f(const vector<double> &r) {
    const double *p = &r[0];
    const_cast<double*>(p)[0] = 2.0;
}

which will happily compile a function modifying the first element of r.

Generally immutable, i.e. impossible to change which is partially supported by Julia, but not for arrays, and const are different, as for example discussed here

7 Likes

I recommend taking a look at the effect analysis work that Jeff outlined in his State of Julia 2022 talk at the 10 minute mark. This is mostly being spearheaded by @aviatesk as far as I understand.

See https://github.com/JuliaLang/julia/pull/46198 and related pull requests for more details. Besides the Base.@assume_effects macro there is also the Base.@pure macro that may be of interest.

2 Likes

But there is no way to automatically delegate dispatch to the wrapped type, so bar(::ROS{Foo}) won’t work, limiting the usefulness of this approach.

If we could automatically delegate all calls to the wrapped type, though…

2 Likes

The designer of a library/function is not in charge to fool people but to help and inform.

This post is not about how we can hack in C++ is about how to inform about intended behaviour and side effects in a function declaration in Julia. That was the scope of the example.

ReadOnlyStruct isn’t as substitutive as ReadOnlyArray, though. ReadOnlyArray is an AbstractArray that implements the entire AbstractArray interface minus the mutating setindex! by forwarding to the parent AbstractArray. Methods building on the AbstractArray interface that work on the parent will immediately work on the ReadOnlyArray. There isn’t any one interface for mutable structs that ReadOnlyStruct can utilize like that e.g. you can’t immediately getindex a ReadOnlyStruct{Dict}. Besides, a immutable struct can be used to implement a mutable type that doesn’t reassign its fields e.g Set, OffsetArray.

1 Like

I think you can say the same thing about Julia it just relies on documentation to tell the user of functions what the function does

3 Likes

Indeed, that’s the reason I started this thread, because the language doesn’t encourage/helps/forces this desired behaviour. This should not be coding guideliness / good practices should be forced and checked by the compiler.

I think what we’ve established is that there are very few languages that actually offer any real hard guarantees about mutability (because most languages offer some “unsafe” way to cast things into other things etc) so in essence these const adornations and similar are largely documentary in nature with a little bit of additional help for the developer of the function to remind them when they’ve done something they shouldn’t have done such as assign to a variable relatively innocently just forgetting that they were not supposed to.

So the real question is what exactly do you want to get from Julia from these proposed ideas? You want it to throw errors that the developer of a function be reminded that they’ve done something wrong? If so we’ve got macros in this thread which helped with that.

If you want to somehow communicate to the user of the function Julia’s preferred method has been to use documentation strings. Those documentation strings are just something that languages like c++ simply don’t have because c++ has no interactive repl so there’s no clear way to query the “runtime” for what a function does. There are some IDE type aids but Julia has those as well.

In Julia if the function ends in exclamation point this is a big sign that something may be changed. To find out what you simply type ? functionname and you get it’s documentation which should tell you what things can change and what things don’t so for the user I don’t see that we have any issue except that people could write bad documentation, but people could write bad documentation in any language, and people could fail to adorn consts and etc.

5 Likes

Yes we had. I think realistically only Sing# a dialect of C# aspires to do this. But it doesn’t matter that we don’t have to defend that some malicious library implementer will cast something in implementation. We should care that expected behaviour is achieved. For example, in my case, I cast away constantness in some cached objects to load cached data from the disk. This doesn’t mean that I’m cheating casting but that I’m accelerating a calculation that was already done. that cast is done to set some flags in the wrapper about caching not the main expected behaviour. Are many examples why sometimes, is desired to cast away constatness that are not interested to discuss here because all this line of discussion about trickery, malicious behaviour, strength of languages are deeply out of my intended scope.

Yes, but what do you want Julia to do? We have established that it’s possible to create code which aids the implementer in avoiding accidental mutation, and we have established that the normal method for communicating mutation behavior is documentation strings and a ! flag to let the user know there is expected mutation of some sort.

What else do you actually want Julia to do? I don’t really understand.

2 Likes

That’s OK if you don’t understand. Read the initial post again.

Fine, going back to the first post:

So we’ve established that there is a way to accomplish this… someone wrote a macro to do it elsewhere in this thread.

And we’ve established that the Julian way to do this is to provide a docstring that specifies the behavior of the function and what it mutates.

If you don’t like these things, can you say exactly what it is you’d like julia to do instead?

For example, I could imagine you wanting an error right after typing something like this:

function foo(a,b)
   a[1] = 1
   b[2] = 2
end

Because there’s no ! in the function name…

or right after something like this:

function foo!(a,const b)
   a[1] = 1
   b[2] = 2
end

because b was declared “const”

or something like that?

3 Likes

That’s certainly a valid opinion, one that is implemented in some languages. But I think people have laid out some big reasons why it’s not in Julia, which is your original question. Let me try to summarize the ones I’ve run across, I’ll rename your desired const as Cpp_const so nobody confuses it with Julia’s current const:

  1. Like Python and very unlike C/C++, a Julia variable is not tied to an instance; an instance can be assigned to many variables, and a variable can change its instance (even the type) through reassignment. Is an instance allowed to be mutated if it assigned to a variable declared Cpp_const and another variable that isn’t? What if it is cyclically unassigned then reassigned to the Cpp_const variable, does it make sense for it to mutate then?

  2. To enforce no attempts at mutation, one approach is to prove all used methods are non-mutating. However, Julia is not a statically typed, AOT compiled language. Arguments in methods often aren’t annotated with concrete types. So the compiler doesn’t have the necessary type information to even know what methods are used until a function is called at runtime with live input instances. This system gives us a lot of flexibility in composing code while only compiling what we need, but the method check happens way too late to smoothly enforce nonmutation.

  3. Another approach is to somehow freeze the instance assigned to the Cpp_consted variable. So what if it has a mutable type, just tell the compiler to not change any of its bits! That’s simple enough, right? Wrong, a Julia type often don’t “own” all of its data bits. An immutable struct with a mutable field doesn’t actually contain that mutable instance, just a pointer to it. Two different immutable struct instances can contain the same mutable instance by both having a field with the same pointer value. What happens when one instance is Cpp_consted and the other is being mutated? Is that mutable field’s instance frozen for both? It’s not so different from the separation of pointers and their values in C++; here’s a read about how their const-ness are separate.

  4. Given how Julia treats its variables (see point 1) and how Julia doesn’t have pointers and references everywhere, Julia has a way to control mutability and immutability: as a property of a type. It’s usually dictated by the type definition struct/primitive vs mutable struct, but there were many exceptions pointed out. The only foolproof way is the documented description and methods of the type; for example, a Set is a struct, but it has a push! method that is unambiguous mutation. Some mutable types will have immutable counterparts e.g. Dict and ImmutableDict. It’s possible to make wrapper types to implement immutable counterparts in little code, but there’s limitations. The ReadOnlyArray is a pretty good way to wrap AbstractArrays into an immutable substitute, but it won’t work for everything e.g. methods specialized to other wrappers like OffsetArray won’t work on ReadOnlyArray.

2 Likes

There is not yet a way this is what we established. For neither problem.

What macro we have that works?
What is mutable here: g!(a, b, c) ?

How this should be writen using that macro to assure a user that a and c are changed, for example, and how the compiler forces a implementer to do so? I still don’t understand how a mutable type (one that has mutable fields) will behave using that macro.

And we’ve established that the Julian way to do this is to provide a docstring that specifies the behavior of the function and what it mutates.

When the “Julian way” is not the strong way relying and not the language and compiler to do this but conventions and comments. I dont feel this is good enough solution by a large margin.