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

proposal: Go 2: negative slice/array indexes #33359

Open
pjebs opened this issue Jul 30, 2019 · 19 comments

Comments

@pjebs
Copy link

commented Jul 30, 2019

Currently a negative index to a slice causes a panic.

This proposal doesn't want to change that behaviour.

...except at compiler level.

The compiler, when presented with a -x in the slice index pretends it is equivalent to len(s)-x.

It is essentially a macro (syntax sugar). It only applies when a human types it in code. It does not apply if you use an int variable of negative value.

@gopherbot gopherbot added this to the Proposal milestone Jul 30, 2019

@gopherbot gopherbot added the Proposal label Jul 30, 2019

@pjebs

This comment has been minimized.

Copy link
Author

commented Jul 30, 2019

v := m[-3]

// is equivalent to

v := m[len(m)-3]

@pjebs

This comment has been minimized.

Copy link
Author

commented Jul 30, 2019

The advantage is that when writing complex algorithms, it improves the readability for some people. It seems more intuitive for -2 to mean second last item in slice compared to len - 2 which requires an extra step in cognitive processing.

@mvdan

This comment has been minimized.

Copy link
Member

commented Jul 30, 2019

This has been proposed in multiple different ways in the past. How does this proposal compare to the earlier issues? For example:

#11245
#16231
#20176

I personally think that #25594 was the best of the group, but it was ultimately closed by the author as it didn't seem like a clear win.

@bcmills

This comment has been minimized.

Copy link
Member

commented Jul 30, 2019

What are the constraints on the x in -x? Must it be an integer literal? What about (typed or untyped) constants?

What happens if x is an int with a negative value? (Consider https://play.golang.org/p/234hf3Youmr.)

@ianlancetaylor ianlancetaylor changed the title Proposal: Go 2: negative index slices proposal: Go 2: negative index slices Jul 30, 2019

@pjebs

This comment has been minimized.

Copy link
Author

commented Jul 31, 2019

  1. You can't use a variable or constants.
  2. It's only for an index. Not an index range (but perhaps this is doable).

The key is to think of it as a macro.

@bcmills

This comment has been minimized.

Copy link
Member

commented Jul 31, 2019

Treating constants differently from literals seems very non-orthogonal.

Treating literals differently from variables seems surprising. Since the operation in question modifies the behavior of index expressions, I would expect it to be indicated by something that looks more like a variant of an index expression than an ordinary mathematical operator, especially for something that is merely syntactic sugar.

@bcmills

This comment has been minimized.

Copy link
Member

commented Jul 31, 2019

Here's an alternative to consider: if you want to reduce the stutter, consider the len([…]) expression to be elided. Normally, we indicate elision in Go using the _ character. So perhaps _ within an index expression could stand for len([…])?

x := someDescriptiveSliceName[_-3]
@pjebs

This comment has been minimized.

Copy link
Author

commented Aug 1, 2019

_ seems unnecessary to me.

@deanveloper

This comment has been minimized.

Copy link

commented Aug 3, 2019

This isn't purely at a compiler level because you can only know the value of an int at runtime. For instance:

func reversedIndex(slice []int, index int) int {
    return slice[-index] // we can't know if this is "-index" or "len(slice)-index" until runtime
}

Technicalities aside, I think that the necessity of having a special case for negative integers is a bit inconsistent, however I understand it's use because of backwards compatibility and out of necessity in order to flip the integer's sign before indexing normally into an array. But the necessary evil seems like an exception that makes the feature not feel... right.

@deanveloper

This comment has been minimized.

Copy link

commented Aug 3, 2019

Sorry, I hadn't seen that. I thought I read the comments but didn't see that part.

The compiler, when presented with a -x in the slice index pretends it is equivalent to len(s)-x.
It is essentially a macro (syntax sugar). It only applies when a human types it in code. It does not apply if you use an int variable of negative value.

That part in the initial proposal seems to indicate that variables are allowed. Might want to reword that and explicitly state that variables are not allowed.

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

commented Aug 27, 2019

Another idea: permit len without parentheses, or len() with nothing in the parentheses, within an index operand (inside the square brackets). It would evaluate to the length of the slice. Then one could write s[len-1] to get the last element of a slice. I don't know if that is a good idea but it would be a minor simplification and would avoid the non-orthogonality of treating negative constants specially. And it seems clearer than ....

@griesemer griesemer changed the title proposal: Go 2: negative index slices proposal: Go 2: negative slice/array indexes Aug 27, 2019

@cespare

This comment has been minimized.

Copy link
Contributor

commented Aug 28, 2019

@ianlancetaylor I like that idea. It doesn't help much if the name of the slice is s, but it makes a difference with longer names:

t := deferredTypeStack[len(deferredTypeStack)-1]

becomes

t := deferredTypeStack[len - 1]

@bcmills's idea seems reasonable as well:

t := deferredTypeStack[_ - 1]

A couple of questions that would need to be answered with either of these:

  1. Does len/_ work with arbitrary expressions, and it takes the value of the nearest surrounding indexing expression?
  2. Does it work for maps as well?

So for example, if the answer to both is "yes", then I suppose you could do something like this (not saying it's particularly good or useful):

m := make(map[string]int)
for item := range <-ch {
	m[fmt.Sprintf("channel item %d", len+1)] = item
}
@pjebs

This comment has been minimized.

Copy link
Author

commented Aug 28, 2019

s[len-1]

I don't think that suggestion is a big enough advantage in the real world. The governing idea of my proposal is to reduce the cognitive processing involved when reading complex algorithms.

When attempting to get a gist of what an algorithm does, seeing a -1 "clicks" conceptually more frictionlessly in my mind than seeing len-1.

Also note that using len is not backward compatible because someone might have redefined len to their own variable.

Python is a language that regularly uses negative indices and it helps with readability (for me, immensely). I've never seen a Python developer complain about negative indices, but I suspect they will miss the feature if it was removed.

I don't know if that is a good idea but it would be a minor simplification and would avoid the non-orthogonality of treating negative constants specially.

I don't know much about the actual developer time to implement my proposal but I suspect it would be minimal to treat it as a special case.

@deanveloper

This comment has been minimized.

Copy link

commented Aug 28, 2019

When attempting to get a gist of what an algorithm does, seeing a -1 "clicks" conceptually more frictionlessly in my mind than seeing len-1.

I disagree. A new user of Go would have much less trouble with len - 1 than -1. The only reason that -1 makes any sense at all is because you're already used to it from other languages (ie Python), however other people are not used to these same features.

Sure, python allows -1 to mean len - 1, but in JavaScript -1 just means the -1 index rather than len-1. In Java it would just throw an IndexOutOfBounds, and in C I think(?) the behavior would be undefined.

Go has always favored verbosity in order to promote clarity, and I think that len - 1 is a great way to do that. The benefit of the reduction of cognitive processing will be achieved once it is widely used and our eyes get used to it, just as it was in languages such as Python.

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

commented Aug 28, 2019

@pjebs Actual developer time isn't a concern here. The concern is lack of orthogonality in the language. It's confusing to add a complex new concept: an expression that can be any non-negative value, or can be a negative constant, but cannot be a negative value that is not a constant.

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

commented Aug 28, 2019

@cespare I think that len with no argument would implicitly take as an argument the immediately surrounding index expression, without re-evaluating it. It would be an error to use it outside of an index expression. I suppose it should work with maps as well although it's hard to imagine people using it much.

@cespare

This comment has been minimized.

Copy link
Contributor

commented Aug 28, 2019

@ianlancetaylor when you write s[len-1], isn't len part of a subtraction expression, not (immediately) inside an index expression?

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

commented Aug 28, 2019

Yes, by "immediately" I meant to describe what should happen with s1[s2[len - 1]].

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

commented Sep 3, 2019

The most common case here is probably len(s) - 1. It's worth noting that if we had generics, we could have library functions like slices.Last(s) and slices.Pop(s) that would work with any slice type. That might remove many of the cases where this is valuable.

I agree with @deanveloper that s[-1] does not click for me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
7 participants
You can’t perform that action at this time.