Explicit denotation of variability and immutability

Thanks for the explanation, I see what you mean now, and you have a point.

My point was that the following should be equivalent (identical machine code), except for call-site syntax in C++:

mutable struct M
    v::Cint
end

function f(x::M)
    x.v=2
    return
end

void f(int &x) {
    x = 2;
    return;
}

void fptr(int* x) {
    *x = 2;
    return;
}

And julia does not support C+±like syntax, which is a decision that makes me happy; it is as explicit as C.

I came across the implementation of immutability in the D language; it is very similar to my proposal:

see also:

esp. the section “Immutable Type”.

I don’t know D, but I find it very hard to map the concept of a reference (as implied by that answer) to Julia.

Some related stuff on Julia Github (https://github.com/JuliaLang/julia)

I am just providing examples for treating mutability/immutability in other languages. That might help to get an idea, but they may not be necessarily applicable in Julia.
We have to give the idea a lot of thinking before it crystalises. In this regard, experience from the design of other languages, like Haskell, Rust, or D, might be helpful.

It also shows clearly that this is a very important issue in modern languages.

You seem to be under a misapprehension about the development stage of Julia. It is not “in its infancy” and it has already quite thoroughly crystallized. We are not spitballing about the basics of how the language works at this point. We are currently in the process of working out the very last API changes for the 1.0 release.

As I explained in my earlier post, your proposal is not merely some additional language or compiler feature, it is fundamentally incompatible with how types work in Julia. The design of not having separate static and dynamic types was decided upon eight years ago and has not changed since then. It is deep in the identity of the language.

While you’re welcome to continue to speculate about type systems with these kinds of features, please be aware that you are speculating about hypothetical languages that have minimal relevance to Julia.

7 Likes

Thanks for the notice – though it sounds rather harsh. I supposed that being in version < 1.0 means being “in infancy” – sorry for the misinterpretation.

My hope is that such a radical proposal or speculation would help shape the future of the language in some way – I remain optimistic.
The items from the Julia Github indicate, imo, that the concept of immutable types is still under consideration/debate anyway, and novel ideas might arrive to change the language.

BTW, could you please explain the place of Immutable Arrays or Immutable Dicts in the type hierarchy? You mentioned earlier that “whether a value can be mutated or not is a property of its type and that type cannot change”; so I cannot see, for instance, the relative position of Immutable Arrays compared to Arrays in the type hierarchy.

Immutable types are well and good. It’s the notion of casting between mutable and immutable types or changing the behavior of a value with type annotations which are non-starters. Immutable and mutable arrays are just different kinds of arrays, both subtypes of AbstractArray. There is no issue with having some arrays that are mutable and some which are immutable. What you cannot do is have a mutable array and turn it into an immutable array or vice versa. If an object has one type, that is always its type, and the mutability is part of the type.

5 Likes

This

seemingly contradicts what you said before,

Could you elaborate on that?
Is there a general mechanism in Julia (like a macro) with which one can “derive” an immutable object from a mutable one?

What do you think about the first part of my proposal, distinguishing between assignment and equality in the syntax?

How so? Mutability is a property of the type of something and you cannot change the type of something. Therefore you cannot change whether something is mutable or not.

Interestingly, making a mutable wrapper around an immutable object is actually useful and efficient, even though at first blush it seems to violate immutability.

Could you elaborate on that?

I wrote a paragraph about it and linked to a rather long issue discussing the technique in detail. What specifically would you like more detail about?

Is there a general mechanism in Julia (like a macro) with which one can “derive” an immutable object from a mutable one?

No, not automatically, at least not at this point. I would potentially be possible through metaprogramming and generating types.

What do you think about the first part of my proposal, distinguishing between assignment and equality in the syntax?

Julia already distinguishes assignment syntactically from both mutation and equality. There are the following assignment-like syntaxes:

  • x = ... is assignment. The name x is bound to the value that the ... evaluates to. Local bindings never leak out of their scope. It doesn’t matter what x was bound to before this happens, that value is not affected in any way. No object is mutated by this and no other binding besides x is changed.

  • x.f = ... is equivalent to setproperty!(x, :f, ...) which, by default mutates the object x by changing its field f to the value of the expression .... If x is visible in another scope or by another bindings, this change will be seen everywhere. No bindings are changed by this.

  • x[i] = ... is equivalent to setindex!(x, ..., i) which, for arrays mutates the array x by changing its ith slot to refer to the value of the expression .... If x is visible in another scope or by another bindings, this change will be seen everywhere. No bindings are changed by this.

There are equality operators == and === which check for value-based equality and identity-based equality, respectively. There is also already a syntax for creating a constant binding as opposed to a variable binding:

const x = ...

The const annotation is not currently supported in local scopes. But as I said, that’s a fairly straightforward feature which has not been added because it has limited benefits – both humans and compilers can easily tell if a local binding could potentially be reassigned in the same scope or not. That is, of course, not the case for global bindings which is where the const declaration is really useful.

We could have bindings constant by default, but decided (consciously) not to. In local scope, defaulting to constant bindings is just annoying and has little benefit – if you want a constant local binding, just don’t re-assign something; as a human or a compiler, it’s easy to see if a local is assigned multiple times. In global scope it would be quite beneficial for bindings to be constant by default. However, this would be very annoying for interactive usage in the REPL where it’s really common to reuse and overwrite variable names. Since code that depends on non-constant global bindings is also usually quite slow in Julia (precisely because it’s hard to analyze statically), there’s a rather strong incentive not to use non-constant global bindings in code where performance matters at all. Some have suggested that this performance hazzard is actually a feature since it discourages the use of mutable global state.

10 Likes

there is also:

x(i) = f(i)  # f could be undefined here

what happend here?

Let me first emphasise that I mean no radical change to the fundamental structure, type system, or the logic of Julia – it is already a lovely “progressive” language. My proposal is meant towards a more readable and safer syntax for translating computational algorithms into Julia. How it would be implemented under the hood is a matter for core developers and the more knowledgeable part of the community – I am no computer scientist, but coming from the math realm.

  1. Most succinctly, what I propose is certain explicit syntactical safeguards to denote mutation of values and bindings (mutations of the state). I firmly believe that it leads to much better coding style in the long run, especially benefits concurrent/parallel programming.

For example, according to the proposal, by

a = 2
b = Array{Int32}(10)
c = ImmutableArray{Int32)(5, 5)

the programmer and the code reader (plus the compiler) will be sure that a is an immutable binding to the value 2, b is an immutable binding to a mutable Array, and c is an immutable binding to an immutable Array. If, somewhere else in the code, such decisions/pledges are violated, the compiler will throw an error or a warning.

Mutability will be then explicitly marked up, as in

v::Integer := 2
w := -1.5
z.c0 := f
z[i] := 32a + h(2)

where everybody (along with the compiler) is informed that v is intended to be a mutable binding (but with immutable type) to a value 2, w is a mutable binding (with mutable type) to a value, -1.5, and z.c0 and z[i] values are intentionally mutated.

Using := for mutation and = for constant binding (in the corresponding global or local scopes) does not hurt any fundamental principle of Julia, or a quick-scripting user of the interactive mode. In fact, it promotes better coding style which is also closer to the mathematical syntax and much safer (since the compiler knows the intent of the programmer and can warn her if needed).

  1. For further safety inside function bodies and easier reasoning about the code, I see a strong need for an explicit syntactical mechanism like the intent keyword of FORTRAN (see above); for instance, by a construction like
# a pure function
function fp(a::Int64, s::Set{Int32})  
    #... do some computation ...
end

the programmer explicitly promises not to mutate the bindings of the arguments or their contents in the function body, so that

a = 2
a := -1
push!(s, 9)
s = Set([1, 2])
s := s2

will produce compiler errors.
In contrast, a definition like

# an impure function
function fip(var a::Int64, mut s::Set{Int32})
    #... do some computation ...
end

allows

a := 2 
a += 1
push!(s, 9)
a = 2  # `a` becomes constantly bound thereafter

where var and mut are “flags” to show explicitly that the programmer intends to mutate the binding/value of a and mutate the contents of s with this function.
This is also not against any fundamental principles of Julia, afaiu, but leads instead to much safer codes, which are very easy to reason about by a human.
[Note: I am not insisting on this particular notation; better notation could be devised for this purpose.]

  1. I think there is a need for an in-built mechanism to map mutable types to immutable types automatically; e.g., suppose that somebody has built up a library which includes a mutable type List with a push method to append elements to the List. Then something like
typealias ImmutableList Immutable(List)

will generate an immutable-List type which can only be initialized; moreover, any mutation method built for List will produce an error when applied to ImmutableList; e.g., after the generation of the immutable type above, and defining

lm = List([1, 2, 3])
lim_1 = ImmutableList([5, 6, 7])
lim_2 = ImmutableList(lm)  # an immutable copy of `lm`

either of

push!(lim_1, 8)
push!(lim_2, 4)

will produce a compile error, while

push!(lm, 4)  # does not mutate `lim_2`

works fine.

This is the only radical part of my proposal, if at all. I am not sure how this could be implemented in Julia or if this violates Julia’s foundations. Yet, that seems to be quite a useful mapping.

Implicit mutation is the “dangling pointer” of modern languages. :innocent:

If it is just a syntactical layer over Julia, have you thought about implementing this either via metaprogramming, or taking something like https://github.com/JuliaLang/JuliaParser.jl, and adapting it to the syntax that you want?
The fact that Julia lowers the Julia syntax to Exprs and then does the rest of its magic on the Exprs means that it is incredibly flexible in that regard. (again, kudos to the four creators of Julia!)
Somebody has even done a radically different syntax based on Clojure over Julia.

I think that would be a much more productive path towards reaching your goals.
If you do it well, maybe you’ll then be able to attract a larger community around your more explicit variant of Julia.

8 Likes

That’s a good recommendation. However, I do not have the know-how or experience to build such a parser. Could you refer me to some references which might help?
Currently, I am still waiting for critiques, suggestions and improvements to my proposal.

1 Like

It sounds like much of what @SepandMeenu wants could be obtained if a @=... expanded to const a = ... . Infix operators don’t seem to produce infix macros, though:

←(a,b) = a+b
1 ← 2

macro (←)(a, b) println("got ",a," and ",b)  end
@← 1 2

But it should be simple to make a macro which acts on all and = in your code. With MacroTools.jl?

As I’ve said, there’s already a syntax for this: x = means what you propose x := should mean and const x = means what you propose x = should mean. Playing musical chairs with this syntax seems like fairly pointless endeavor that doesn’t actually fix anything and will make the language considerably less familiar to programmers coming from most mainstream languages. It is also way too late in the game to do this since it would break every Julia program everywhere. The only feature missing for bindings is that const x = is not currently supported for local x, but as I’ve explained several times, it’s not particularly important and will be added in the 1.x series.

This whole proposal seems predicated on the idea that function arguments work like they do in Fortran or like pass-by-reference arguments do in C++ where assigning to a in your example changes the caller’s a. That is not the case – that never happens in Julia. A function cannot affect the local bindings of its caller. If a function assigns some other value to an argument a it only affects what a refers to inside that function. We don’t need to annotate this because it’s not a problem in the first place.

Regarding the mutation of s, that’s a different story. A function can mutate an object passed to it, which will be visible to the caller. There is already a syntactic convention for this in Julia: functions that mutate one or more of their arguments have names ending in ! as in push!. What you seem to want above and beyond this is an explicit annotation of which arguments are mutated. Currently, that’s a matter of documentation.

If you want to enforce that a function only mutates arguments which are annotated for mutation, that’s vaguely plausible, but it’s actually quite difficult and maybe impossible in a language with as powerful dynamic dispatch and higher order programming as Julia. It would probably have to be a runtime check rather than a compile time check, although it might be possible to check some subset of cases statically. Adding this feature would complicate the implementation of the language considerably and seems of dubious value considering that the ! mutation convention has been quite effective without incurring any implementation complexity.

I’m not really sure this makes much sense. The implementations of immutable and mutable data types that are conceptually similar are often quite different. This is certainly not something that is going to happen in Julia 1.0. Perhaps something like this could happen in Julia 2.0.

4 Likes

For what it’s worth, I think if you spend some more time programming in Julia (or a language where this sort of thing works similarly such as Python) you will gain some distance from C++ and may no longer see this sort of thing as useful. In C++ explicit statements of variability and immutability are deeply ingrained in the way we think about things, but frankly in Julia a lot of that stuff seems rather pointless.

4 Likes

#11902 and #21912 are in that spirit.

1 Like

I believe that a syntactical convention being “less familiar to programmers coming from most mainstream languages”, e.g., as you say here

should not pose a problem to Julia designers/learners. What you say in the next paragraph,

clearly shows that Julia is not afraid to have its own conventions – be they different from the mainstream or not. And I think programmers can learn – as we learned to say farewell to once-common GOTO.

It is neither “playing musical chairs”, nor “fairly pointless endeavor”, Stefan. It’s an endeavour for the clarity of syntax of computational code, and its better compliance with mathematical notation. The essence of my proposal is, as I have mentioned several times, a modification of the syntax to make mutations explicit in any scope. The stylistic convention to add ! to the name of functions which mutate one of their arguments is clearly in this direction. I just say: Let’s generalize this nice decision systematically. I think, sadly, you are underestimating the value of such an explicitness. :frowning_face:

Note that I do not insist on its enforcement either; I would be happy even with warnings. In this way, you’ll not break any previous code, but promote a better style of code for the future where mutability in binding or value is explicitly denoted. If the programmer intends to mutate something in any scope, she should make that explicit.

Unfortunately, I don’t have an idea how difficult it can be to implement. That’s far above my knowledge.