Lazy - fun project exploring lazy sequences in Erlang

This is a small fun/toy project that we have been entertaining for a while, to emulate lazy sequences as found in other languages.
In a nutshell, instead of iterating over a concrete list, lazy uses generators (built-in or custom write-your-own) that produce values on the fly and only when needed, topped off with a lists-like API.

7 Likes

Nice. A while back I implemented something similar inside fez to cover the api for fsharp "Seq"s:

What I have found though is that I have never missed not having lazy sequences when programming erlang but maybe one doesn’t miss what one doesn’t have :slight_smile:

3 Likes

Gleam has another similar module here: gleam/iterator - gleam_stdlib

Quite fun to see how each implementation differs!

4 Likes

Yeah, I feel the same when it comes to the pipe operator :wink:

So yes, this was just a fun project to see if and how we could pull it off, and as we got in the swing of it, we added function after function, which was a drag to test and document afterwards :blush: But it turned out quite nice IMO and so we decided to show it off here :sweat_smile: It might even be useful to someone.

What surprised me was that both libraries have a lot of the functions in common, often with the same or at least similar names even. Did you model it on and take inspiration from the lists module? At least this is what we did :wink:

Personally, I’m a bit torn on the topic of lazy evaluation, and lazy sequences in particular. They are quite cool, but somewhat less predictable than non-lazy ones, more so if you allow for infinite sequences. You might go chugging along nice and lazy, taking the low memory usage for granted and all, and suddenly through some lapse it may explode into memory all at once, or you end up in an infinite loop as you run down an infinite sequence.

I think Erlang is wise not to have laziness built in into lists like other languages do (or maybe that wasn’t a conscious decision). It’s predictable by default, but it does not block the way for implementations like ours (or Gleams), which you may use (or implement yourself) if you need it and hopefully understand the hidden risks.

3 Likes

I think the documentation could use a few more warnings, for cases we outlined in lazy here. You have some more functions in there that may run into that scenario than we do, namely all, any and find.

3 Likes

It’s based on the Gleam list module, which was designed based off the list modules of Erlang, Elixir, Elm, OCaml, and others.

I pretty much only use it for when a sequence would otherwise be too large to fit in memory, such as if reading a very large file. There are Gleamers who tend to use it over lists though, and languages such as Python and Ruby seem to do well using lazy sequences by default. Not that they’re overly performance focused languages.

A great suggestion, thank you

3 Likes

You’re welcome :blush:

We added most of the missing functions to lazy yesterday, btw.
Ah, the kids, they grow so fast (or so people say) :sweat_smile: But it is just so much fun once you’re into it :smile_cat:

3 Likes

FWIW, we released lazy as a Hex package.

2 Likes

Cool module - would be nice to have something like that built-in. Coming from python I have missed “lazy” lists - what python calls generators, e.g.

[X*X || X<-seq(0,2)]  % Erlang List
L=[x*x for x in range(3)] # Python List
G=(x*x for x in range(3)) # Python Generator

Easy to convert one form to the other in Python. In Erlang the lazy form is different and more verbose - since you have to define it as a recursive function…

1 Like

For what it’s worth, lazy lists and generators are two different things.

1960s: the British AI programming language Pop-2 had lazy lists but not generators. Simula-67 had coroutines, so arguably had generators, but did not have any kind of list.

1970s: InterLISP had generators (via the “spaghetti stack” mechanism) but not lazy lists.
1980s: Icon (university of Arizona, same stable as SNOBOL and SL5) had generators but not lazy lists.

Perhaps the most obvious difference, even in Python, is that a lazy list evaluates next elements on demand and then retains them, while a generator yields one item at a time and then it’s done.

Here’s an example in Python adapted from the Wikipedia page on generators. You’ll find the definition of countfrom there.

squares = [n * n for n in range(5)]

print(“First time”)
for j in squares:
if j <= 20:
print(j)
else:
break

print(“Second time”)
for j in squares:
if j <= 20:
print(j)
else:
break

This prints 0 1 4 9 16 twice.
Change the definition of squares to
squares = (n * n for n in countfrom(0))
and you only get the numbers the first time.
The second time they are gone forever.

The syntactic similarity between generator expressions and list comprehensions in Python can only be described (in Cadogan’s memorable phrase) as “laying the foundations for a building full of fail”. Or to use the phrase I was taught in high school for French words that look like English ones but mean something quite different, generator expressions are “faux amis”.

Now the curious thing is that despite Pop-2 having had lazy lists for decades, when I met it in the 1980s, the only application of lazy lists I ever saw was the compiler, which processed a list of tokens, which was normally a lazy list consuming the current character input sequence. And it wasn’t as if people weren’t aware of the possibilities: this was for AI developed by AI researchers who were sufficiently interested in “exotic” control strategies to implement backtracking (rather like CMS SETL did).

Historically, people lost a lot of interest in coroutines when they realised they could do the same things and more using lightweight processes. Burroughs Algol implemented coroutines (back in the late 60s/early 70s) using the same “cactus stack” model it used for full processes. Algol 68 used concurrency rather than coroutines. Simula 67 used coroutines mainly to fake concurrency. Come to that, the TRIPOS operating system, written in BCPL for BCPL, used BCPL coroutines instead of processes (using the each-coroutine-has-its-own-stack model). Apparently Python implements generator “functions” basically by turning them inside out, and making essential use of mutable objects.

It’s always a delight to read things from you :pray:

A lot of ideas in computer science go back a long time.

Yes, Python generators are not lists - so you can only iterate over them once, and not ask how long they are. I don’t know that that makes them faux amis :slight_smile:

David Beazley had a very good tutorial on them back when they were new.

“faux amis” was for “syntactic similarity” - I think.

Generator expressions in Python are deliberately made to LOOK like list comprehensions, but they don’t ACT like list comprehensions. That definitely makes them “faux amis”.

Generator functions in Python are (and how very Python this is) significantly crippled. Lua does it much better. Lua does so much so well that I regret never being able to like it.