Iterators in Ribbon
Ribbon currently supports iterators, and I plan on supporting generators soon, though I haven't fully planned out what the semantics for generators will be yet.
Making an iterator
There's two easy ways to make an iterator. The first is to simply create a list:
let my_iter = [3, 6, 9]
The second way is to use some interesting syntax I came up with for iterators:
let my_iter[i: [0, 1, 2]] = 3 * (i + 1)
Once again, I can decipher this for you.
0. Yes, I came up with this idea from inspiration by my function declaration syntax (e.g.
let f(x) = ...
). I tried identifying the logical analog for the index operator
[]
in a declaration, and I arrived at this. Interesting, isn't it?
- The index
i
has the type of[1, 2, 3]
. In Ribbon, types are nothing more than some kind of expression which denotes what values are legal members or not. Types can be expressed as type literals, as arbitrary functions that return a Boolean (i.e._ < 3
, which for reference evaluates to the lambdax => x < 3
), and they can even be expressed as a list of valid members. However, you can imagine that there are caveats to what I can accept here. I'm currently only allowing a type value which defines a countable enumeration for itself, which includes lists (like above) or types likeNat
. - Only integer-based indices are currently allowed, but I'm considering allowing arbitrary values which will create a dictionary instead. Technically this means lists are a subset of dictionaries.
- Currently, this is implemented using lazy evaluation, similar to a function definition, which makes it a proper traditional iterator.
- As a bonus, this makes for easy infinite iterators, such as
let my_iter[i: Nat] = i
Iterating over an iterator
Typically, this is when you'd use a for loop. So, perhaps you'd be surprised to hear that I have not implemented for loops yet! I will... eventually... probably. In the mean time, I've been using a map operator, one that I'm quite proud of.
So, as far as operators go, the map or apply operator is pretty modern, and hence
late to the game. Most programming languages don't have dedicated operators for it, instead using
methods like .map()
. I always thought it was a little clunky, but I also didn't want to
introduce a new operator token.
However, I had an interesting idea. Conceptually, I want Ribbon to support calling functions
f(x, y)
using syntax like x.f(y)
, like the D language. This is quite handy
for a lot of cases. In fact, quite a few languages support this, but they use something called the
pipe operator, typically with the token |>
. So instead of
x.f(y)
, they'd write x |> f(y)
.
I don't think it's a surprise that I find the dot notation is quite a bit more elegant, if not for any reason more than it avoids introducing a new operator. I thought how the map operation is quite similar to the pipe operation, with the only difference being how the map operator applies to lists. But Ribbon already distinguishes list types from non-list types! I could reuse the dot operator!
Hear me out! So, if x.f(y)
calls a function f
with x
as the
first parameter, then x.f
should logically be equivalent to the lambda
y => f(x, y)
. Additionally, because of Ribbon's list semantics, if x
is
a list, then x.f(y)
and f(x, y)
return a list of the results for each
element of x
. So, it could be argued that this .f
is already
mapping the list x
to a new list of outputs. In which case, all we'd need to
do is replace the f
in .f
with whatever function you wanted to use to map.
And you can just insert parenthesis for that!
[1, 2, 3].(_ > 1) // equals [false, true, true]
It's so clean! No new operators, no ambiguities, and it can be chained easily! And supports syntax like for loops!
[1, 2, 3].(i =>
let x = 3 * i
print(x)
)
I don't think I've mentioned this yet, but Ribbon considers white space when parsing, to a higher
(and hopefully smarter) degree than Python does. I tried really hard designing it to not be
annoying. In the case above, this is equivalent to
[1, 2, 3].(i => { let x = 3 * i; print(x) })
.
Note that this also means we can use this as a pipe operator, such as 3.(_ + 4)
(equals
7).
Also note that when given a lazy iterator, the map output is also a lazy evaluator.
Imperative access
Of course, to support systems programming and other low-level applications, I will also provide imperative access to iterators. Here's what I currently have planned:
var my_iter = [1, 2, 3]
let a = &my_iter.next() // a = 1, my_iter = [2, 3]
let c = &my_iter.skip().next() // c = 3, my_iter = []
let d = &my_iter.next() // d = Error (precondition failed), my_iter = []
Conceptually, iterators are treated as if they are just lists. It's just that some lists can be lazily generated... We'll see if I run into problems with this conceptualization later as I implement this. I'll report on my findings later.
Conclusion
I hope this gives you inspiration! I always thought that supporting iterators is a core component of flexible software. There's nothing more satisfying than the idea: "If I can do it once, I can do it a million times." It has never been easier!
For next week's blog, I might try something new and discuss something other than Ribbon. I've had a few ideas, but I'm not sure which one I'll pick to write a thousand words for. Regardless, I hope to see you next week! Take care!