Inconsistent rebinding rules for function arguments

The function below is a minimal example of a bug I have seen a couple of times:

function update!(x)
  dx = [1]    # Compute change
  x += dx     # Update x  (developer intended to write x .+= dx)
  # Any tests here show that x is correct.
  return dx
end
x = [5];
update!(x)
# Any tests here show that x is incorrect.

What happens is that the local binding of x is implicitly changed at the line x += dx. It would be nice if julia required one to make this rebinding explicit, as in local x += dx, but interestingly this gives:

ERROR: syntax: local variable name "x" conflicts with an argument

That is, Julia allows implicit rebinding of function arguments, but not explicit rebinding? To me, explicit rebinding should be mandatory to simplify debugging.

I don’t see the problem. x += dx is (syntactically) equivalent to x = x + dx which clearly rebinds x to the object that x + dx evaluates to.

Did you mean to use x .+= dx?

Here is the lowering:

julia> Meta.lower(Main, :(x += dx))
...
1 ─ %1 = x + dx
│        x = %1
└──      return %1
))))

which is equivalent to the lowering of :(x = x + dx).

Edit: Just noticed the (developer intended to write x .+= dx) comment which answers one of the questions I had.

3 Likes

The inconsistency I am pointing out is that you can do implicit rebinding:

function update!(x)
  x += dx  # or x = 10
end

but if you try to make the rebind explicit:

function update!(x)
  local x += dx    # or local x = 10
end

you get a syntax error:
ERROR: syntax: local variable name "x" conflicts with an argument

Okay, I can see I have been misunderstanding the local keyword. I thought

function foo(x)
  # some code
  local x += dx
  # some more code
end

would be the same as

function foo(x)
  # some code
  let x=x
    x += dx
    # some more code
  end
end

That is, the location of local decided where the new local scope began. This is not the case, local x basically just means do not look in parent scope for x. So there is no inconsistency after all… Just a small handful of people that occasionally forgets the dot in x .+= dx…

Can you, by any chance, lock a binding so that you got errors coming out if you tried to rebind? This would also be nice for mutable structs…

There’s an open issue somewhere about implementing const in local scope. The same could be allowed for fields in a mutable struct to make those fields write-only. They’re both good features that will get implemented at some point but they’re fairly low-priority because they mostly don’t enable anything that can’t already be done. The compiler is able to see whether a local is assigned or not (cf a global where that’s impossible to know statically). The const annotation on fields of mutable structs is a bit more useful since you can avoid redoing field lookups if you know you’re accessing the same field in the same object.

3 Likes

@StefanKarpinski : Sound very good with the const. Will it work like local that you just specify it anywhere in scope, and then the binding cannot change within that scope?

function update!(x)
  const x
  x += dx  # could throw "ERROR: x is const, did you mean x .+= dx?"
end

From a C-programmers perspective, the choice of keyword const can be interpreted both correctly as char * const a; (binding cannot change) and incorrectly as const char * a; (value cannot be manipulated via the binding). So it may create some confusing. I have no better suggestion though…

Note that there already is a const keyword that can be used on global variables and already means that the binding cannot change.

2 Likes

In Julia, that should not be a problem; as usually we are talking about the value, not the contents. Cf

julia> struct Foo # immutable
           vec
       end

julia> foo = Foo([1,2,3])
Foo([1, 2, 3])

julia> foo.vec = [4,5,6]
ERROR: setfield! immutable struct of type Foo cannot be changed
Stacktrace:
 [1] setproperty!(::Foo, ::Symbol, ::Array{Int64,1}) at ./Base.jl:21
 [2] top-level scope at REPL[13]:1

julia> foo.vec .= [4,5,6]
3-element Array{Int64,1}:
 4
 5
 6

Indeed, it would only refer to the binding, it cannot change the mutability of the value that is bound—that’s not how Julia’s type system works.

Just for the record const char * a; also does not change the mutability of the value that is pointed to. It only prevents mutations via that particular binding. So the two possible interpretations of const still apply to Julia’s type system I believe… Nevertheless, as @kristoffer.carlsson mentions, the meaning of the keyword has already been fixed by its use on globals so I don’t think there is anything more to discuss. Thank you for your time. :wink:

Yes, in C you are prevented from doing primitive mutating operations on a const value and if you pass the value to another function, that function must have a compatible signature—i.e. also const—so the restriction is recursive. You can still mutate the value by explicitly casting to a non-const type. How would that kind of constness work in Julia?

1 Like

Example of what I am thinking:

mutable struct Foo
    x
    y
    const z
end

foo = Foo([1,2,3])

foo.z = [4,5,6]    # error if const has meaning "binding cannot change".
foo.z .= [4,5,6]   # error if const has meaning "binding cannot be used to manipulate".

I understand what effect you’re asking for but how would it work? The array [1,2,3] is a mutable object which is a property of its type. Marking a field as const doesn’t change the type of the value that it refers to—mutability is a property of the value, not the binding.

1 Like

I am not sure what you are trying to achieve.

Assuming that you would like to prevent accidentally modifying the elements of z, you can eg define a wrapper type that has no setindex! method. See eg

https://github.com/bkamins/ReadOnlyArrays.jl

@Tamas_Papp: The last 8 posts was created in response to a small remark of mine that can be summarized as:

I am glad you take the language so serious as to defend the choice of the word, but I am afraid the discussion have gone to technical for me, as I cannot defend how const could possibly have the alternative meaning within the Julia type system. Anyway, thanks for Julia!

I am not sure I am defending anything here, just describing how Julia is.

Given that programming languages are different, I expect that some confusion is inevitable if someone insists on mapping concepts from one to another directly. Fortunately, we have a section in the manual about this:

https://docs.julialang.org/en/v1/manual/noteworthy-differences/

I you think that the C/C++ part could mention const, perhaps consider making a PR.

1 Like

That comment was addressed pretty simply:

The rest was in response to:

The following replies are explanations are about why this is not the case. Specifically, the C meaning of “you may not mutate this value via this binding” is not sensible in Julia because of two facts:

  • mutability is a property of the type of something, and
  • bindings—expressions in general—do not have types, values have types.

That is because Julia is a dynamic, not a static language. If you want to read more about this, I’ve explained it in depth in this stack overflow post:

If const was to affect the mutability of a bound object, it would have to implicitly limit or modify the type of the value being bound. That’s possible, but it’s a significant complexification and it’s unclear if it should be done, let alone how exactly it would work.

3 Likes