Currently, the best you can get for iteration of Lists and Hashes is to do each-ing the way you'd go about it in Ruby, except that the language currently lacks iterators.
That's not to say I don't want iterators. But I've spent a huge amount of time of this release in making sure that all of the built-in classes get dynaloaded so that memory usage stays low. Now that everything's getting dynaloaded properly, I've less of a problem with doing iterators.
The biggest design mistake in Java was the omission of stack allocated value types, and the same for C# was the distinction between value and object types. If something can be done on the stack it should be done on the stack - the biggest lie we were told was that "short lived objects are cheap".
Or maybe I'm in a total echo chamber. Working with a client today, they were astonished to find out that:
var x = new List() { 1 , 2 };
var y = x;
x.Clear(); // This affects y.
Absolutely astounded. They also had no concept of local variable scope, thinking they had to manually clear out the List so the next invocation of the function wouldn't have the old data. They aren't fresh devs. They've had professional programming jobs. They've built a successful company based on their code. It does non-trivial work (not just a web UI on a DB) and processes many $/sec. In fact, I'm impressed how successful they are.So who knows, maybe MS's overwhelming feedback is from people that fundamentally don't understand what's going on, and they're the ones with money?
To be fair, I've seen successful C projects with lines like: buffer = malloc(....); // gets memory
(n.b.: when I write C#, it's for game development, and I am constantly thinking about structs versus classes and I'm very careful about them, so my perspective is probably not the same as your average C# developer.)
In case it’s the latter, I’ll just point out that you can have zero-cost abstraction because, in C#, `foreach` is pattern-based: An object is enumerable with respect to `foreach` whenever:
1. It has a public method named “GetEnumerator” of type ‘E
2. 'E has a method named “MoveNext” of type () -> bool and a property named “Current” of type () -> ’T (where ’T can be any class (including those that inherit from System.ValueType.. i.e. a struct)).
While it is the case that `yield return` does result in allocation for a reference type, it’s also true that this is easily remedied should it become a problem: In my experience, it only takes about 5-10 minutes to implement a zero-cost iterator as I described above.
Many of the BSL collection classes (e.g. System.String (as an IEnumerable<char>) and System.Collections.Generic.List<T>) use the zero-cost abstraction.
I never got around to writing in detail about it, but I have that goal with iteration in Wren, and came up with a little iteration protocol[1] that I think accomplishes it.
In most languages a loop structure like:
for (i in sequence) {
print(i);
}
Desugars to something like: {
var __iterator = sequence.getIterator()
while (__iterator.moveNext()) {
var i = __iterator.getValue()
print(i)
}
}
You've got one fundamental piece of state: a cursor that tracks where you currently are in the sequence. This is separate from the sequence itself because you can iterate over the same sequence multiple times, sometimes even simultaneously.And you have three fundamental operations you need to perform:
1. Produce a new cursor pointing to the beginning of the sequence.
2. Advance the cursor and determine if we've reached the end.
3. Get the value at the current cursor position.
In most languages, the last two operations are defined as methods on the cursor itself. That means for any given sequence, it needs to define a cursor type specific to that sequence (and storing a reference to it), and you have to allocate that object at runtime, mainly so that you can use it as a dispatch token to find its implementations of those two operations.
That's kind of lame. In Wren, it desugars a little differently:
{
var __iterator = null
while (__iterator = sequence.iterate(__iterator)) {
var i = sequence.iteratorValue(__iterator)
print(i)
}
}
The first two operations are combined into a single iterate() method that uses a "null" parameter to mean "produce a new cursor" and any other parameter to mean "advance". If it returns null or false, we're done with the sequence. Otherwise, it returns a cursor pointing to the next element in the sequence.To get the value at the current cursor position, we call iteratorValue() on the sequence passing the cursor in.
This means the cursor object is pure state—it's just a bit of data that gets passed repeatedly to the sequence. The sequence itself has the dynamic dispatch of the methods for its own iteration protocol.
What this means in practice is that for simple things like lists, you can use a dead simple cursor type: a number.
The implementation of the protocol for lists looks pretty much like this (except its actually in C in Wren):
class List {
iterate(iterator) {
// Start the at first element.
if (iterator == null) return 0
// Stop at the end.
if (iterator >= count) return null
// Advance.
return iterator + 1
}
iteratorValue(iterator) {
return this[iterator]
}
}
Since numbers are unboxed in Wren, it means many sequences can be iterated without any dynamic allocation at all. It makes it easier to implement the iteration protocol because you don't always have to define a class that stores a reference back to your sequence.Wren is dynamically typed, so you don't really have to think about explicit boxing or unboxing.
> Why not have a separate method on the sequence to define the initial value?
Yes, I am definitely considering that. There's no real reason why that needs to be combined with the method to advance, and I'm honestly not even sure why I originally designed it that way.
A pet peeve of mine is the sloppyness by which .NET allocates things like iterators etc. I'd very much like to see zero-cost abstractions in a new language, so that there is no surprise differences between various ways of iterating over collections. I read it only quickly but couldn't find anything related to how enumeration/iteration works under the hood.