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

It’s a balancing act — the downside of having a moderator splitting off a thread is that the new thread can be difficult to read because it abruptly starts in the middle of a conversation. By starting a new thread intentionally, on the other hand, the initial poster can (hopefully) begin it with a clear summary of the new topic (even if it links to other topics).

See also this comment: Time limits for unfocused discourse threads? - #11 by Nathan_Boyer

7 Likes
3 Likes

I was rather enjoying the historical programming language musings. Julia does definitely inherit more from the Algol => Wirth lineage, mostly via Matlab, but not entirely. Turbo Pascal was also my first language (possibly also Jeff’s?) and I did a lot of Ruby in the 00’s.

I’m sympathetic to using := for initial assignment / declaration and using = for reassignment, but I think that ship has sailed. Doing our own version of the great vowel shift and renaming = to := and == to = and === to == might be better syntax if we could rewind history, but given the way languages have developed, that would be a very confusing choice.

15 Likes

While I concur that it is too late to change the meaning of = in Julia, it does seem that the walrus operator, := has been reserved and thus could be defined in the future.

julia> x := 5
ERROR: syntax: unsupported assignment operator ":="

There are a number of special assignment operations that I could think of.

  1. Strict variable declaration. This would analogous to Go’s short variable declaration syntax. In Julia’s case, this would ensure that we are assigning a new variable and not reassigning an old variable.
  2. This could be also imply local variable assignment.

Consider the following idiosyncratic example.

julia> let x = 5
           let
               x = 7
           end
           println(x)
       end
7

julia> let x = 5
           let
               local x = 7
           end
           println(x)
       end
5

We could have the walrus operator work as in the second example.

julia> let x = 5
           let
               x := 7
           end
           println(x)
       end
5
  1. We could use this for binding a type on assignment.

Similar to the following.

julia> y::Float64 = 5.0
5.0

julia> y = 3 + 2im
ERROR: InexactError: Float64(3 + 2im)

We could do as follows.

julia> y := 5.0
5.0

julia> y = 3 + 2im
ERROR: InexactError: Float64(3 + 2im)
5 Likes

Imho = is ideal syntax for defining a variable, and defining rather than reassigning variables should be (slightly) encouraged. My preference is to make := reassignment and keep = for defining. That’s the policy Guy Steele described for Fortress in his JuliaCon keynote.

In 1.x we can use a linter to make sure reassignment is always done with :=. Then in 2.0 we can make it the law.

1 Like

I think that, for better or worse, a lot of the potential user base for Julia is used to the conventions of C, Fortran, and descendants (= for assignment, == for equality), so while such a notation would be consistent, it would have instantly alienated a large number of people.

This is not necessarily rational — personally, I think that I can get used to any kind of surface syntax (after all, I used Lisp a lot, so while I took the opportunity above to mention my Pascal-related traumas :wink:, I could live with :=), and a large number of coherent and useful options are available for syntax. But it is one of those quirks of the human brain.

7 Likes

This is the opposite meaning of Go, which strikes me as a rather bad choice. If we’re going to make the same distinction with the opposite meaning, that just seems willfully obtuse. Yes, Go is a different language, but languages are partially social constructs and you can’t just make up whatever notation you want (APL not withstanding); trying to avoid confusion based on what other languages have done matters. Using something like x <- y for rebinding would be better imo, or having an option to require := for declaring assignment.

8 Likes

I agree that reassignment needs to be more difficult, but I would not be willing to have :=. An alternative idea I have been sometimes dabbling with is that variables need explicitly be marked with local when reassignment is allowed:

function example()   
    local s = 0
    for i in 1:10
        s += i
    end
    return s
end

which would provide a warning or raise an error if s would not be marked as local.

It’s clear that “= means equality in mathematics, let’s change 6/10 lines of code written in Julia and make everyone’s life harder thereby” is a non-starter, yes.

But I could see “:= means declaration” being a win for 2.0. We’ve seen a few variations on that already, so I’ll sketch out mine.

I like the scoping rules in Lua, but I don’t like that local is five frikkin letters long and pushes declarations off to the right. It’s a very heavy word for what it does! Julia has the same keyword with the ~same meaning, but it doesn’t matter because the scope rules are different, it won’t silently dump your variable in the global environment table if you don’t tell it not to.

With := a 2.0 could move toward Lua’s scope rules with some ancillary benefits. Lua has a very simple rule that a block is a scope, I prefer the Julian distinction between scope-introducing blocks and scopeless blocks (let/for vs. if/begin).

But if in 2.0 a variable declaration required :=, Julia wouldn’t need local or global. It would still need outer however. This would create less of a hard line between top scope and inner scopes, which I consider a good 2.0 goal, along with eliminating syntax which is only legal in inner scopes, or at least reducing it to a bare minimum (macro declarations might have to stay global, for example).

It seems a bit redundant to require it in let expressions, which introduce a declaration without exception, so let’s say I’m agnostic as to whether it should be required there (it would need to be permitted at least).

Julia does a rather good job with handling implicit declaration relative to other languages (Python) which use it, but there’s an inherent conceptual ambiguity which requires closer attention to the scope rules to write correct code. Using a dedicated syntax for declaration minimizes that ambiguity, if not eliminates it.

It’s also nice that it would make repeated definitions in a single scope an error. There are two contexts where that would make a difference: global scope, which can be scattered across thousands of lines in many files, and complex if statements, where one might think that every declaration is mutually exclusive, but the compiler might disagree.

It could be introduced as an optional syntax in 1.0, since it’s a syntax error now. Then = for declaration could be deprecated, with a rewrite tool which translates code to the new standard: the compiler knows which is which, after all, so this isn’t a guessing game. Then 2.0 could complete the transition, and change the scope rules, so () -> (b = 5; return b) either refers to an outer scope variable (up to global) or is an error.

That would also buy some time to see if users hate it. I don’t think they would, but you never know.

I’m not even convinced it would be worth the hassle, but I do know that I’d be pleased with the change.

It’s worth noting that Go has a much larger user base than Julia (for now) and uses := in much the way we’re discussing. This gets dissed on Hacker News sometimes but clearly poses no barrier to adoption.

1 Like

The reversal from Go is a definite negative. That said, only 3% of Julia users use Go (2023 Julia survey) so I suspect it’s relatively unlikely to cause excessive confusion in practice.

In other languages there are different uses too. Eg 14% of Julians use SQL which uses = for equality testing. 6% use Mathematica in which lhs = rhs is “immediate assignment, with rhs evaluated at the time of assignment” and lhs := rhs is “delayed assignment, with rhs reevaluated every time it is used”.

1 Like

The set of Julia users is a tiny fraction of the set of potential Julia users, and (I hope at least) also a small fraction of eventual Julia users. Paying attention to how larger languages use syntax should be a consideration.

5 Likes

there could potentially be an interesting way to use : as an operator; if so then := probably should not be used except to mean op=

This kind of dichotomy would be a huge pain for interactive use — if you have a long-running interactive session, every time you assigned a variable you would have to remember whether you had used that variable name before.

Yes, I think PSA: Julia is not at that stage of development anymore needs to be re-iterated here. It’s fun to talk about the etymology of syntaxes and what-might-have-beens, but no one is seriously contemplating major changes to Julia’s spelling/aesthetics at this point. If people think they are arguing over the future direction of Julia in this thread, they are bound to be disappointed.

13 Likes

The REPL already has a few differences from scripts, and any hypothetical := for declaration could simply be relaxed in that context.

Just idle chitchat as far as I’m concerned. I probably see the suggestion as less major, and less aesthetically-grounded, than you do, but no matter. Work on a 2.0 hasn’t begun, and it’s not clear that it ever will, so it seems like a safe place to store alternate-universe Julia fanfic. IMHO the only reason to introduce an optional declaration syntax would be as a transition to a 2.0 where it was load-bearing, the language doesn’t need more ways to spell the same thing.

I think my proposal would be a concrete improvement, not window-dressing, or I wouldn’t have bothered making it. But (as I believe I indicated all the way at the top) I’d expect to get voted down because people don’t like how it looks. Which is a valid reason, I don’t like working with languages where I have aesthetic objections to the syntax either. Of which I have none at all for Julia, which is unusual though not quite unique.

2 Likes

For me, accidental reassignment is definitely frequent and consequential enough to merit significant syntax changes. To wit:

= defines a local variable.

julia> x = 1
1

julia> let
           x = 2
       end
2

julia> x
1

So far so good.

julia> let x = 1
           let x = 2
           end
       x
       end
1

Cool, all good.

So then when it bites, it’s actually much more surprising:

julia> let x = 1
           function f(y)
               x = abs(y)
               y - x
           end
           sort(1:5; by=f)
           x
       end
4

Whenever I define a variable in a block, I try to read every line lexically above it to make sure the variable doesn’t exist already, lest I corrupt my runtime state by accident. Variable definition is therefore a quadradic-time operation, which is quite annoying.

Worse, I still sometimes fail to notice x already exists and get confused why variables are different than I left them. I hope my testing is always good enough to catch these mistakes, but I don’t want to rely on that: I want a guarantee that when I define x = 2 all the information I need is right there on the line I’m currently looking at.

6 Likes

is that really a “bite” ? isn’t this a direct consequence of like scoping rule #1 ? I don’t mean to sound condescending, only that I feel you are presenting this as a ‘WAT’ when I see it as just very fundamental to how Julia scope rules work

I do get the sense that a similar PSA is needed regarding what “Julia 2” might be. If we have learned anything ftom the experience of other languages, most but perhaps not necessarily all “Julia 1” code should continue to work under “Julia 2”.

Changing the meaning of = or requiring the use of := does not seem compatible with the idea of “Julia 1” code continuing to work under “Julia 2”.

1 Like

This seems entirely premature if there’s no Julia 2 on the horizon. If there are official statements on the subject, that should be downstream from an solid intention to write it.

I wouldn’t conflate these two things. Switching = to := and == to = is purely cosmetic and worth rejecting out of hand.

There are a few strategies to achieve a laudable goal like most Julia 1 code continuing to work under Julia 2. In the specific case of := for declaration, that’s a translation a program can make with perfect fidelity, either at load time, by changing the file, or both.

It should in fact be possible, with the right process, to assure that 100% of Julia 1 code can be executed in the Julia 2 runtime. But a detailed exploration of that thesis would be off topic.

There are plenty of good arguments against := for declaration, but ruling it out on procedural grounds is suboptimal.

I’ve written a bug the other way, where I had a global variable I was trying to close over and neglected the fact that global scope is “special”. So := works both ways there.

Python’s scoping rules are clearly defined, but complex, in a way that leads to authors, especially beginners, making frequent mistakes. Julia’s are better, but this doesn’t mean they can admit of no improvement.

1 Like

The mistake in my demo above does not occur in Python. It would require nonlocal x in order to reassign nonlocal variables.

Nor does it occur in Scheme which uses set! for assignment, nor Rust which uses let for definition, nor Go which distinguishes = from :=, nor Zig, etc.

In Python it’s the other version of the problem, where you can see a variable from an enclosing scope on the right-hand side of a statement, but using it as an lvalue will create a new variable instead of assigning to the outer one. This issue was particularly acute before nonlocal came along in Python 3, because you just couldn’t do it. Julia does this for globals, but since module-level names can be declared anywhere in the module, I think it’s the better choice there, instead of switching the meaning of a variable in a function you weren’t thinking about when you add a global, possibly in a different file.

We agree that a lightweight syntax to distinguish declaration and assignment is preferable to learning the ins and outs of implicit scope rules and applying modifiers to get exceptions to the rule. None of these issues would come up and neither global nor local would be necessary.

2 Likes