Hacker News new | ask | show | jobs
by mhansen 5290 days ago
I'll copy below jashkenas' longer answer from the old github issue about this, https://github.com/jashkenas/coffee-script/issues/712#issuec...

"""

Sorry, folks, but I'm afraid I disagree completely with this line of reasoning -- let me explain why:

Making assignment and declaration two different "things" is a huge mistake. It leads to the unexpected global problem in JavaScript, makes your code more verbose, is a huge source of confusion for beginners who don't understand well what the difference is, and is completely unnecessary in a language. As an existence proof, Ruby gets along just fine without it.

However, if you're not used to having a language without declarations, it seems scary, for the reasons outlined above: "what if someone uses my variable at the top of the file?". In reality, it's not a problem. Only the local variables in the current file can possibly be in scope, and well-factored code has very few variables in the top-level scope -- and they're all things like namespaces and class names, nothing that risks a clash.

And if they do clash, shadowing the variable is the wrong answer. It completely prevents you from making use of the original value for the remainder of the current scope. Shadowing doesn't fit well in languages with closures-by-default ... if you've closed over that variable, then you should always be able to refer to it.

The real solution to this is to keep your top-level scopes clean, and be aware of what's in your lexical scope. If you're creating a variable that's actually a different thing, you should give it a different name.

Closing as a wontfix, but this conversation is good to have on the record.

"""

5 comments

> As an existence proof, Ruby gets along just fine without it.

Missing that Ruby stops scoping variables at a method and uses separate lexical scoping rules for constants thereby avoiding this issue mostly.

Also, Ruby can shadow method names with local variable names just fine:

    class Foo
      def bar
        "bar"
      end

      def baz
        bar = "foo"
        puts bar
      end

      def bang
        puts bar
      end
    end

    foo = Foo.new
    foo.baz  # => "foo"
    foo.bang # => "bar"
Well, this is a case where the equivalent CS behaves exactly like Ruby.

  class Foo
    bar: -> 'bar'
    baz: ->
      bar = "foo"
      console.log bar
    bang: ->
      console.log @bar()

  foo = new Foo()
  foo.baz()  # => "foo"
  foo.bang() # => "bar"
Not quite; @bar is "this.bar", and is an unambiguous reference, not just "bar". In Ruby, because self can be implicit, it's literally just "bar" in both cases, and Ruby gives the local variable precedence over the method name. You can still invoke the method via self.bar, but accessing the method as just "bar" is shadowed by the variable. Additionally, even though the method may be invoked by referencing bar, you can't redefine the method by assigning a new method to bar. Neither Javascript nor Coffeescript has that behavior.

Here's a better example:

Ruby:

    def bar
      "I called bar!"
    end

    def foo
      puts bar
      bar = "I manually assigned bar"
      return bar
    end

    puts foo()
    puts bar()

    # =>

    I called bar!
    I manually assigned bar
    I called bar!
Coffeescript:

    bar = ->
      "I called bar!"

    foo = ->
      console.log bar()
      bar = "I manually assigned bar!"
      return bar

    console.log foo()
    console.log bar()

    # =>

    I called bar!
    I manually assigned bar!
    TypeError: string is not a function
When I said the two programs have the same behavior, I was simply referring to the end result--what they output. I understand how both languages work here. Ruby solves a problem that simply doesn't exist in CS. In JS/CS "bar" never implicitly refers to "this.bar", so there's no ambiguity in the first place.
Your example does demonstrate how CS works. When you assign bar a new value inside its original scope, the value of bar does indeed change.

  bar = ->
    "I called bar!"

  foo = ->
    console.log bar()
    bar = "I manually assigned bar!"
    return bar

  console.log foo()
  console.log bar()
  
Here is a slightly less contrived example:

  log = (data) ->
    console.log data
    
  enhance_logging = ->
    i = 0
    log = (data) ->
      i += 1
      console.log i, data

  log "no line numbers"
  enhance_logging()
  log "line one"
  log "line two"
  
Here is the output:

  > coffee foo.coffee 
  no line numbers
  1 'line one'
  2 'line two'
and well-factored code has very few variables in the top-level scope -- and they're all things like namespaces and class names

Unless you happen to embrace JavaScript's functional side and write lots of top level helper functions (in a closure of course after which you export)

shadowing the variable is the wrong answer. It completely prevents you from making use of the original value for the remainder of the current scope.

This smells of static enforcement - strange for a language expounding JavaScript's dynamic nature and an odd departure from "it's just JavaScript".

Shadowing doesn't fit well in languages with closures-by-default

Not sure what evidence this is based on given the heap of great languages with closures-by-default that give programmers more control over scope without introducing goofy constructs like special assignment operators or global/nonlocal keywords.

CoffeeScript breaks the one real form of encapsulation (which includes the power of naming) that JavaScript has - function locals.

I may just ask you in a few minutes when you get in ... but what exactly is the scoping scheme (and generated JavaScript) that you're proposing here, without "var", "nonlocal", ":=", and with shadowing?

In addition, to repeat myself elsewhere in this thread, the goals here are conceptual simplification and readability, not giving the programmer more control over scope. The final result is that hopefully:

    someVariable
      ... more code here ...
        someVariable
          ... more code here ...
            someVariable
          ... more code here ...
        someVariable
... in the above code, you can know that "someVariable" always refers to the same thing. With "var", the above code could allow "someVariable" to refer to three different things, each for slightly different sections of the above chunk of code.

If you really want three different values, use three different names. In all cases, it will read better than shadowing would have.

In all cases, it will read better than shadowing would have.

You are going against the consensus established by ALGOL and Scheme (and used by most languages in the functional camp since then) here. That's your prerogative of course, but personally I'd be wary of design choices that go against established wisdom.

With "var", the above code could allow "someVariable" to refer to three different things, each for slightly different sections of the above chunk of code.

Yes, but that's not an issue, as it is easy to check (by looking at the code section in isolation) which of the three bindings an identifier refers to - which is, for me, the definition of lexical, static scope.

[Shadowing] prevents you from making use of the original value for the remainder of the current scope.

That's totally false.

In languages like Scheme, O'Caml, etc you are never prevented from using the original value.

The point is that lexical, aka static scope is all about lexical pieces of code that you fully control, and all their properties are statically apparent, by looking at a single piece of code.

Scheme:

  (DEFINE FOO 1)
  (LET ((FOO 2)) ... FOO IS SHADOWED HERE ...)
Inside the LET, FOO is shadowed (FOO is "your FOO") and that's lexically, statically apparent by looking at the piece of code.

If you don't want it shadowed, and use the global FOO, you just use another variable name. You cannot be prevented from using the original global FOO, because you choose the local variable names you use in a piece of code.

You cannot be prevented from using the original global FOO, because you choose the local variable names you use in a piece of code.

Unless you're using a system in which non-hygienic macros are present and they expand into it...

If you really favor protection from errors, then the correct choice is to require 'var' for new declarations but forbid shadowing (it's an error). Shadowing a la Scheme is a source of errors, too -- you can intend the outer variable, failing to notice it's been shadowed. Of course, if you really favor early detection of errors, you'll also have static name binding.
ruby is terribly ambiguous..