Why no := operator for initializing variables

I was not aware of that, maybe you are right. However I do not know if this is feasible if you just want to create a single variable since it blows up code size. Also it adds another layer of nesting which may be undesirable. However, maybe it would make sense to define

for i=1:10
   x:=i+1
   ...
end

as a shorthand for

for i=1:10
   let x=i+1
      ...
   end
end

So a := would just add a let… end - block to the innermost block of code
?
This would not be a breaking change and not require too much work to be done…

We already have scope keyword, like global and local. := is not needed. Scope keywords are more flexible than := because they can also be used on function declaration. Also, there’s no more benefit using :=, since there is no difference between x := 1 and local x = 1. Instead, you should propose a let or const variable declaration syntax to avoid accidental overwrite.
Another problem is that, := is a syntax construction instead of a semantic one. It’s generally impossible to know which assignment statement actually initializing the variable, that is, the first one to assign the variable. Consider this multi-threading program:

parallel for i in 1:10
    x := i
end

So you don’t really know which i initializes the x variable…

2 Likes

AFAIK this was an explicit design choice: having different syntax for introducing variables into a scope and assignment is something that programmers used to Python, R, Matlab, etc, may find inconvenient.

This came up in the (epic) scoping discussions: having separate syntax for the two would have solved a lot of scope problems trivially — cf (let ((x x-val)) ...) in Lisp.

5 Likes

I actually don’t think that would be such an unreasonable proposal. I am assuming there could then only ever be one := per variable name in each scope and multiple ones would be a syntax error? The main question here is just whether this pattern is used often enough to warrant its own syntax. I think this does warrant an issue on GitHub at least to see what others think.

7 Likes

I would love to have a := 3 as a shorthand for a::Int = 3 (and have it work in global scope)!

So I think := could serve two purposes that often go together:

  1. Make sure a variable is not re-declared by mistake.
  2. Give the variable a fixed type inferred by the value on the right-hand side. This means you can fix the type without knowing/writing the type signature (which can be complex).

Plus, it’s nice for readability as the code clearly says “this is were the variable is first defined”.

I also wonder: would this not solve a lot of performance issues if it becomes the “default” declaration syntax (i.e. recommended when the flexibility of = is not needed)? I mean, this would give

  • a nice syntax to declare globals with good performance.
  • a simple way to catch type instabilities: declare with := and you will get an error if you later assign a value that cannot be converted to the initial type.
  • if we can make the parser/compiler a bit smarter this could also help with closure performance (see #15276).
4 Likes

AFAIK not requiring explicit declarations for locals was an explicit design choice for Julia. Cf eg

5 Likes

Having recently learned Go (where := is used very much), I was very pleased with the middle ground that they have in that kind of code between explicit scope and syntactic parsimony.

My own background is heavily influenced by an older tradition of Scheme (and Julia is very much of a Schemish flavor), as a result I find let to be a natural expression that has very nice and clear scope. I would have expected that my reaction to := would be more negative. In fact, I found Go’s syntax to work really well.

On the other hand, I don’t think that the benefits of := would be fully recognized without some of the other aspects of Go, notably its very intentionally opinionated nature. For instance, the way that Go polices unused imports, unused variables and simple definition structure really work together for a more maintainable world. That level of strictness occasionally binds, but the solution is always clear enough that it is easy to fix. It would be hard, however, to convince somebody from a very dynamic world of that fact without actually having them experience it first hand.

1 Like

That makes my head hurt. :slight_smile:

Perl had a similar feature where you would have to write

my $var = 0;

upon the first use. It’s like int var = 0 in C but without the type. The advantages are code readability (a visible guarantee that this is the first use) and catching of typos. If you typo’d $car = 1 later on, it’d be a syntax error. AFAIK the my keyword had no real effect other than causing an error if you didn’t use it.

Reverse compatibility was handled by having the user opt-in with use strict. These sorts of checks were useful enough that all well written code would have use strict on the first line.

Though you could claim having to declare variables is just a pain, I considered it to be an important code quality feature and missed it when switching to python.

1 Like

This is certainly an interesting design direction that’s fun to think through. You would want something like this: := is required to initialize a variable, = can only be used to update an existing variable. That would take some getting used to for people coming from Python, R or Matlab, who are unaccustomed to thinking about variable declaration much. Their question would likely be “what is the difference?” — I’ll leave that to someone else to try to answer in this hypothetical alternate universe (perhaps a different version of me).

It could, as you suggested, also declare the type of the variable to be the type of whatever the value that is assigned is. One of the reasons you don’t want variables to default to being type-const is that it’s fairly common for people who are using a REPL or doing other “fast and loose” work to reuse the same variable name like x for something different — say a float in one case and a string in another. The := version = distinction could help with that: if x := occurs twice, the second one creates a new, separate x variable. Any previous capture of x still reference the old version of x (which can no longer be modified except by a closure), any new capture references the new x with a different type. That’s slightly confusing but I think that it would be fine and people would be able to enter multiple examples in the same REPL, which is the main thing they want. And we’d have type-stable globals, which is pretty nice. So this:

x := 0
x = 1 # fine, works
x = "hello" # error, can't convert `String` to `Int`
get() = x # capture this `x`
set(v) = x = v # modify this `x`

x := "hello" # works, new `x`
get′() = x # capture new `x`

get() # returns 1
get′() # returns "hello"

x = "goodbye"
get′() # returns "goodbye"
get() # still returns 1

set(2) # only way we can change the old `x` now
get() # returns 2

So that’s pretty nice. You can reuse common variable names but get, set and get′ get to assume consistent types, which seems good. Thinking through some examples. First closures that do and don’t modify an outer local:

function f()
    x := 1
    g() = x = 2 # modifies outer `x`
    h() = x := 3 # doesn't affect outer `x`
end

Now for loops. The first version has a new iteration variable in each iteration:

for i := 1:10
   # new `i` in each iteration
end
# `i` is undefined here

Next a version where the iteration variable is an outer local:

i := 0
for i = 1:10
    # same `i` in all iterations
end
# `i` is 10 here

Unclear what one would do with the in syntax that many people prefer. Do we just treat in the same way as :=? The assignment syntax for for loops seems strictly better in this design since it lets you choose between using an outer variable and having a new variable per loop iteration. Classic example of non-obvious interactions between different language features.

I can’t think of any other interesting example off the top of my head. Anyone think of any?

21 Likes

Could this type of syntax help for easier compiling and linking of standalone executables (slim executables without the julia runtime included) ?

However there I see the issue that casting should be somehow allowed, else

x:=1
x=1.5

would throw an error which may be undesired.
I would only fix a type if the type is explicitly specified.

I was thinking that keeping the normal assignment behavior should continue, and if someone uses := then you enforce the types, that way you can get the benefits of having a special assignment operator without losing the flexibility.

I think then we would need a new type for variables initialized with :=. Otherwise, how do we know if a particular assignment can change the type of the LHS? If the initialization happens in the same scope, you could conceivably tag the variable and restrict it there, but if that variable is passed into a function you’d need a way to distinguish variables initialized with := vs =

I think this is a bad idea: scoping is inherently a difficult and subtle concept which new (and old) users struggle with. But if := becomes possible it will appear all over the place, and require a careful explanation (that no one will actually understand) very early on the docs… so the more verbose version with let sounds much better to me.

5 Likes

This was a lot more necessary in Perl because if you do $x = 123 inside of a function body, it would assign to / create a global variable. So the only non-breaking option given that was to introduce a new syntax for assigning a new local variable, which was my $x = 123. The next evolution was to introduce our $x = 123 to explicitly ask to assign to the global. Once you have that, you can introduce use strict disallowing creating a new variable implicitly by assignment. But note that they didn’t get there very intentionally, it was a bit of a rambling path.

3 Likes

My recollection is that local $x to create a dynamically scoped local variable came very early. I know that my $x for lexically scoped variables came quite a bit later. I don’t recall if strict came before my. But yes, it was more useful in perl than it is for Julia (and probably python).
I agree that introducing lexically scoped variables a few years down the road was probably not planned. But, perl was originally designed with simplicity in mind; and not thinking about scope at all is simplest. local could be used by more advanced users. This is just guesses based on my recollection of perl culture.

No, the syntax for declaring variables has no impact on standalone executables.

Without the type-stable globals, this idea doesn’t seem very appealing to me. Then all it does is trade one way of declaring new variables (by x = when an outer local named x doesn’t exist) for another one (by x :=). I can guarantee that this would also confuse people and my guess is that it would be more confusing to more people than what we do now. What we do now has the property of mostly doing what people want without them having to think about the difference between declaration an assignment.

It’s just an interesting thought experiment — we can’t practically do this even in a 2.0 release. It would be a massively breaking change: we would need to disallow implicit declaration of locals by assignment, which breaks all Julia code everywhere. I also think that what we do now is good and less annoying for new users since they will be accustomed to just assigning to initialize and declare new locals if they are coming from Python, R, Matlab or Ruby (to mention just a few).

10 Likes

Reminds of an adventure game I played. It started with a joke. This person keep answering questions with another question. So this other person asked him, “why do you always answer a question with another question”, to which the person replied “Why not?”

1 Like

I should maybe add that the only change I think we should make to the local scope behavior as documented here is that this sentence:

  • in non-interactive contexts (files, eval), an ambiguity warning is printed and a new local is created;

should instead be changed to this:

  • in non-interactive contexts (files, eval), an ambiguity warning is printed and the existing global is assigned;

Of course, that makes the interactive and non-interactive behavior the same except for the ambiguity warning so I would probably change that section to this:

  1. Soft scope: If x is not already a local variable and all of the scope constructs containing the assignment are soft scopes (loops, try / catch blocks, or struct blocks), the behavior depends on whether the global variable x is defined:
    • If global x is undefined , a new local named x is created in the scope of the assignment;
    • If global x is defined , the global variable x is assigned.

The assignment in the last case (soft scope, existing global) is considered ambiguous: it is not clear from local syntactic context alone whether the assignment is meant to assign to the global or create a new local as it would if there were not already a global by that name. Therefore, when this code is evaluated in a file or other non-interactive context, an ambiguity warning is printed, prompting you to explicitly declare the assignment to be global or local (or rename the variable to avoid the name collision). In interactive contexts the global is assigned without any warning.

4 Likes