Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shortcut for array[array.length - 1] #156

Closed
jashkenas opened this issue Feb 16, 2010 · 45 comments
Closed

Shortcut for array[array.length - 1] #156

jashkenas opened this issue Feb 16, 2010 · 45 comments

Comments

@jashkenas
Copy link
Owner

While implementing the parser, I've often wished that CoffeeScript supported negative array indexes, to peek at elements towards the end.

last: array[-1]

Do you all think it would be a worthwhile addition?

@defunkt
Copy link

defunkt commented Feb 16, 2010

+1 - both Python and Ruby support this

@hen-x
Copy link

hen-x commented Feb 16, 2010

It's a great idea, but would it require wrapping runtime checks around all array accesses and hash["style"] property lookups? If so, it would probably incur a significant cost in terms of speed and generated code size.

@jashkenas
Copy link
Owner Author

Now that you remind me, I think that's the reason why we didn't implement it before. The very best you could do is this:

array[i >= 0 ? i : array.length + i]

But that's pretty nasty for every single non-numeric array access. Hmm.

@hen-x
Copy link

hen-x commented Feb 16, 2010

A compromise: how about having negative array indices meaningful only in the slice syntax? Eg, having list[0..-1] returrn all elements except the last, while list[-1..-1] returns a unit array containing the last element only.

The incremental cost of the feature would be smaller since slices are already more expensive than a lookup, and since slices are only meaningful for numerical indices in the first place. You could even have the compiler recognize the special case where both indices are the same expression, and just generate

[list[a >= 0 ? a : list.length + a]]

when it sees

list[a..a]

@hen-x
Copy link

hen-x commented Feb 16, 2010

Another option would be to add a different array access syntax that is guaranteed to do bounds checking and automatically wrap around, eg.

assert list[..15] is list[..5] if list.length is 10
# list[..i] compiles to list[(i + list.length) % list.length]

assert list[..-1] is list[list.length  - 1]
assert list[..list.length + 5] is list[5] 

@StanAngeloff
Copy link
Contributor

I like the array[:-1] syntax. I was desperately attempting to use it the other day not realising I was in Coffee (doh). I am not particularly fond of the array[-1..-1] syntax, I find it confusing and there is no way to tell what it does if you are not familiar with the language. On the other hand array[...-1] looks good, but since ... (three dots) is an excluding operator, I reckon array[..-1] (two dots) is correct.

@jashkenas
Copy link
Owner Author

After thinking about all the issues raised here, and how they would interact... I don't think that there's a good way to add this for the (admittedly minor) gain it provides, while maintaining consistency. And it's not worth introducing a new syntax for. The only thing we could realistically do is straight transformations of negative array literals into the appropriate access, like so:

array[-1]

Into:

array[array.length - 1];

But then this would fail, probably unexpectedly:

index: -1
array[index]

It's very much something that needs to be added to the runtime, not the language. So, closing as a wontfix. If you come up with a way to make it work, we'll reopen.

@ben-x9
Copy link

ben-x9 commented Jun 9, 2010

I really think introducing new syntax here is a good idea.

In Ruby and Python, I've always been extremely bothered by the way array indices are zero-based when counting from the front array[0] but one-based when counting from the back array[-1].

By creating a new symbol, say <, that means "count from the back", we can avoid the unexpected fail jashkenas mentioned above, and get zero-based indices both ways:
array[<0]
Into:
array[array.length - 1]

@jashkenas
Copy link
Owner Author

That's a nice idea. To play devil's advocate, briefly: it's not just a syntactic convention in Ruby and Python, you can actually pass a negative value.

array[x]

...could be indexed from the front or indexed from the rear, depending on the value of x. array[<x] doesn't have that flexibility. Also ... would it be possible to use it as part of a larger expression? Or is it only a valid symbol immediately after the opening bracket. Really, this would be a modified form of [ ... ], that meant "count from the back": [< ... ]

Reopening the ticket.

@ben-x9
Copy link

ben-x9 commented Jun 9, 2010

That's indeed true, and I guess I'm playing devil's advocate from the other side.

At the cost of brevity I considered mentioning that there may be advantages to the Ruby and Python approach when writing certain kinds of algorithms (where one might be imagining the elements of an array as a continuous "circle", rather than a line with a start point and a finish point at opposite ends). But it's nothing that can't be replicated with an extra line of code reseting the array index variable -- and in that sense it's just another form of syntactic sugar. The key point is that these hypothetical kinds of algorithms are the least common case. Simple array access is much more common, and therefore making simple array access less verbose and more intuitive is the bigger win. And for that reason I think a syntactic convention that allows zero-based negative indices is better than accepting actual negative values.

I think at the very least we need to allow it for simple expressions. That's an easy win.

For the larger expressions you mention, it's a more complicated matter, and I wrote down a bunch of ideas in my other ticket: http://github.com/jashkenas/coffee-script/issues/issue/382/

It's a long read -- guilty of thinking out loud rather than editing my thoughts in advance -- but in the end I decided that this makes the most sense:
array[1..<1] # everything except for the first item and the last item
array[2..<2] # everything except for the first two items and the last two items

array[2..<0] # everything except the first two items
array[0..<2] # everything except the last two items

array[0..2] # first three items only
array[<2..<0] # last three items only

These would expand out to:
array[1..array.length-2] # everything except for the first item and the last item
array[2..array.length-3] # everything except for the first two items and the last two items

array[2..array.length-1] # everything except the first two items
array[0..array.length-3] # everything except the last two items

array[0..2] # first three items only
array[array.length-3..array.length-1] # last three items only

And we also have a possibility of omitting zeros entirely (which I'm leaning towards as a good idea, but it might be too implicit for too little gain -- it's not much extra effort to just put the zeros in):

array[2..] # everything except the first two items
array[..<2] # everything except the last two items

array[..2] # first three items only
array[<2..] # last three items only

Obviously there are other possible approaches. It would be interesting to hear others' thoughts.

@ben-x9
Copy link

ben-x9 commented Jun 9, 2010

Here's another obvious approach, which I think is what you were suggesting Jeremy -- only allowing the < symbol just after the opening bracket, where it means "count the whole range from the back":
array[<0..2] # last three items

It might arguably be simpler, but I don't like it as much because it doesn't allow "pinching" parts from the middle of an array, which I think is a very common use-case:

array[3..<3] # carve off the head and the tail -- we only want the juicy belly

@jashkenas
Copy link
Owner Author

Sorry Ben. I'm afraid that special symbols just to index the array from the rear is a little too much syntax for my taste. I looked through the CoffeeScript compiler to see how many times we look at the last element of the array, using this pattern:

list[list.length - 1]

And found 15 cases. But I can't imagine that switching those 15 cases to:

list[<0]

Would really be an improvement -- it's pretty mysterious what that second line is doing, especially to those unfamiliar with this ticket. More so when the first form isn't too onerous.

Of course, if you have a particular script that comes up against this all the time, you can make a function for it:

peek: (list, index) ->
  list[list.length - index]

peek list 1

Closing the ticket, once again.

@ben-x9
Copy link

ben-x9 commented Jun 12, 2010

Thanks Jeremy. Although I disagree -- perhaps because the particular program I'm writing makes heavy use of these operations, and I think having them built into the language would make my code more readable -- it's a small issue in the scheme of things.

I'm (still ;-) a big fan of Coffeescript, which is a huge improvement over vanilla JS, and, I think, a standalone achievement as a language (would still have value if it didn't compile down to JS).

I guess the larger issue here is how much to deviate from what incoming members of the Python / Ruby / Javascript communities find "intuitive" due to prior experience with these languages.

I have a bias toward throwing away understood conventions if it means creating something better, but I also understand the advantage of not mixing things up too much.

And thanks for the suggestion, that's pretty much what I've been doing. Not as nice as inline syntax, but sorta does the job.

@hen-x
Copy link

hen-x commented Jun 12, 2010

One last thing: if you know what JS environment you are targetting, and you don't mind tampering with prototypes, you can kinda-sorta hack array[-1] into the language at runtime:

Array::__defineGetter__ (-1), ->
    @[@length - 1]

confirm ['a', 'b', 'c', 'd', 'e'][-1] is 'e'

It won't work everywhere, but it works in v8/node and spidermonkey.

@jashkenas
Copy link
Owner Author

sethaurus: that's really neat.

benlambert: Thanks for the civil response. Regardless of whether or not a particular feature winds up included, these discussions are fantastic to have on the record, and make the archive of issues a real treasure trove for language ideas.

@ben-x9
Copy link

ben-x9 commented Jun 12, 2010

Thanks Jeremy, I enjoy the discussions too :)

sethaurus: That's neat feature -- though unfortunately I'm developing client-side for the lowest common denominator all the way down to IE. I don't think progressive enhancement really works for language features -- though perhaps I should give this more thought :D

@brookr
Copy link

brookr commented Jun 7, 2012

Maybe I'm missing something obvious here, but why not just add a .last(n) method to Array? Roughly:

Array.prototype.last = function(n) {
  n = typeof n !== 'undefined' ? n : 1;
  return this[this.length - n]
}

With modification for it to return a range of objects, this could make even more semantic sense (eg, like ruby):

  • "I want the last one": arr.last()
  • "give me the last 4 elements of the array": arr.last(4)

...rather than coming up with some non-obvious 0-based backwards indexing syntax.

@dhlavaty
Copy link

dhlavaty commented Jun 7, 2012

@brookr +1

@msuarz
Copy link

msuarz commented Jul 5, 2012

'abcz'[-1..] works for me

@da99
Copy link

da99 commented Aug 3, 2012

It seems to fail in this situation:

coffee> a = [ [1,2] ]
[ [ 1, 2 ] ]
coffee> a[-1..]
[ [ 1, 2 ] ]

Using: CoffeeScript version 1.3.3

@michaelficarra
Copy link
Collaborator

@da99: Array::slice is not String::slice. For arrays, a[-1..][0] will give you the last element. I know, it kind of sucks.

@gmile
Copy link

gmile commented Dec 5, 2012

Just for anyone else who stumbles upon this this thread in future. To retrieve the last element of array, use (of course, if you can):

my_array.pop()

@alpha123
Copy link

alpha123 commented Dec 5, 2012

@gmile You do know that removes the last element of the array, right?

@vendethiel
Copy link
Collaborator

[].concat(m_array).pop()

... Wait, what ?

@alpha123
Copy link

alpha123 commented Dec 5, 2012

@Nami-Doc that's so horrendously ugly that it's almost cute, except that it's very, very inefficient.

@vendethiel
Copy link
Collaborator

This was obviously a joke

@alpha123
Copy link

alpha123 commented Dec 5, 2012

I hoped so. :P

@caseywebdev
Copy link
Contributor

Everyone knows the most efficient method is m_array.slice().pop()

@tiye
Copy link

tiye commented Dec 7, 2012

Want it. +1

@samccone
Copy link

samccone commented Jan 1, 2013

+1

@darvin
Copy link

darvin commented Jan 11, 2013

come on!

michaelficarra added a commit to michaelficarra/coffee-of-my-dreams that referenced this issue Jan 14, 2013
Compile to something like `array.length > 0 ? array[array.length - 1] : void 0`

See jashkenas/coffeescript#156 and others.
@lorenjohnson
Copy link

+1

@xixixao
Copy link
Contributor

xixixao commented Apr 21, 2013

+1 (note from top of the thread: array[-1] is not a workable syntax)

@swistak
Copy link

swistak commented May 15, 2013

Would compiling array access to something like this:

var _x = (expression);
a[_x < 0 ? a.length + _x : _x]

solve a problem? it shouldn't be a big performance hit, and seems to work with strings correctly.

@epidemian
Copy link
Contributor

-1

Don't think it's necessary to add special syntax when a simple function would suffice:

last = (arr) -> arr[arr.legnth - 1]
# And then use `last someArray`
# Or:
at = (arr, i) -> 
  len = arr.length
  arr[(i % len + len) % len]
# And then use `at someArray, -1`

Maybe adding new syntax for the array destructuring case where you don't want to get the slice of elements before the last one:

[first, ..., last] = arr
# Or:
[..., secondToLast, last] = arr

@vendethiel
Copy link
Collaborator

Which needs you to declare it at the top of each file, or require it, or include it or...

@swistak
Copy link

swistak commented May 16, 2013

@epidemian Well convention of using negative numbers to represent index from end is pretty well established. and if we went the route of "why introduce syntax if function suffices" we'd get just another variant of Lisp.

@xixixao
Copy link
Contributor

xixixao commented May 16, 2013

Problem is it would be a big performance hit + unreadable JS output. That's why array[-1] won't work (as discussed above).

@swistak
Copy link

swistak commented May 16, 2013

@xixixao Performance hit on doing one comparison and additions in (_x < 0 ? a.length + _x : _x) would be so insignificant that you'd probably wont notice it at all even in small loops (it'd be a fraction of millisecond).
It's acceptable now to sacrifice a little performance for usability - jQuery selectors and/or iterators that have huge overhead and jet it's the most popular web library out there - jsut becouse it's easy to use and makes annoying tasks trivial.

@vendethiel
Copy link
Collaborator

JQuery is popular because it's so easy, not because it has no overhead, and more an more people/frameworks recommenf avoiding it. I don't think the "<?" is not noticeable - we know the for in [a..b] performances problems.

@xixixao
Copy link
Contributor

xixixao commented May 16, 2013

Totally agree with Nami-Doc. You would see the performance hit once you iterate over large arrays etc. But the point is not the overhead, it's the fact that you couldn't avoid avoid this compilation. Plus, the unfortunate implicit range direction is enough for me, I wouldn't want another case where I have to worry about hidden bugs because instead of an exception the index wrapped around. Explicit > implicit, imo.

@wizardwerdna
Copy link

Messing with the prototype works, but why rewrite the accessor to support an obscure syntax, when adding a "last" function is 100% clear?

jashkenas added a commit that referenced this issue Jan 24, 2014
Implement #156, Added expansion to destructuring
@lydell
Copy link
Collaborator

lydell commented Feb 26, 2015

In my opinion, the only thing that needs a shortcut here is a way to get the last element of an array as a single expression. The CoffeeScript compiler used to have a last helper function, that you could pass an optional offset from the end. That extra parameter was never used. See #3841.

Want the next to last element? Use [..., nextToLast, last] = arr; doSomethingWith nextToLast.

Need to get an element at a possibly negative index? Use arr = someArray followed by arr[if index < 0 then arr.length + index else index] or arr[index %% arr.length] or whatever is appropriate for your case.

Want the last element? Idea: Use arr[]. if ideas[].beautiful? then console.log 'go for it!' else throw new SyntaxError.

@carlsmith
Copy link
Contributor

SugarJS does the last thing suggested by @brookr: No args gets the last item, or you can pass a number. list.last 4. It's nice to use. If you don't want to extend prototypes, using a function is an ok option, last 4, list.

Negative indexes are really nice to have, but they're problematic to implement, easy to live without, and we often end up wrapping their use with functions anyway. +0

@xavieryao
Copy link

Sounds really great.

protez pushed a commit to protez/coffee-script that referenced this issue Jul 28, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests