Because lazy lists don’t compute their tails until needed, you can even throw an exception in the tail of the list without causing an error; for example, if *>> is the lazy list cons operator:
val myInts = 3 *>> (throw new Exception("END OF LIST!") *>> LazyNil)
then this code won’t throw an exception!
(Until the program looks at the tail of myInts.)
Under the hood, a lazy list is either the empty lazy list, or it’s a lazy cons cell.
A lazy cons cell has a value for the head of the lazy list, but either a computation or a cached value for the tail of the list.
The first time the tail is accessed, the stored computation is executed to produce the tail of the list, and then cached. Each subsequent access to the tail gets the cached value.
In a compiler, the lexical analysis phase converts a sequence of characters into a sequence of tokens. Lazy lists are an excellent data structure for representing both of these sequences.
A bad option for encoding the input sequence of characters would be to use a C-like getchar() call. Occasionally, lexers need to do back-tracking, and the ANSI C standard guarantees only one character can be un-getchar'd.
The parser may also want to do look-ahead peeking and backtracking, and lazy lists of tokens provide a convenient way to do this as well.
Reading the entire program into an array or an ordinary list isn’t a great option either, since it forces the entire input program to be held in memory all at once. (This is less of a concern for modern machines.)
Scala happens to be a great language for implementing lazy lists in a natural way, thanks to its support for by-name parameters, lazy fields, custom operators and first-class view patterns.
The demonstrative implementation of lazy lists (below) shows off these features.
Scala also supports streams in the Scala API, but without the syntactic sugar.
Code
Lazy list implementation: LazyList.scala
Exercise
Implement a CharLazyList extension to LazyList class, which has three extra fields: position, line, column.
Have CharLazyList update these fields automatically.
Related articles
- 26 languages in 25 days: Reflections on language design
- 26 languages in 25 days: Strategy, tactics, logistics
- Understanding and implementing laziness
- Parsing S-Expressions in Scala
- A pipelined, non-blocking, extensible web server in Scala
- Architectures for interpreters
- Learning Scala in small bites
- An interpreter for Lambdo
- Okasaki red-black tree maps in Scala
- Parsing M-Expressions in Scala with combinators
- Advanced programming languages
- Church encodings and the Y Combinator in Python
- Introducing QuickCheck: Number theory and red-black trees
- Higher-order list operations
- Implementing Java as a CESK machine, in Java
- Writing an interpreter, CESK-style
- Writing CEK-style interpreters in Haskell
- Compiling up to the λ-calculus
- Parsing with derivatives (Yacc is dead: An update)
- Deleting from Okasaki's red-black trees
- By example: Continuation-passing style in JavaScript
- Self-inlining anonymous closures in C++
- 7 lines of code, 3 minutes: Implement a programming language
- Self-inlining anonymous functions in C++
- Lambda-style anonymous functions in C++
- Lambda-calculus in C++ templates
- Church encodings in Scheme
- Non-termination without loops, iteration or recursion in Javascript
- Memoizing recursive functions in Javascript with the Y combinator
Twitter: @mattmight
Instagram: @mattmight
LinkedIn: matthewmight
Mastodon: @mattmight@mathstodon.xyz
Sub-reddit: /r/mattmight