jashkenas / coffeescript

Unfancy JavaScript
https://coffeescript.org/
MIT License
16.49k stars 1.99k forks source link

Exclamation mark for instant object assignment #1738

Closed jussiry closed 12 years ago

jussiry commented 12 years ago

In Ruby exclamation marks in function names are used to indicate that the function changes the object in question (for example: "foo".capitalize!). CoffeeScript could do something even cooler, and make this functionality dynamic, so that: foo.bar! would compile to foo = foo.bar();

This would allow for design pattern where prototype functions by default would never need to alter the object, but this could be chosen when the function is called simply by adding (or by not adding) exclamation mark.

Here's another, bit more complex example:

some_string = "lorem ipsum"
some_string.replace!('em', 'am').split(' ')

would compile to

var some_string = "lorem ipsum";
(some_string = some_string.replace('em', 'am')).split(' ');
aslilac commented 12 years ago

Interesting idea.

I don't think it'll make it in though.

caseyohara commented 12 years ago

I like this idea. I use the do keyword quite a bit, and foo.bar! is more idiomatic than do foo.bar for methods which alter the object.

jussiry commented 12 years ago

Thanks for the feedback, i'd really love to see this feature implemented. Though i don't see how do is related, since do foo.bar only compiles to foo.bar();

hen-x commented 12 years ago

Outside of string operations , I'm not sure that foo = foo.bar() is idiomatic enough in JS to deserve dedicated syntax. Strings are among the only immutable objects in the language, and I'm not familiar with common APIs where instance methods routinely return a modified version of themselves without also mutating the receiver. (Array slices come to mind, but we already have dedicated syntax for these.) In Ruby, the exclamation point is actually part of the method name, whereas under this proposed syntax, every method could be called both with and without the exclamation point; and in most cases, the presence or absence of this syntax would not help a reader to understand the purpose of the code. All existing mutative functions would work as written without the ! mark, and code like node.querySelector! "div" would be possible, even though it represents a search rather than a destructive change.

The proposed syntax would actually be a form of compound assignment, akin to +=, *= etc. I think .= may have already been suggested for this purpose, and while I much prefer the look of this proposed syntax, I feel that it suffers by not looking enough like an assignment. In this form, is it expressive or useful enough to be a part of the language?

alpha123 commented 12 years ago

See #447 and #1335. I like this syntax more than those though, even though it's not as obvious at first what it does.

showell commented 12 years ago

-1

In Ruby there is a big difference between foo.bar!("yo") and foo = foo.bar("yo"). It's like the difference between modifying source code and forking it. In Ruby "bang methods" are hand coded to actually mutate the original object, so that all references to that object benefit from the side effect. In this proposal CS would be using the former syntax to implement the latter semantics, which is just gonna lead to confusion.

Also, there is a strong convention in JS to use non-banged names for mutator methods. Consider Array. The methods pop, push, reverse, shift, sort, splice, and unshift all mutate Arrays without having a bang at the end.

jussiry commented 12 years ago

I know Ruby has '!' only as part of method name, but personally i'v come across situations where i wasn't sure if i should modify the original object or just return the modified version in prototype methods i'v written. And writing both modifying and non-modifying version of the methods feels like bit of a waste, so for me it would be an improvement if the user of that method could decide each time if they want to modify the original object or not.

jussiry commented 12 years ago

Sorry, didn't mean to close this.

showell commented 12 years ago

@jussiry wrote: "And writing both modifying and non-modifying version of the methods feels like bit of a waste, so for me it would be an improvement if the user of that method could decide each time if they want to modify the original object or not."

You have to write two versions of the method. You can't magically have a non-modifying version of a method suddenly modify the original object. Reassigning a variable is not the same as modifying the original object.

caseyohara commented 12 years ago

There is an enormous difference between @jussiry's idea and Ruby's implementation of ! in method names. It's simply a convention in Ruby to mark "dangerous" methods with a !. It doesn't actually do anything -- it's just an indicator -- leaving it up to the developer to make the method actually modify the object.

In your example, foo.bar! would compile to foo = foo.bar();, binding the value of foo to the return of foo.bar with the assumption that foo.bar returns foo (implied in your examples with chaining).

If the !'ed method does not return the object it was called on then you will likely run into issues.

foo =
    bar: (x)-> x * x

# foo.bar!(5)
foo = foo.bar(5)
console.log foo # 25

foo is no longer an object, but is now a number. So:

# foo.bar!(5).bar!(5)
(foo = foo.bar(5)).bar(5)
console.log foo # TypeError: Object 25 has no method 'bar'
showell commented 12 years ago

@caseyohara Just to be clear, are you -1 on this idea now? The intentions behind this proposal are fair enough, but the proposal is flawed, and the issue should be closed.

satyr commented 12 years ago

# foo.bar!(5).bar!(5)

You wouldn't be able to chain like that (as far as I understand the proposal). (foo = foo.bar(5)) = (foo = foo.bar(5)).bar(5) doesn't make sense.

caseyohara commented 12 years ago

I agree. I'm in favor of the idea if the implications of the proposed compilation are clear.

For instance, what are the ramifications of !ing a regular function?

square = (x) -> x * x

square!(5)

# compiles to
# square = square(5);

console.log square # 25

The variable square which was bound to a function is now a number. I'm not sure there's a practical use for that.

Ruby's ! is a convenient naming convention and it's too bad it's not allowed in JS identifiers. But Coffee shouldn't be a port of all of Ruby's conveniences. That said, I think this is an opportunity for Coffee to improve on Ruby's ! by making it a real feature instead of simply a convention.

showell commented 12 years ago

Trying to find a use case for ! is sort of like having a hammer in search of a nail. It's difficult to make ! a real feature, because there's no magical way to turn a non-mutating method into a mutating method. There's a reason that ! is only a convention in Ruby, and not a feature.

Having said that...

You can actually create ! methods in CS, but they're missing syntax sugar. I suppose CS could provide sugar for this code:

counter = (n) ->
  self = {}
  self['incr!'] = -> n += 1
  self.incr = -> counter(n + 1)
  self.n = -> n 
  self

c1 = counter(1)
console.log c1.n() # 1
c1['incr!']()
console.log c1.n() # 2
c2 = c1.incr() 
console.log c2.n() # 3
c1['incr!']()
c1['incr!']()
c1['incr!']()

# c2 is still 3
console.log c2.n() # 3

With sugar, you'd write this:

counter = (n) ->
  self = {}
  self.incr! = -> n += 1
  self.incr = -> counter(n + 1)
  self.n = -> n 
  self

c1 = counter(1)
console.log c1.n() # 1
c1.incr!()
console.log c1.n() # 2
c2 = c1.incr() 
console.log c2.n() # 3
c1.incr!()
c1.incr!()
c1.incr!()

# c2 is still 3
console.log c2.n() # 3

I'm not sure CS really needs this, but it's food for thought.

robotlolita commented 12 years ago

-1

Though Scheme conventions for non-consing methods and predicate methods would be interesting. Too bad ? is already taken =/

Also, there is a strong convention in JS to use non-banged names for mutator methods. Consider Array. The methods pop, push, reverse, shift, sort, splice, and unshift all mutate Arrays without having a bang at the end.

Well... JS doesn't support special characters in an identifier, unicode aside -- though that differs from implementation to implementation. The thing is that ! is used to add distinction between a destructive and a non-destructive version of a functionality, thus it little sense if you only have a destructive version. Imho.

jussiry commented 12 years ago

Seems like i didn't quite think this through; reassigning a variable instead of modifying the original object is a big defect. So i started thinking, what if we create a helper function to change the original object? Something like this:

__changeAtoB = (a,b)->
  throw "Error!" if a.constructor != b.constructor
  delete a[key]  for own key,value of a
  a[key] = value for own key,value of b
  a

And thus foo.bar! would compile to __changeAtoB(foo,foo.bar())

Well, turns out this would work for almost all objects, except for Strings, Numbers and Booleans, since, as noted by @sethaurus, they are immutable. So at this point i think i'll give up. Thanks for the discussion!

ps. I would still +1 @showell's idea on the syntactic sugar to allow ! in function names.

showell commented 12 years ago

@jussiry Allowing ! in identifiers would be problematic at top level scope. Otherwise, I would be more in favor of it. Thanks for both opening and closing the issue--it was definitely worthy of discussion, even if it was ultimately flawed.