On using `=` vs `:=` for assignment

Here’s my favorite manifestation of Python’s weird scoping rules:

 def f():
     x = 10
 
     def g():
         print(x)
 
     def h():
         print(x)
         x = 20
 
     g() # prints 10
     h() # UnboundLocalError

Editing to add: this error doesn’t occur in Julia because of its different (better) scoping, but some version of the ambiguity will necessarily exist as long as declaration and assignment have the same syntax.

I’d argue that this is the more fundamental point than the aesthetics of = vs :=. For example, C does make this distinction: variable declaration includes a type, whereas reassignment does not. Consequently I consider C to be in the := camp, even though that is not the actual syntax.

That’s not to say that I think Julia should adopt :=. In my experience, these scoping footguns are rarely encountered in practice

4 Likes

I don’t know if it is reasonable to talk about Julia 2 at this point, but based on past experience with much more minor changes, I would bet that if there are significant alterations to the language, there will be tooling to convert existing code with minimum effort whenever possible.

So it is not something I would worry about. Just keep coding. :sunglasses:

2 Likes

Even Haskell uses == for equality and = for binding, i.e., introducing names as in let x = 4. Obviously, assignment does not exist in Haskell – except within some monads.
On the other hand, I think that Lisp was right in explicitly distinguishing between binding via let and assignment via setf and maybe also of not eating up = syntax-wise for any of these …

1 Like

That post already talked about this:

But despite what it sounds like, this is not the time to make all the random changes that anyone might want to. Frankly, that time has passed for Julia and will never come again. There will probably be a few renamings of unfortunately named things. But that’s not what the 2.0 release is really about.

What the 2.0 release is about is thinking about big challenges and figuring out how to address them in novel ways without having to worry about how it fits into the existing language design. […] But often one finds that in order to implement a really radical solution to a hard problem, at least some corner cases of the language have to change in breaking ways. That is why 2.0 will be breaking, not because we want everyone to have to change their code because something should be spelled slightly differently.

5 Likes

This is the part of this discussion which I most firmly agree with: declaration and assignment are different operations, and using a single syntax for both conflates distinct operations. When you do two things with one syntax, it’s always possible to do one while thinking you’re doing the other.

Has this lead me to write a lot of bugs in Julia? Actually no. The scope rules are somewhat complex, but in a way which (mostly) comports with my intuitions, and I reuse variable names sparingly. One time I left out a global declaration, but this was a shallow bug, easily fixed.

Where I do feel the burden is in reading code which I didn’t write. Concentrating on the algorithm takes all of my attention, and when I see an l = r I have to switch to ‘scope mode’ to figure out what’s going on.

= for declaration and <- for assignment creates some known (if surmountable) problems for the compiler, but I actually prefer that syntax, and it’s a safe bet there are more declarations than assignments in the wild, so it would change less code.

What I know for sure is that a syntactic distinction between the two would improve an already excellent language in concrete ways, it’s not window dressing or the color of a bikeshed at all. This is worth keeping in the back pocket, in case an opportunity to make the change arises.

This is a good policy, but I really want to stress that the topic of discussion is not about syntax, it’s entirely about semantics, and their impact on program correctness and comprehensibility.

I’ll compare it to division in Python. In Python 2, / was a homonym: for integers it was truncating, for floats, ordinary division. Python 3 decided to introduce // as the exclusive spelling of truncating divison, such that it applied to floats as well, and / on integers performs ordinary division, converting to floats in the process.

There are many rational arguments against separating assignment and declaration in hypothetical Julia 2, but it shouldn’t be compared to, say, wanting to spell function as lambda or fn. It isn’t a trivial change, it’s a real distinction which is worth fully exploring when the time comes. I’d expect the outcome to be a decision against making the change, in fact, but the idea deserves to be in-bounds.

1 Like

Oh, that is another assignment syntax I particularly hate. I recall that when I was teaching R, 90% of student kvetching came from the <- syntax. Even R allows = as an alternative (maybe only at the toplevel, I cannot recall exactly). (And of course they had to go overboard, so there is x -> y, equivalent to y <- x).

I think that there are two local optima:

  1. Julia’s DWIM =, with reasonable semantics about scoping. Yes, sometimes = creates a new variable and sometimes it assigns, and further complications arise with x[i] = and x.y = , but the language deals with all of them in a more or less sane way so we can get work done.

  2. Common Lisps let & friends with setf, where the latter can be extended by the user. That’s just so neat.

5 Likes

I’ve barely ever used R, but it looks like it doesn’t distinguish declaration and assignment, it just offers three ways to conflate them, which is obviously bad.

means declaration and means assignment” is not at all difficult to teach or understand, to pick a couple random symbols to stand in for the distinction. Plenty of languages have this distinction without a reputation for being hard to learn, and the syntax is mostly disliked by people who don’t actually use it.

But variations on “making everyone learn the new syntax isn’t worth the benefits it could offer” is probably the best argument against this line of inquiry, and I expect it would carry the day, in the event. Purely aesthetic objections are somewhat weaker but deserve consideration nonetheless.

Don’t even get me started on sacrificing familiarity for mathematical purity. Julia uses * instead of + for string concatenation, and the documentation justifies it by going on a rant about abstract algebra and monoids.

Still, if there is a language to be a stickler for mathematical purity, I guess Julia would have a reasonable claim to striving for it – It does have support and good tooling for unicode symbols commonly used in mathematics. I just wish they would have committed by enforcing := and * or going with the flow by allowing = and +. It’s unfortunate that it’s sort of halfway committed to mathematical purity.

depends on what you’re familiar with… + for concentation is by no means a universal convention. it just happens to be the convention of the most popular language

I made this list some time ago

3 Likes

This is off topic (and extensively discussed elsewhere) but I 100% endorse this decision. one(String), try it! It’s a bit of surface syntax which is a trivial part of the cost of learning the language, and it’s correct in a way that people with mathematical backgrounds appreciate. If you’re going to use a mathematical operator (rather than distinct syntax like Lua’s ..) for concatenation, why use the popular one when you can use the correct one?

3 Likes

In my dreams, = would be equality comparison, and assignment would require a keyword let or set for declaration or reassignment, so that code would read more like math proofs. This would improve legibility imo. For example:

let a, b=2       # declaration & assignment within current scope
set a=2, b=3     # (re)assignments of whatever is visible to this scope
(a=2, b=2)       # tuple of Bool's (true, false)
(; a=2, b=2)     # named tuple of Int's
for i=1:10       # loop
    set a+=i     # reassign
end
let (x,y) = (1,2)        # itr destruct
set (;x,y) = (;y=2,x=1)  # prop destruct
let typed c = (;a=1,b=2) # like c::NamedTuple{(:a,:b),Tuple{Int,Int}} = (;a=1,b=2)
set d[k] = v             # setindex!(d, v, k)

Semantics similar to Julia 1.x’s let block could be satisfied by with:

with a=2, b=3
    #= ... =#
end

with could serve double-duty:

with my_object
    my_object.a = a  # true
    set a=1          # setproperty!(my_object, :a, 1)
end

I’d also have do...end to replace begin...end, and I’d replace && and || with and and or.

6 Likes

yikes, I don’t like any of this :sweat_smile:

But it’s a good demonstration of the difference between dressing Julia’s semantics in new syntax, and proposing new syntax to address a different semantics, so thanks for that.

Try writing code assuming that was the rule, I think you’ll like it :wink:

With respect, I am quite sure I wouldn’t. I dislike it on a “I would not use a language which looks like this” level.

This?

(a=2, b=2)       # tuple of Bool's (true, false)
(; a=2, b=2)     # named tuple of Int's

No! No way! Kill it with fire!

1 Like

You meant this link.

In the theory of finite state machines, the “multiplication” operator is used for concatenation of regular expressions. Since regular expressions describe strings, it’s pretty natural to extend this to strings. I think the docs would be better off simply saying this.

Maybe I should’ve left that example out—I was playing around to see how it’d interact with named tuple syntax :sweat_smile:

Again, try writing code and see how it looks and feels. Other than named tuples, it feels pretty good.

1 Like

See now I’m actually on board with * for concatenation. I guess my initial aversion to it was due to getting an error and then looking it up only to encounter a grandiose explanation that isn’t relevant to the overwhelming majority of programmers. Regex is its own can of worms, but it’s definitely something most are familiar with!

Thanks for helping me fix my link.

2 Likes

Nope, I’m not going to like it, and I can explain why while keeping this thread on topic, so I will.

You’ve made = a homonym, and that’s the same conflation I’ve been on about in the second half of this thread. When you have a homonym, it’s too easy to think you’re using it one way when the compiler thinks you’re using it the other way.

And you’re applying modifiers to every use of = as (still conflated! it’s a triple homonym! nonono) either declaration or assignment. set this, let that. This means that set x = y, the set/let is 'modifying =, not x, this is hard to scan. It’s noisy, you’d need this on every line with declaration or assignment, and set and let differ by one letter, so it would just sort of smear together on the screen. Leave it out and you get a boolean, which is the sort of fun that ruins my whole day.

In := vs =, the : is what modifies = to be declaration, and they’re touching; that’s easy to read. It also eliminates the global and local modifiers, which are infrequent enough that their syntactic weight isn’t so burdensome, but I’d still call it a win. They also more clearly modify the variable they’re next to, rather than the operator.

If you completely dumped the connection to Julia and went for a syntax like this from first principles, you’d end up with something like OCaml, which a lot of people like. It’s consistent, this isn’t.

I don’t think I can engage in good faith if you refuse to try it, but I’ll briefly inform you that = is already a homonym in Julia.

julia> for i=0:9
           print(i)
       end
0123456789

julia> ((i for i=0:9)...,)
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)

= has an identical meaning in both of these constructs.