823e7be15ffd9c9821981276a19a93c65b4dd570 — Lorenz (xha) 17 days ago 7dee845
blog: introducing for-each loops in hare

Signed-off-by: Lorenz (xha) <me@xha.li>
Signed-off-by: Vlad-Stefan Harbuz <vlad@vladh.net>
Signed-off-by: Drew DeVault <sir@cmpwn.com>
Reviewed-by: Vlad-Stefan Harbuz <vlad@vladh.net>
Reviewed-by: Drew Devault <sir@cmpwn.com>
1 files changed, 126 insertions(+), 0 deletions(-)

A content/blog/2024-04-01-Introducing-for-each-loops-in-Hare.md
A content/blog/2024-04-01-Introducing-for-each-loops-in-Hare.md => content/blog/2024-04-01-Introducing-for-each-loops-in-Hare.md +126 -0
@@ 0,0 1,126 @@
title: Introducing for-each loops for Hare
author: Lorenz (xha)
date: 2024-04-02

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.

## What kind of for-each loops do we want?

Before for-each loops, you could iterate over an array/slice like so:

let array = [1, 2, 3];
for (let i = 0z; i < len(array); i += 1) {
	fmt::printfln("{}", array[i])!;

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:

foreach (x in [1, 2, 3]) {
	fmt::printfln("{}", x)!;

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`:

for (true) match (bufio::read_line(file)!) {
case let line: []u8 =>
	fmt::printfln("{}" strings::fromutf8(line)!)!;
case io::EOF =>

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:

foreach (line = bufio::read_line(file)!) {
	fmt::printfln("{}" strings::fromutf8(line)!)!;

With these changes in mind, I wrote the first version of the RFC to discuss
these changes to the language.

## How should these loops really look?

The purpose of the Hare [RFC process][0] 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.

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.

// 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)!;

As you can see, we've also introduced another variant: for-each reference loops.
Instead of assigning the value itself, we assign a pointer to the value. This is
useful when you want to manipulate values in the slice/array itself or your
values are too big for copying and it's unlikely that an optimizer would catch

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:

for (let line => bufio::read_line(file)!) {
	fmt::printfln("{}" strings::fromutf8(line)!)!;

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.

## Closing thoughts

There is still a lot of Hare code that needs to be updated to use the new
for-each loops. In particular, the extended libraries haven't received updates
yet. We plan to go over this code during the regular course of our work.

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.