I noticed this today because it was posted on Lobsters, but it's actually a few weeks old:
https://harelang.org/blog/2024-04-01-introducing-for-each-loops-in-hare/ (
archive)
I must admit, it's a shame to see such an important language get useless bloat from the last century, but bloat which the C language lacked, making it bad. It's kind of weird for a one hundred year language to get an addition like this.
Today, for-each loops were merged into Hare’s development branch! This includes patches for the compiler, standard library, specification and tutorial. I want to give some background on how we designed for-each loops and what challenges are associated with implementing them.
I'm sure a lot of design went into designing something other people had already designed.
Before for-each loops, you could iterate over an array/slice like so:
C:
let array = [1, 2, 3];
for (let i = 0z; i < len(array); i += 1) {
fmt::printfln("{}", array[I])!;
};
This is also known as the stupid way to do it.
Having an index can be very powerful if you require it. However, in most cases, you will not, and I’ve found that using indices makes code less readable. This is how a for-each loop over an array/slice could look:
C:
foreach (x in [1, 2, 3]) {
fmt::printfln("{}", x)!;
};
It's important to note there are also languages which lacked a loop type like this, but still made the previous loop type safe, by providing a better mechanism for getting each index without writing manual additions and silly things like that.
In addition to for-each loops over arrays/slices, you might remember the concept of iterators from languages such as Rust. The idea is very simple: you have a function, typically called something like next(), that you call every loop iteration. This is useful when, for example, iterating over every line of a file — you don’t have to load the whole file in one go, but only ever read it line by line. This concept is also used extensively in the standard library, but so far, it has been implemented using for (true) and match:
C:
for (true) match (bufio::read_line(file)!) {
case let line: []u8 =>
fmt::printfln("{}", strings::fromutf8(line)!)!;
case io::EOF =>
break;
};
Wow, that looks awful.
While this works, it takes up a lot of lines and is not very readable. We can do better. Here is what that could look like:
C:
foreach (line = bufio::read_line(file)!) {
fmt::printfln("{}", strings::fromutf8(line)!)!;
};
Oh, assignment is an expression in Hare. That's another lesson I see fell on deaf ears.
The purpose of the Hare
RFC process is to discuss and reach consensus on bigger changes to Hare. This is especially important for new language features such as for-each. In the three revisions to the RFC, we found solutions for a number of challenges.
Yes, but what's the RFC mailing list's consensus on gender-affirming care for children?
First of all, there was the syntax question. Do we want foreach? Or should we integrate it into for? We quickly reached the conclusion that it is best to introduce a new syntax to for instead of foreach, because it should remain the only statement that can loop, since there is no while statement in Hare.
That's a very reasonable decision.
C:
// for-each value
for (let x .. [1, 2, 3]) { // The type of x is int here
fmt::printfln("{}", x)!;
};
// for-each reference
for (let x &.. [1, 2, 3]) { // The type of x is *int here
fmt::printfln("{}", *x)!;
};
Well, that still looks awful. The reference idea is just retarded.
Specifying the iterator loops was especially challenging. How should these kind of loops end, by the iterator returning a special type, or void? Do we use a special method for every type or will it just be a function call? What kind of operator do we want to use? After lots of discussion, this is the syntax we came up with:
C:
for (let line => bufio::read_line(file)!) {
fmt::printfln("{}", strings::fromutf8(line)!)!;
};
That's tolerable, I guess.
This “for-each iterator” loop executes its binding initializer, bufio::read_line(file)!, at every start of the loop. This initializer returns a tagged union with a done type. In this case, the initializer returns ([]u8 | io::EOF). This is a tagged union, which is a type that can contain one of []u8 or io::EOF. Unlike a union in C, it also indicates what type it currently holds, using a tag.
io::EOF is defined as a done type. This means that the for-each loop checks if bufio::read_line(file)! returns done and terminates the loop in this case. Otherwise, it will assign the value to the line variable and run the body of the loop. The type of this variable is []u8, because that is the type that is left in the tagged union when excluding the done type.
I don't think I need to comment on these two paragraphs. They're perfect the way they are.
The implementation of for-each proves that the Hare RFC process is working really well. We were able to collectively come up with a for-each design that fits Hare really well and that everyone is fine with. I am, personally, very happy with the end result.
I prefer my languages to be done before people start using them, but whatever.
I'd like to thank null for adding Hare syntax highlighting to the formatting options, although I'm not certain why I have to call it C.