Array#[](range : Range) and similar methods (including the ones added in #7338) should be valid for any Indexable. Most methods could be implemented directly in Indexable and only #[]?(start : Int, count : Int) needs to be abstract and implementation-specific.
Currently, only Array implement a full set of range methods, but most other including classes implement at least some method like #[](range : Range) or #[](start : Int, count : Int).
The only trouble are Tuple and StaticArray which can't create dynamically sized instances of subsets of themselves. So the question is, what type should Tuple#[](range : Range) return. Or should it be possible at all? If not, we can't add this to Indexable.
Instead we could consider adding a module (for example List) which inherits from Indexable and implements range methods and allows modifications using []=, << etc.
The problem with [] that returns several values is what type to return. For Array it's clear: Array. Should Deque return a Deque? What about Tuple and StaticArray as you mention?
That's why I don't think [](range) belongs in Indexable. Maybe another module... but I'm not sure if it will be a big win.
It's possible to return Tuple for Tuple#[range], but this means having potentially huge macro expands, with every possible subsets size of the instance pre-defined in it.
For Tuple I always thought about supporting [](range) in a built-in way, similar to how [](index) is magical. But I didn't find good use cases for that so it might not be worth it.
I've a use case:
def first_n_args(num : Int, *args)
args[0..n]
end
puts first_n_args 2, 'a', 'b', 'c'
I'm using a similar logic for an interpreter, for now I use case/whens with case num; when 0 {...}; when 1 {...} etc, then another case/when to match this Tuple against others.
I don't think it's that necessary, but I like the idea of having #[] for all Indexables.
And what about String? We could include Indexable in it. This means having String#each, String#first and loads of new methods form Indexable
Ruby had each for String, I think. The problem is that a String is a collection of chars and bytes so it's not clear what should be iterated by default. Plus String already implement those tons of methods from Indexable.
Ruby no longer supports String#each since 1.9
I don't think it's wise to include Indexable(Char) because of Char::Reader, nor possible to include Indexable(Bytes) due to #[] methods clash.
I see a String as a collection of Char in an encoding, which only happens to be a collection of bytes for some encodings (e.g. ASCII). I'd expect a String#each to iterate each Char, similar to how String#[] gives me the Char at an index.
By comparing https://github.com/crystal-lang/crystal/blob/master/src/indexable.cr#L186 and https://github.com/crystal-lang/crystal/blob/master/src/string.cr#L3722, this means creating a new Char::Reader for each unsafe_fetch, if we use the actual String#at method.
It will be less efficient, unless we refactor this. It may be beyond my knowledge :grimacing:
Tuple#first(n : Int) and Deque's one returns Array since Enumerable#first(n : Int) is defined, so it's reasonable that Indexable#[](range) returns Array.
IMHO, Indexable#[](start, count) should be defined in Indexable and it returns Array as default. Then we can implement these method by using [](start, count):
[](range)first(n)last(n)skipskip_whiletake_whileWhen some classes require to change these methods result (e.g. Slice), we can override [](start, count).
Most helpful comment
I see a String as a collection of Char in an encoding, which only happens to be a collection of bytes for some encodings (e.g. ASCII). I'd expect a
String#eachto iterate each Char, similar to howString#[]gives me the Char at an index.