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

Champion "slicing" / Range #185

Open
gafter opened this Issue Feb 26, 2017 · 256 comments

Comments

@gafter gafter added this to the 7.2 candidate milestone Feb 26, 2017

@Thaina

This comment has been minimized.

Copy link

Thaina commented Feb 27, 2017

Do we still need this feature while we have return ref support?

We could just create readonly ref collection couldn't we?

struct ReadonlyRefCollection<T>
{
    T[] array;
    int slice,length;
    public ref T this[int i] { get => ref array[i] }
}
@gafter

This comment has been minimized.

Copy link
Member Author

gafter commented Feb 27, 2017

@Thaina Slicing may end up being just that, though it is possible we will want a special syntax for taking a slice, like e[a:b].

@Thaina

This comment has been minimized.

Copy link

Thaina commented Feb 27, 2017

@gafter Thanks for answer

but then I would disagree for adding syntax just for that

Extension method array.Slice(from,to);or array.Slice(from,count); would be sufficient and more familiar

@chrisaut

This comment has been minimized.

Copy link

chrisaut commented Feb 27, 2017

Would the runtime be smart enough to know it can collect/free parts of eg. an underlying array that is not in a slice? In other words, if I have

var arr = new int[HUGE];
var slice = arr[500:1000];
// assume arr is no longer referenced anywhere except through slice, will it keep holding entire arr alive?

I assume there are huge complications with object headers that would make this very complicated. But it sure would be nice.

@eyalsk

This comment has been minimized.

Copy link
Contributor

eyalsk commented Feb 27, 2017

@chrisaut The way I read it is a slice is a view over an existing array so I think that arr must be alive in order to manipulate it; otherwise, you may need to make a copy of the slice.

@eyalsk

This comment has been minimized.

Copy link
Contributor

eyalsk commented Feb 27, 2017

@Thaina str[a:b] reads a lot nicer than str.Slice(a, b) but if you don't like that you could use the API directly. :)

@Thaina

This comment has been minimized.

Copy link

Thaina commented Feb 27, 2017

@eyalsk Sorry to say that I think completely opposite. It not intuitive at all to assume that array[10:100] means slice it out. It also not sure that what 100 mean count or endIndex. In C# we never use a:b to mean range of number before

On the contrary we had string.Slice to be familiar with

@Thaina

This comment has been minimized.

Copy link

Thaina commented Feb 27, 2017

@chrisaut I think it would be too hard to determined if resizing array, which involve copy and clearing memory, would yield performance when it really necessary. Too many scenario compiler need to do guesswork

such as, what if the sliced array will also dispose in the next minute? Then it no point to waste time dispose the old one anyway

Developer could just explicitly optimized by manually resize

@MovGP0

This comment has been minimized.

Copy link

MovGP0 commented Feb 27, 2017

In case special syntax is needed, I suggest to use the range operator from Pascal, which is .., because it is more closely to the operator in mathematics (unicode character 'horizontal ellipsis'). So instead of e[a:b] we would write e[a..b].

The a..b could map directly to Enumerable.Range(a, b-a).

@chrisaut

This comment has been minimized.

Copy link

chrisaut commented Feb 27, 2017

@eyalsk @Thaina I'm not talking about making copies, slices should absolutely under no circumstance require copying of data, that's their entire point.

I'm talking about making the runtime smart enough to understand, that a given array only has ranges x-y reachable.

An array is basically a bunch of header info, followed by a block of memory.
A slice is a view over this bunch of memory, at a specific offset. If the slice's header itself contains header info such as underlying type, lengths etc., then the original array's header would no longer be needed.

If the GC understood that for arrays (and maybe other types later), before reclaiming it has to check some state somewhere to see if any slices are holing onto it, it could understand that only a small region is "alive" and free the rest.

This is just a rough idea.

@Thaina

This comment has been minimized.

Copy link

Thaina commented Feb 27, 2017

@chrisaut Slice itself never need to copy, unless it trying to do what you want, resize it

To have the mechanism you said, partially return memory to the pool, involve CLR optimization. Not about language or compiler

@eyalsk

This comment has been minimized.

Copy link
Contributor

eyalsk commented Feb 27, 2017

@chrisaut Where did I say that slices are about making copies? please reread what I wrote.

@eyalsk

This comment has been minimized.

Copy link
Contributor

eyalsk commented Feb 27, 2017

@Thaina

In C# we never use a:b to mean range of number before

There's always first time...

We didn't have many things in C# and now we have them so it isn't really an argument, if you don't find the syntax pleasant to read then that's a different story. :)

@MovGP0

This comment has been minimized.

Copy link

MovGP0 commented Feb 27, 2017

I am also wondering what happens when the b in e[a:b] becomes b<a. Will the slice be counted backwards or will there be an exception?

Version 0
When b<a an exception is thrown.

int[] primes = new int[] { 2, 3, 5, 7, 9, 11, 13 };
int[:] a = primes[5:2]; // throws an exception
int[:] b = primes[1:0]; // throws an exception
int[:] c = primes[0:-1]; // throws an exception

Version 1
When b<a, it counts backward from element e[a] by b elements. This maps to primes.Slice(int from, int count).

int[] primes = new int[] { 2, 3, 5, 7, 9, 11, 13 };
int[:] a = primes[5:-3]; // A slice with elements {11, 9, 7} 
int[:] b = primes[1:-2]; // A slice with elements {3, 2} 
int[:] c = primes[0:-1]; // A slice that yields {2} and then throws an exception

Version 2
When b<a, it counts backward from element e[a] to element e[b]. This maps to primes.Slice(int from, int to).

int[] primes = new int[] { 2, 3, 5, 7, 9, 11, 13 };
int[:] a = primes[5:2]; // A slice with elements {11, 9, 7} 
int[:] b = primes[1:0]; // A slice with elements {3, 2} 
int[:] c = primes[0:-1]; // A slice that yields {2} and then throws an exception
@Thaina

This comment has been minimized.

Copy link

Thaina commented Feb 27, 2017

@eyalsk Well, that's why most of the time I would like to recycle keyword more than add new

And as I said, it's not that we can't. I just argue that it not intuitive because we never have it. Compare to what we already have, a slice function

@eyalsk

This comment has been minimized.

Copy link
Contributor

eyalsk commented Feb 27, 2017

@chrisaut Just to make it crystal clear, what I meant is that if you no longer need a reference to the array but you still want to have the slice around then I think that you will need to make a copy of the slice, just a hunch based on my understanding, I could be wrong.

@chrisaut

This comment has been minimized.

Copy link

chrisaut commented Feb 27, 2017

@eyalsk I understand what you said to mean that for what I want, slices would need to copy. I don't think it does. Apologies if that's not what you want.
I agree with you that in the normal/straight forward approach you need to copy if you no longer want the array, but only the slice. I'm actively questioning if we can make it so that that is not needed.

@Thaina yes of course it would require clr/gc work, but many new features would require that. I'd guess slices in general will need runtime support (regarless of the partial freeing) as slices are not just a new Type.

@eyalsk

This comment has been minimized.

Copy link
Contributor

eyalsk commented Feb 27, 2017

@Thaina async/await is intuitive? pattern matching is intuitive?

@Thaina

This comment has been minimized.

Copy link

Thaina commented Feb 27, 2017

@eyalsk Well, intuitive relative to what? I was talking slice comparing between string.Slice and that f:t syntax

Actually I would argue that many feature of pattern matching is also really not intuitive. I was propose alternative many times but I am not contributor so my voice is not important

But still many feature of pattern matching are intuitive. such as case. But if it me I would use switch instead of introducing match

But the point is many feature in pattern matching have nothing to comparing to. So add new one is not necessary to be intuitive

But async/await is good example. It is really more intuitive to write code than callback. You can write a loop intuitively just add await

I would also argue that we should not need await and should only have async keyword in Task, like yield can be used in IEnumerable without special syntax

But as I said, intuitive is relative. async/await still more intuitive than callback. But f:t is less intuitive than Slice in my opinion

@eyalsk

This comment has been minimized.

Copy link
Contributor

eyalsk commented Feb 27, 2017

@chrisaut

I'm actively questioning if we can make it so that that is not needed.

Just to cite the relevant part from the proposal:

As demonstrated in this code example, slicing wouldn’t make a copy of the original data; rather, it would simply create an alias for a particular region of the larger range. This allows for efficient referencing and handing around of a sub-portion of an array without necessitating inefficient copying of data. However, if a copy is required, the ToArray method of Slice could be used to forcibly introduce such a copy, which could then be stored as either an array or as a slice (since arrays implicitly convert to slices):

Therefor, I wouldn't expect any clever tricks here but I could be wrong. :)

@Thaina

This comment has been minimized.

Copy link

Thaina commented Feb 27, 2017

@eyalsk I think what @chrisaut means is.

If we have alloc memory for array, suppose from ptr0 to ptr50

Then we slice it at ptr20 to ptr30 and then stop using the original array

CLR could dealloc ptr0 to ptr18 and ptr31 to ptr50. And maybe the implementation of slicing array could force CRL to check that

But I don't think CLR can do thing like this

@eyalsk

This comment has been minimized.

Copy link
Contributor

eyalsk commented Feb 27, 2017

@Thaina

intuitive relative to what?

Intuitive relative to the person, many language constructs require some learning so this wouldn't be any different, especially when the person has no prior knowledge.

Actually I would argue that many feature of pattern matching is also really not intuitive. I was propose alternative many times but I am not contributor so my voice is not important

Thaina being a Contributor doesn't mean "anything" and your voice is as important as mine.

But async/await is good example. It is really more intuitive to write code than callback. You can write a loop intuitively just add await

It's straightforward to create asynchronous methods but it isn't straightforward to understand how asynchronous code works and what it means, especially for people that never did such work before.

My point being is that there's a bit of learning to do before you can use a feature and not so much about how it can be more intuitive...

@Joe4evr

This comment has been minimized.

Copy link

Joe4evr commented Feb 27, 2017

I am also wondering what happens when the b in e[a:b] becomes b<a.

Existing methods in the BCL that have similar behavior (such as string.Substring()) have the arguments startIndex, length. I would hope that this convention carries forward to slices as well.

@Thaina

This comment has been minimized.

Copy link

Thaina commented Feb 27, 2017

@eyalsk

Intuitive relative to the person

Not at all. Relative is objectively compare with existing things. Even leaning curve is also relative to what exist and what newly introduced

being a Contributor doesn't mean "anything" and your voice is as important as mine

Disagree. people who not being contributor might be important but I never see it will be the same importance as contributor

It's straightforward to create asynchronous methods but it isn't straightforward to understand how asynchronous code works and what it means, especially for people that never did such work before.

Still the same word, relative. If there are newbie learning C# there are callback function and async/await we can present to let them learn how to do asynchronous. async/await has less learning curve compare to callback function that not straightforward

My point is comparing learning curve with similar feature is necessary. And I would vote that we should better use array.Slice more than array[f:t] because it more intuitive and familiarity

@eyalsk

This comment has been minimized.

Copy link
Contributor

eyalsk commented Feb 27, 2017

@Thaina

Not at all. Relative is objectively compare with existing things. Even leaning curve is also relative to what exist and what newly introduced

And a person isn't a thing?

Disagree. people who not being contributor might be important but I never see it will be the same importance as contributor

Thaina... let's not get into the meaning of a Contributor when it's pretty well defined and has absolutely nothing with the subject.

And I would vote that we should better use array.Slice more than array[f:t] because it more intuitive and familiarity

Thaina you would still have the ability to use the API directly.

I understand that you don't like the syntax but what do you think about the syntax that was proposed by @MovGP0? personally I find the proposed syntax : pretty nice.

@MovGP0

This comment has been minimized.

Copy link

MovGP0 commented Feb 27, 2017

i am totally happy with the : syntax. Just would argue that the .. syntax can be used more universally like in

foreach(var item in 3..5){ /* 3,4,5 */ }

foreach(var item in a[3..5]){ /* a[3], a[4], a[5] */ }
@Thaina

This comment has been minimized.

Copy link

Thaina commented Feb 27, 2017

@eyalsk

Thaina... let's not get into the meaning of a Contributor when it's pretty well defined and has absolutely nothing with the subject.

As always you are the one who pick the word to argue with me. So you stop yourself not telling me to stop what I don't start

Thaina you would still have the ability to use the API directly.

If that's your argument then you would never ever come mess with my threads in the past. Remember when you annoyingly mock me when I try to introduce delete keyword? Why you not just think that you could still use dictionary.Remove?

To introduce new syntax have many cost and drawback or else we would have all syntax we could imagine already. Same here that you like this syntax so you want it and I'm against it because it make code more weird and cost too much for little benefit

I know why you try to argue with me in the past even you make fun of me in every aspect and make our argument become silly. You should understand yourself too

@DweeberlyLoom

This comment has been minimized.

Copy link

DweeberlyLoom commented Sep 21, 2018

@orthoxerox

However, what does it mean to iterate over a range '0..^5' when there isn't an array applied against the range? The relative "hat" operator has to be relative to something.

I would expect Range.GetEnumerator() to throw if any one of the indices is reversed.

In #198 I see comments where people suggest being able to use a range outside of the slicing context. For example "foreach (var i = 0..5) {}" which makes sense, but if the range contains some sort of relative offset (0..^5). Would that be a syntax error? Is that a special case for the "hat" operator, or when using a range in that way would it be a different kind of range than one used for slicing?

These aren't complaints, just questions. I like the ideas behind the suggested features.

@DweeberlyLoom

This comment has been minimized.

Copy link

DweeberlyLoom commented Sep 22, 2018

@HaloFour & @CyrusNajmabadi

I agree language design and syntax is hard and there are lots of limited factors, especially when dealing with an existing language. I think the C# team has done an excellent job, I was particularly impressed with how the various features needed for Linq were architected as independent functionality.

I don't necessarily agree that the choices for this operator are severely limited. It does not have to be a single character. 'await', prefixed decrement and increment and type casts are considered unary operators (https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/).

I have previously suggested some alternatives, but I think it might be more constructive to think about what the operator is. bbarry posted code from the current track showing Index with a FromEnd property and Range with FromStart and ToEnd properties. The current "hat" unary operator acts on Index.

How is this expressed in discussion. For example the => operator is called the lambda operator and is expressed as "goes to/becomes/such that" (see: https://blogs.msdn.microsoft.com/ericlippert/2008/05/16/reading-code-over-the-telephone/). If you accept that -> indicates 'points to', it isn't hard to interpret => as 'goes to'. "Hat" isn't an overly descriptive term and says nothing about what the operator does. Is this the "from end" operator, if so what about the glyph '^' indicates that?

@CyrusNajmabadi

This comment has been minimized.

Copy link

CyrusNajmabadi commented Sep 22, 2018

I have previously suggested some alternatives,

could you link to those?

if so what about the glyph '^' indicates that?

The language spec :)

I have no idea what about * means pointer to, nor do i have any idea why * means to dereference, or & means to get the address of. I have no idea why '@' allows me to use a keyword as an identifier. I don't know why curlies are used both for creating a statement block, but also for surrounding the elements of a collection. I have no idea why ** is to exponentiate in some languages, and ~ is to twiddle bits. I don't know why / starts a regex in JavaScript/TypeScript. I don't know why ocaml picked @@ for function application. Or why C++ uses && as an rvalue reference. I have no idea why some languages use many of the operators they do (including C#, and i've contributed to that language for more then 15 years). And the end of the day, i don't really care. :)

@HaloFour

This comment has been minimized.

Copy link
Contributor

HaloFour commented Sep 22, 2018

@DweeberlyLoom

I'd agree that "hat" isn't a particularly helpful descriptor.

I can't speak for how the language team came to decide on that symbol except out of possibly a process of elimination. To me I see the logic in the symbol appearing somewhat like an upward arrow. If you imagine the beginning of a range as the top, the end would be at the bottom, and ^ points away from the bottom. Tenuous, indeed, but I don't think any moreso than many of the operators that we've come to accept.

@Xenoprimate

This comment has been minimized.

Copy link

Xenoprimate commented Sep 22, 2018

I have no idea what about * means pointer to, nor do i have any idea why * means to dereference, or & means to get the address of.

"*" is a pointer because it's pointy; and "&" is the andress of something! Hahaha... But of course I kid; those were just mnemonics I used to use when learning.

Anyway... The only operator I think might be better would be something like !3 because ! is kind of the 'opposite' operator already (yes, it's really negation, I know). So you could have [0..!0].

But I guess maybe it would clash with types that have inbuilt overloads for very-rarely-used true/false operators.

And even then I'm not really convinced anyway.

Personally anyway I happen to agree that the actual symbol used isn't that important. $ for interpolated strings makes no sense to me (the only other time I've used $s in programming is declaring PHP variables), but I still use that with no problem every day.

There is also an option of a new keyword, maybe something like [0..0 fromend]. But again, just throwing ideas out. shrug

@yaakov-h

This comment has been minimized.

Copy link
Contributor

yaakov-h commented Sep 22, 2018

Maybe the hat operator should be an RTL control character instead, since we're counting from the other end of the collection. 😉

@qrli

This comment has been minimized.

Copy link

qrli commented Sep 22, 2018

  • The range form [a..b] has no problem to use negative numbers to mean index-from-end, like what many other languages do.
  • The obstacle is that it cannot be applied to normal array indexing form, e.g. arr[-1], as it would be a breaking change. That caused the birth of the hat operator.

It is like JavaScript has to introduce === because it cannot fix the ==. It does not matter objectively, but it matters subjectively.

Personally, I'd prefer to fix the array index form rather than creating a new operator. One idea is to reuse the ! operator which is introduced by non-nullability. (I'm not so happy with this idea though.) E.g.

arr[-1] // throws as before
arr[-1!] // I know what I am doing, so enable index-from-end

Or, instead of letting ^ mean create-index-from-end, let it mean create-index:

arr[^1] == arr[1]
arr[^-1] == arr[new Index(-1)]
arr[5..-1] // the .. operator takes Index type so no need for ^

Or, the ^ could be dropped completely, so have to write
arr[new Index(-1)] or arr[Index.FromEnd(1)], but we can still write arr[1..-1].

@DweeberlyLoom

This comment has been minimized.

Copy link

DweeberlyLoom commented Sep 22, 2018

Ok, I'm going to put together multiple replies here.

@CyrusNajmabadi
Here's a link to a previous comment with possible alternatives: #185 (comment)

I hear ya. Programming is filled with abstractions and choices often seem (perhaps are) arbitrary. Even the symbols we use (*&$…) are abstractions of other ideas. Intuitive is what you get used to … mostly.

@HaloFour
Tenuous, yes, but better than not having an analogy/image. I tend to think of this used with LTR strings but thinking top to bottom works with arrays/vectors.

@CyrusNajmabadi

This comment has been minimized.

Copy link

CyrusNajmabadi commented Sep 23, 2018

Here's a link to a previous comment with possible alternatives:

The alternatives suggested seem exactly equally as good/bad as the current approach. So, given that, i would stick with the current suggested approach

Thanks!

@Eirenarch

This comment has been minimized.

Copy link

Eirenarch commented Sep 23, 2018

@Xenoprimate assuming the choices made by C decades ago were the right choices and didn't cause a great deal of pain to the industry because they weren't thought through. I only suspect the * for pointers was a bad idea but for && and || I am sure they were bad ideas. and/or keywords would have been much better. I hope to see less of these glyph based syntax in languages.

@GGG-KILLER

This comment has been minimized.

Copy link

GGG-KILLER commented Sep 30, 2018

I didn't read the entire discussion (sorry), but what if like @qrli suggested instead of the "hat" operator the unary negation operator was used but as an opt-in feature as with #36 or just change the compilation behavior in C# 8 since the user will already have to opt into the new language version?

I believe this would solve both backwards compatibility with already existing code and would remove the need to introduce the new "hat" operator.

@HaloFour

This comment has been minimized.

Copy link
Contributor

HaloFour commented Sep 30, 2018

@GGG-KILLER

but what if like @qrli suggested instead of the "hat" operator the unary negation operator was used but as an opt-in feature

C# couldn't do this even if it wanted to. Indexers are just syntax candy over a normal method call, and it's up to that method to interpret the index as it sees fit. Attempting to go this route means rewriting every indexer method ever written both in the BCL and across the major ecosystem of libraries, which represents a massive undertaking, after which a developer could still never be sure that a library that they're using would support it.

Opt-in dialects of the language are reserved for only the most extreme of cases where it is deemed worthwhile to solve the problem at hand. This is so far limited to nullable references.

@GGG-KILLER

This comment has been minimized.

Copy link

GGG-KILLER commented Sep 30, 2018

@HaloFour

Attempting to go this route means rewriting every indexer method ever written both in the BCL and across the major ecosystem of libraries, which represents a massive undertaking, after which a developer could still never be sure that a library that they're using would support it.

What I actually meant to suggest was a compiler opt-in to transform indexing operations with negated unary integers to an indexing operation with an Index (same as we do with System.Runtime.CompilerServices.IndexerNameAttribute and System.Runtime.CompilerServices.MethodImplAttribute).

But now after thinking more about it I see how badly that could impact already existing indexers that accept unary negated integers and the ambiguity that it'd introduce. I didn't think about the broader picture but only cases with the BCL.

@JeffreySax

This comment has been minimized.

Copy link

JeffreySax commented Nov 14, 2018

@jcouv While I generally like the current proposal, there is one element that stands out to me. It requires that the two arguments to the range operator be convertible to a System.Index. I believe this is both unnecessary and unwelcome.

It is unwelcome because it puts an extra and unnecessary performance burden on the vast majority of code that uses ranges. Indexing from the end is rare compared to regular indexing, even in ranges. The burden comes from the extra code required to test the index value. It may also prevent inlining in some cases.

Furthermore, it puts a constraint on indexes that they must not be negative, throwing an exception if they are. This is not always desirable:

  1. Arrays created with Array.CreateInstance may have negative lower bounds. There are no restrictions on indexer properties.
  2. It can lead to different behaviors for indexers that take one index vs. a range. The indexer method has control of which exception to throw, if any. Using System.Index, the exception is thrown by the System.Index constructor, which is completely outside the control of the indexer property which isn't even called.

Index is a common term. Putting a type with that name in the System namespace may break a significant amount of code.

It is unnecessary because in the vast majority of cases, the compiler knows the size of the collection (or dimension) that is being indexed.

The solution: The Index operator should be pure syntactic sugar, and only be allowed in the argument list of indexer properties.

The compiler should replace it with an expression that calls the appropriate method to get the length of the dimension (for example, IList<T>.Count for 1D, GetUpperBound(dim) for ND). If such a method cannot be found, the compiler can raise an error that the object doesn't support indexing from the end. As with compound assignment operators, the target of the indexer call should only be evaluated once.

@HaloFour

This comment has been minimized.

Copy link
Contributor

HaloFour commented Nov 14, 2018

@JeffreySax

It is unnecessary because in the vast majority of cases, the compiler knows the size of the collection (or dimension) that is being indexed.

That would require that the compiler have knowledge of everything that is indexable and how to obtain its length. It would also make it impossible to pass the "index" around outside of the indexer operation.

@JeffreySax

This comment has been minimized.

Copy link

JeffreySax commented Nov 15, 2018

@HaloFour

That would require that the compiler have knowledge of everything that is indexable and how to obtain its length.

Not everything. Most 1D collections implement IList<T>. Would it be such a loss that indexable objects that don't follow this convention don't support indexing from the end?

It would also make it impossible to pass the "index" around outside of the indexer operation.

That is only a problem if the passed around index could be both from the start or the end. Mostly it would be one or the other but not both.

You can always fall back on what you have to do currently.

The biggest issue for me, even more important than the performance penalty, is that putting an Index type in the System namespace will cause countless CS0104 "'Index' is an ambiguous reference between 'System.Index' and '.Index'" errors in code that compiles fine today.

@CyrusNajmabadi

This comment has been minimized.

Copy link

CyrusNajmabadi commented Nov 15, 2018

is that putting an Index type in the System namespace will cause countless CS0104 "'Index' is an ambiguous reference between 'System.Index' and '.Index'" errors in code that compiles fine today.

That shouldn't be the case. 'Index' types in your own namespaces will be found first. And that's not really a good reason anyways, given that this effectively means CoreFx can't add new types.

@qwertie

This comment has been minimized.

Copy link

qwertie commented Nov 18, 2018

If JeffreySax is correct that the .. operator won't support negative indexes, I agree it's not good. I have occasionally used collections that supported negative indexes.

It would be nice to resolve the tension between performance and generality, but there isn't an obvious and general solution.

If all .. expressions produce one "IntRange" type, then supporting it will require a lot of conditionals, like

public Span<T> this[IntRange r] { get {
   int first = r.First, last = r.Last;
   if (range.FirstHasHat) first = this.Count - first;
   if (range.LastHasHat) last = this.Count - last;
   if (first < 0) throw new IndexOutOfRangeException();
   if (last < first || first >= this.Count) return Span<T>.Empty;
   if (last > this.Count - 1) last = this.Count - 1;
   ...
} }

The final ifs may be unavoidable, but the first two aren't - for example, if X..^Y could produce a different type than X..Y, then instead of checking FirstHasHat and LastHasHat here, there could be four different indexers for the four combinations of LastHasHat and LastHasHat. But gee, writing four indexers would be overly burdensome!

It looks like JeffreySax's idea would improve performance in most cases, but it has at least two limitations:

  1. You can't write var x = ^1.
  2. If you write list[^3..^1] and list happens to be an interface, it means list[list.Count-3..list.Count-1] which involves three two interface calls [edit: the compiler can store list.Count in a temporary variable and use it twice], which may be slower than executing a couple of if statements inside the indexer itself.

Also, I disagree that the compiler should only allow the hat with "indexer properties"; it would be interesting not only to allow it to be used with other methods (like list.RemoveAt(^1)) but to allow hats outside collection types (e.g. FileStream.Seek(^1) has an obvious interpretation.) On the whole, I would favor this flexibility over a very small performance win.

@qwertie

This comment has been minimized.

Copy link

qwertie commented Nov 18, 2018

Another option is to support better performance or better flexibility based on context, by allowing the operators involved to be user-defined.

Let us suppose .. (exclusive range), ... (inclusive range), and ^ are all overloadable operators. By default they produce some data type that a collection can consume, and the collection needs "if" statements to check if there was a hat on the first and last index of the range.

But suppose that the operator overloading works a little different than other operators: the operators look for non-static implementations when used in a context of the form foo.Method(^X) or foo.Method(X..Y) or foo.Method(X..^Y) or foo.Method(^X..Y). foo's class could define these operators, for example, as

public int operator^(int x) { return Count - x; }
public (int,int) operator..(int x, int y) => (x, y-1);
public (int,int) operator...(int x, int y) => (x, y);

In this way foo.RemoveAt(^x) always receives the actual index to remove, not some "hat" data type, and foo[x..^y] receives a simple range of integers and need not worry whether the hat operator was used or not. foo's class could overload foo[] to understand an "abstract" range like var r=x..^y; foo[r], but it doesn't have to. Note a small disadvantage in this design, that operator .. and ... are non-static but do not use this, as the non-staticness is merely used as a "dog whistle" to the compiler about when to call it...

Ideally, these operators could be defined by third parties via extension everything.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.