Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

||= and ?= assignment operators not working #2605

Closed
tigercat opened this Issue · 29 comments

6 participants

@tigercat

I just installed the latest version of node (0.8.14) and coffee-script (1.4.0) on Mac OS X 10.7.4, and everything seems to work but the ||= and ?= assignment operators. Typing a simple statement in the REPL like:

y ?= 2

causes:

Error: In repl, the variable "x" can't be assigned with ?= because it has not been defined.
at Assign.exports.Assign.Assign.compileConditional (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:1655:15)
at Assign.exports.Assign.Assign.compileNode (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:1517:23)
at Assign.exports.Base.Base.compile (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:46:21)
at Parens.exports.Parens.Parens.compileNode (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:2509:19)
at Parens.exports.Base.Base.compile (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:46:21)
at Value.exports.Value.Value.compileNode (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:701:24)
at Value.exports.Base.Base.compile (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:46:21)
at Assign.exports.Assign.Assign.compileNode (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:1539:24)
at Assign.exports.Base.Base.compile (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:46:21)
at Block.exports.Block.Block.compileNode (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:307:23)

or even:
z = 1
z ||= 3

causes:

Error: In repl, the variable "z" can't be assigned with ||= because it has not been defined.
at Assign.exports.Assign.Assign.compileConditional (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:1655:15)
at Assign.exports.Assign.Assign.compileNode (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:1517:23)
at Assign.exports.Base.Base.compile (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:46:21)
at Parens.exports.Parens.Parens.compileNode (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:2509:19)
at Parens.exports.Base.Base.compile (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:46:21)
at Value.exports.Value.Value.compileNode (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:701:24)
at Value.exports.Base.Base.compile (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:46:21)
at Assign.exports.Assign.Assign.compileNode (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:1539:24)
at Assign.exports.Base.Base.compile (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:46:21)
at Block.exports.Block.Block.compileNode (/usr/local/lib/node_modules/coffee-script/lib/coffee-script/nodes.js:307:23)

@michaelficarra
Collaborator
z = 1
z ||= 3

works for me, producing

var z;

z = 1;

z || (z = 3);

And the first case is working as expected. Closing.

edit: just noticed that you were in the REPL. See previous issues about REPL scope issues: #1829, #1627.

@tigercat

Are you using the same versions of node (0.8.14) and coffeescript (1.4.0)?

Even when I run coffee -p on a simple file containing "y ?= 3", I get a similar error.

@vendethiel
Collaborator

Yep, you can't ?= a variable that hasn't been declared yet. If you want a global, do so explicitely

@tigercat

You were right about ||= running in the REPL. Fails there, but runs inside a file - thanks.

You lost me with ?=. I thought ?= was meant to handle undefined/undeclared variables.

I didn't pull this example out of the blue. I am reading Mark Bates' "Programming in CoffeeScript" (Adison Wesley, 2012), and his assignment.coffee file example on page 36 blows up the same way. He has the exact statement: "y ?=3" with no previous definition of y. You can find this example on https://github.com/markbates/Programming-In-CoffeeScript/blob/master/chapter_03/assignment.coffee. Worked for him .... he was using CoffeeScript 1.2.0 at the time the book was written.

In the "Little Book on CoffeeScript" (http://arcturo.github.com/library/coffeescript/04_idioms.html), they say:

CoffeeScript's existential operator, which only gets activated if ... is undefined or null

Seems like the real issue is what ?= should do when a variable is undefined/undeclared. When Mr. Bate's book was written, ?= worked when a variable wasn't defined/declared. The "Little Book" seems to say the same thing (an "existential" operator should check if its operand exists). It seems to me that the ?= operator's only purpose is to initialize variables that have never been assigned to (which is the same thing as saying declared, since assignment is the way everyday variables get declared in CoffeeScript). If they have to be assigned to (declared) before ?= is run, does ?= have a purpose? Did someone change the definition of ?= between 1.2 and 1.4?

@michaelficarra
Collaborator

It's been a long time since 1.2.0. This behaviour has changed.

@epidemian

@michaelficarra Yeah, but it probably deserves a mention on the changelog :)

It seems to me that the ?= operator's only purpose is to initialize variables that have never been assigned to (which is the same thing as saying declared, since assignment is the way everyday variables get declared in CoffeeScript).

Well, kind of. For local variables i think ?= doesn't make much sense most of the time, because of the ? operator:

annoyingNotifications = userPreferences.notifications
annoyingNotifications ?= on # On by default.

# I'd prefer:
annoyingNotifications = userPreferences.notifications ? on # On by default.

But ?= also has its uses for local variables, just like or= does (i just can't think of one ATM =P).

The biggest advantage of ?= is for property assignment though:

# Copy of _.defaults
defaults = (obj, src) ->
  obj[k] ?= v for k, v of src

And using property assignment is how one should initialize global variables too:

# Avoid undefined console.log
window.console ?= log: ->

Instead of:

console ?= log: ->

That being said, i agree with @tigercat; i don't like the behaviour of the compiler when using ?= on undefined variables, as a ?= b should be equivalent to a = a ? b, but it's not:

# This compiles!
a = a ? b
@vendethiel
Collaborator

It is mentioned.
Conditional assignment of previously undefined variables a or= b is now considered a syntax error.

And a = a ? b does compile, with var a;.

@epidemian

Ho, snap. I though this was changed in 1.4.0 or 1.3.3 and didn't look further down. My bad :persevere:

And a = a ? b does compile, with var a;.

Yes, it does. But shouldn't it fail with the same error as a ?= b?

@michaelficarra
Collaborator

@epidemian: It shouldn't fail, it's just a needlessly verbose alias (a = b).

@vendethiel
Collaborator

Why not ? You won't create a global

@epidemian

@michaelficarra Ok, i probably phrased it in a confusing way. I meant to ask if shouldn't a ?= b be the equivalent to a = a ? b just as a += b is equivalent to a = a + b?

@tigercat

That makes perfect sense ... and that's what it used to be.

@caseywebdev

Ok, i probably phrased it in a confusing way. I meant to ask if shouldn't a ?= b be the equivalent to a = a ? b just as a += b is equivalent to a = a + b?

This is definitely my expected behavior for ?=.

@michaelficarra
Collaborator

a += b is not the same as a = a + b in the same way that ++a is not the same as a = a + 1. Please read the issues where these decisions were made (specifically, #1122) and you will understand.

@epidemian

Thanks for the link Michael, lots of interesting bits in that discussion. I didn't know about this one either:

foo = ->
  bar = 2
bar = 1
foo()
console.log bar # 1

I would have assumed that as bar is declared at the same scope than foo, bar = 2 would not declare a new bar variable inside foo.

Anyway, i guess all this issues reinforce my dislike for mutable variables =P

@tigercat

I'm not sure where to look ... Still, comparing the ++ postfix and prefix operator to a = a + 1 isn't logical. ++x was created for the purpose of incrementing something before looking at it (and x++ for incrementing something after looking at it). The ++ operator does something += was never meant to do, nor can it. They are not the same, and no one ever confuses the two.

In every language I've worked with (C, C++, Perl, Ruby, to name a few), a += b is just shorthand for writing a = a + b. I'm less experienced in JavaScript but the popular web site W3 schools (http://www.w3schools.com/js/js_operators.asp), says x+=y is "the same as" x = x + y.

I think the = operators are just "syntactic sugar" to get rid of clutter, not to define new operations. If CoffeeScript +=, ?=,||= etc. mean something different from their expanded versions, I'm confused. What do all these <+,-,? ....>= operators mean, and why is CoffeeScript defining well understood operators differently from it's brethren languages?

@michaelficarra
Collaborator

@tigercat: In JavaScript, a += b is not always equivalent to a = a + b either because of getters/setters and direct references to scope objects through either with or global variables. This is not unprecedented.

edit: I was mistaken. See my comment below.

@tigercat
@vendethiel
Collaborator

It's working the same way. a = a ? b is just a different way of writing a = null ? b. It'll never evaluate to try. And since there's no risk in global creation, it's not forbidden.
See #2316

@epidemian

Can you give me an JavaScript example where a = a + b and a += b give different results?

For curiosity's sake, i'd also like to see an example of this :)

The problem here arises, i think, from the fact that in CoffeeScript the first variable assignment is also the declaration of that variable and the scope of those variables is the the whole function they enclosed in. These two things combined is what result in these problems.

I can't think of a case where a += 1 differs from a = a + 1 in JavaScript, but there is a difference between a = a + 1 and var a = a + 1. In the first case, if the variable a does not exist, it will throw a "ReferenceError: a is not defined", but in the second case it doesn't explode (and a will be NaN afterwards) as var a declares the variable a not only from that = statement onwards but also backwards, until the start of the current function scope. That is why this works:

function foo() {
  console.log(a); # undefined
  var a = 1;
}

While this doesn't (assuming a is not global or form an outer scope):

function foo() {
  console.log(a); # ReferenceError: a is not defined 
  a = 1;
}

That's a very known behaviour of JavaScript. But the thing is that the issue discussed here does not happen in JavaScript (at least not that i know), even though Coffee inherits its scoping rules, because JS distinguishes variable declarations from assignments. In fact, var a += b is illegal in JS.

In Python, for example, you can't do print(a); a = 1 nor a = a + 1 if a wasn't declared before. It will throw a NameError because the declaration of a does not declare it in the lines before it nor at the right side of the assignment/declaration = (because the right side "happens" before the assignment/declaration). Python's syntax to declare variables is the same as Coffee's, but it doesn't suffer from the issue discussed here because the scoping rules don't suck as in JavaScript.

I'm not sure if my reasoning was correct, but if it was, i think it's kind of unfortunate that this combination of circumstances made a += b and a = a + b potentially mean different things in CoffeeScript.

(BTW, maybe Python was not the best example for a += b semantics, as += can be overridden with the __iadd__ method and make it mean something totally different from a = a + b... but well, i could argue that in that case it would be the programmer's decision to mess with in-place operators, not something that the compiler messed up for them hehe)

@michaelficarra
Collaborator

Never mind my comment, it seems I was mistaken about javascript getter/setter behaviour.

> o = {}
{}
> Object.defineProperty(o, 'a', {get: function(){ console.log('getter'); }, set: function(){ console.log('setter'); }})
{}
> o.a = o.a + 1
getter
setter
NaN
> o.a += 1
getter
setter
NaN
> 

I thought only the setter would be called in the compound assignment example, but I was mistaken.

@tigercat

Ok, so isn't having a ?= b mean something different from a = a ? b a problem with the language?

Shouldn't it be fixed or documented?

@vendethiel
Collaborator

Let's say a = a ? b is more permissive, it's just, as we repeated many times, the same thing as a = b

@tigercat

Then put it in the Doc. There are 4 good reasons.

1) a ?= 1 declared and initialized "a" in version 1.2 when "a" wasn't declared. In 1.4 it stopped working.

2) This isn't some corner case. ?= is called the "existential operator". There's a lot of ambiguity here. It's reasonable to think that a ?= 1 will test for the existence of "a".

3) ?='s expanded counterpart, a = a ? 1, works.

4) When the function of ?= changed, it wasn't documented (it's hard to find it in a bug report).

@vendethiel
Collaborator

I said it above in this thread

It is mentioned.
Conditional assignment of previously undefined variables a or= b is now considered a syntax error.

@tigercat

The "or=" operator is not the "?=" operator we are talking about.

@vendethiel
Collaborator

Same semantics

@epidemian

I think there are two different things that are being discussed here. @tigercat touched both of them in their enumerated comment above (points (2) and (3)). I'd like to differentiate them.

One thing is the desired behaviour of the ?= operator and whether it is intuitive or not. I think that given the circumstances of having implicit declarations on first assignment and having JavaScript's scoping rules, the decision to make a ?= 1 fail at compile time if a was not defined in that scope is a good decision, as it is semantically an error. The correct way of initializing a global variable is:

window.a ?= 1 # or global.a ?= 1 in Node

The other thing being discussed here is whether a ?= b and a = a ? b should be equivalent. And this is where i am in disagreement with the current implementation. Making those two statements non-equivalent is very unintuitive in my opinion. But i understand where the need for inconsistency comes from. That's why i'd like to propose a change for all assignments, including all forms of <op>=, to make this inconsistency go away.

Proposal: Make variable declarations fail if the variable that is being declared appears in the right-hand side of the declaration.

In the case of e1 <op>= e2, e1 is considered to be part of implicit e1 <op> e2 right-hand side expression.

Examples:

a ?= 1 # Error: the variable "a" can't be assigned with ?= because it has not been defined.
a = a ? 2 # Error: variable "a" can't be used in it's own declaration.
a = a + 3 # Error: variable "a" can't be used in it's own declaration.
a = foobar a, 4 # Error: variable "a" can't be used in it's own declaration.
a += 5 # Error: the variable "a" can't be assigned with += because it has not been defined.
a = something()
a ?= 6 # OK
a += 7 # OK

Aaaaand, just for the sake of consistency over all things:

a++ # Error: the variable "a" can't be assigned with ++ because it has not been defined.
# :)

This change would make all a <op>= b fail when the variable a was not defined before, not just some of them. And it would also make a = a <op> b be equivalent to a <op>= b: if a was previously declared in that scope no problem, no new variable is declared; if a wasn't previously declared in that scope, then it will fail just like it's <op>= equivalent would.

The change would also make the compiler complain about declarations like a = foo a, which is a good thing in my opinion :)

What do you think?

@mu-is-too-short

There's a similar discussion occurring over on StackOverflow right now. I think it would make sense to widen the scope of the discussion to get a better perspective on things and @epidemian asked me to throw my two cents into the ring.

The problem as I see it is that there are two groups of <op>= assignment operators in CoffeeScript: those that are in JavaScript and those that aren't. AFAIK, only ||=, &&=, and ?= fall into the CoffeeScript-specific category. CS has specific checks for a ||= b when a isn't declared in Assign#compileNode and Assign#compileConditional but there isn't a similar "can't be assigned because it has not been defined" check for a += b, a |= b, and the other JavaScript assignment operators.

So += and ||= aren't treated consistently by CoffeeScript and I don't see any reason for that, presumably invalid <op>=s just haven't come up enough for anyone to do anything about it.

This is only my second venture into the CoffeeScript source but it looks like this could be fixed with a couple lines of code:

  1. Generalize the "is it declared" test that's in Assign#compileConditional.
  2. Use that test to check += and friends in Assign#compileNode.

I'd whip up a patch myself but I'm not familiar enough with the code to know if that's all that's needed.

I think epidemian's proposal makes a lot of sense. The main point of CoffeeScript is to make programming easier and pointing out mistakes as soon as possible certainly makes things easier.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.