A possible change for Julia 2.0: per-argument mutation ! marks?

This is just a brain-dump.

I find the convention of putting a ! in a function name to indicate a parameter might get changed to be less than satisfactory.

Because consider this change!(a, b). What if the function changes b but not a? What if it changes both a and b? There’s no way to indicate that.

For example in Julia 2.0, could we do something like this

  • using the keyword not instead of ! for negation
  • allow the ! to be specified per argument and dispatch on that; explained below

For bullet point 2 I am thinking

function change(a, b) doesn’t mutate the input args at all
function change(!a, b) is a function that mutates a and must be called by change(!a, b)

similarly function change(a, !b) only mutates b and function (!a, !b) mutates both a and b and the compiler can check within the body if a or b are mutated.

Obviously not a language designer. So might have missed something big.

3 Likes

That would be such an enormous change to how the language works that I think this applies: PSA: Julia is not at that stage of development anymore

5 Likes

Unlike function names, argument names in the definition are not visible in the function call, so it’s a much worse hint. Maybe it’s not a hint at all because we have to actually look up the function, figure out its dispatch at that call, and check the method’s arguments. At that point, I could just read the docstring and figure out which argument is mutated or possibly mutated, and there’s no need to write ! everywhere.

If I really needed to indicate what gets mutated throughout my code, it should be apparent in the variable name or a comment. If I for some reason want an indication on every variable, there’s no need to change the parser to allow leading ! because I can just append any number of them to variable names.

Why is this useful? What happens when the mutations are runtime decisions? We could instead say that !a means that a could be mutated by the method, but that’s a very significant difference.

But the totally original language 2lia is at that stage.

4 Likes

Some time ago I proposed using change(a!, b) (or change2(a, b!)) to indicate what was getting changed. It is perfectly legal to use right now.

11 Likes

Ah nice one. Putting it in the back makes sense and is not breaking.

1 Like

No need to sound harsh. Makes me feel stupid. Like “why would u even do that?!”

I mean like statics analysis and conventions help prevent mistakes.

Just because there are alternative eg read up fix strings doesn’t mean an idea is totally bad. Like python can call numba and so why need Julia? Imagine someone making such a claim.

I don’t understand why you had such a negative reaction to the exact same suggestion as that offered by the following comment. If it was my inability to imagine a reason for a convention marking variables assigned to mutables, then it’s not at all an insult of anyone else. If you’d like, you could provide a reason, perhaps an example of the mistakes that could be prevented by static analysis of such a convention.

On a tangent, I wish we had gone the route of functions appended with ? return a boolean.

7 Likes

I like the way Swift does it, but I am aware that Julia can’t adopt it.

func doubleInPlace(number: inout Int) {
    number *= 2
}
var myNum = 10 
doubleInPlace(number: &myNum)
1 Like

This is a bit different — this is to pass an argument by reference, so that you can mutate the binding of the argument when the argument is otherwise an immutable value. It’s not needed, even in Swift I believe, if the argument “points” to a mutable object whose contents you want to change. (Make sure you understand the difference between assignment and mutation.)

The analogue of &arg in Swift (which comes from C) in Julia is Ref(arg), though you then have to access its contents in the function as arg[] (much like *arg in C).

In fact, it would be perfectly possible to make &x valid syntax for Ref(x), since this is invalid syntax at the moment. I once created a PR to do just that — make &x sugar for RefValue(x) by stevengj · Pull Request #27608 · JuliaLang/julia · GitHub — but no consensus was reached.

Part of the reason is that this is simply not needed much in Julia. If you have multiple immutable things you want to return, you just return a tuple (unlike C which has no easy way to return multiple values from a function), and this is cheap. As a result, people hardly ever use Ref to simulate mutable arguments in Julia (except when doing ccall to other languages).

6 Likes

It’s possible we can do this without breaking the language with better tooling!

I can imagine a world in which the ? help menu, or docs in general can query the mutation of various arguments. Then our documentation and other tooling can display mutation in some way (highlighting, inserting a grayed out character like a bang at the end as you’re saying).

It’s then also possible that the compiler tools could propagate this mutation info around and display it in more places.

7 Likes

Others have already commented that enforcing something like this is a pretty fundamental change in the language. But your intuition that this type of stuff is potentially desirable is of course correct, as evidenced by many experiments and languages that are trying to innovate in this field.

Rust, for example, distinguishes between passing (borrowing) an argument in a mutable and immutable way in order to make sure that there either is only one reference to an object, or all references are immutable. This “shared-xor-mutable” rule has profound consequences (e.g.: Captcha Check) in making it possible to verify properties of your code locally. It’s also shared by the other two examples:

Mojo also intends to follow the Rust model, but make it more palatable and simplify it for the programmer: Ownership and borrowing | Modular Docs I haven’t played with it enough to say whether I’d consider this successful.

The language that I know of that seems to most directly go in the direction you envision is hylo ( https://www.hylo-lang.org/ ) which explores mutable value semantics. It actually requires explicit markers whenever a value is mutated. A function has to declare that it wants to mutate a particular variable (inout) and when you call the function, you have to denote that the function may mutate the argument with an &:

fun offset_inout(_ target: inout Vector2, by delta: Vector2) {
  &target.x += delta.x
  &target.y += delta.y
}

# Arguments to inout parameters must be mutable and marked with an ampersand (&) at the call site:

fun main() {
  var v = (x: 3, y: 4)               // v is mutable.
  offset_inout(&v, by: (x: 1, y: 1)) // ampersand indicates mutation.
}

# [...]

The compiler guarantees that the behavior of target in the body of offset_inout is as though it had been declared to be a local var, with a value that is truly independent from everything else in the program: only offset_inout can observe or modify target during the call. Just as with the immutability of let parameters, this independence upholds local reasoning and guarantees freedom from data races.

3 Likes

Thanks for the info. Interesting to see hop doing this