A better for loop?

One of the things that winds me up in Rust is that by defult you’re “supposed” to iterate over collections using .map and other fancy iterator methods.

This actually is not a very bad thing, the real problem is that you’re also “supposed” to return results by putting a cheeky ? on the end of the line.

But, as anyone who’s actually used Rust can tell you, you can’t do both. That means if you want to map over a collection with a fallible function (or an async one, or …), you have to defactor your code and use a for loop.

Relatedly, one thing that winds me up about Go is that there’s no real way to map over a list to transform each item in turn, yet this is a very common operation. You’re back in the stone-age creating an empty list (and manually specifying the type) and then using append on every iteration of the loop.

The for-yield loop

I’m sure this is not an original idea, but “what if” a for loop also had a keyword (yield seems good) that let you build up elements on each iteration.

// rust, for-yield loop evaluates to an `impl Iterator<Item = X>`
let a: Vec<_> = for i in 0..10 {
  yield i
}.collect();

// go, for-yield loop evalutes to a []X
a := for i := range 10 {
  yield i
}

The nice thing about a for-yield loop is that you can use it instead of all the nonsense methods. No need for map vs filter_map vs flat_map, etc.

// filter map
a := for i := range 10 {
  if i % 2 == 0 {
    yield i
  }
  if err := whatever() {
    return 0, err
  }
}

// flat map
a := for list := range lists {
  for i := range list {
    yield i
  }
}

Your default error handling keeps working, your returns keep working. You don’t need to invent a ruby-style distinction between blocks and methods.

You can’t build fold like this, but in my opinion that’s a feature – use a mutable variable outside the loop like in the good old days.

For-break

You can also extend the idea slightly to implement find/position:

// find
item := for i := range 10 {
  if i % 3 == 0 {
     break i
  }
}

// find-index
let index: Option<_> = for (ix, i) in (0..10).enumerate() {
   if i % 3 == 0 {
     break ix
   }
}

And even borrow one of my favorite niche Python features, the for-else:

item := for i := range 10 {
  if i % 3 == 0 {
     break i
  }
} else {
  panic("not found")
}

Should we actually do this?

I don’t love that this gives you a super verbose way of creating a value; it’d be kind of annoying to pass a for-yield loop as a function argument for example:

print_all(for i in 0..25 {
  if i % 5 == 0 { yield i }
})

But I guess the same is true of large structs, and you just don’t do that if it’s too hard to read.

It also seems kind of antithetical to Go (which doesn’t in general allow use of statements as values). Rust does allow most statements to return a value (though not a for loop), so maybe we could persuade them to adopt this (but Rust also already has two ways to do it, do they really need another..?).

Time to start building a new language I guess…