Implementing an infinite list of consecutive integers in Lisp for lazy evaluation
Asked Answered
C

3

5

Prelude

In Raku there's a notion called infinite list AKA lazy list which is defined and used like:

my @inf = (1,2,3 ... Inf);
for @inf { say $_;
           exit if $_ == 7 }
# => OUTPUT
1
2
3
4
5
6
7

I'd like to implement this sort of thing in Common Lisp, specifically an infinite list of consecutive integers like:

(defun inf (n)
  ("the implementation"))

such that

(inf 5)
=> (5 6 7 8 9 10 .... infinity)
;; hypothetical output just for the demo purposes. It won't be used in reality

Then I'll use it for lazy evaluation like this:

(defun try () ;; catch and dolist 
  (catch 'foo ;; are just for demo purposes
    (dolist (n (inf 1) 'done)
      (format t "~A~%" n)
      (when (= n 7)
    (throw 'foo x)))))

CL-USER> (try)
1
2
3
4
5
6
7
; Evaluation aborted.

How can I implement such an infinite list in CL in the most practical way?

Clarinda answered 21/1, 2021 at 16:26 Comment(11)
https://mcmap.net/q/793700/-is-there-a-straightforward-lisp-equivalent-of-python-39-s-generators/124319Wages
The try function is just for demo. It iterates on the infinite list and stops when a certain condition is met.Clarinda
@Wages the link, however useful ideas in Common Lisp it may contain, doesn't address the exact issue of this topic. Besides it assumes Python knowledge which I don't have, for instance what is yield ?Clarinda
No it's not any kind of homework. It's an exercise to emulate the infinite lists I've come across in Raku basically. But I refrained from using or giving a code sample in Raku to keep the question as plain as possible.Clarinda
Your example use case would require more than the ability to implement lazy lists; it would also require dolist to be aware of lazy lists.Digress
@adabsurdum I've just edited the question, to clarify the use of dolistClarinda
This library defines lazy an composable iterators: github.com/cbeo/gtwiwtgConcuss
here's an answer by me showing several ways how to do it, even though in Scheme; it should be easy enough to transform to Common Lisp.Restivo
While the question is phrased for clarity, there is a more natural way to express in raku, namely: for 1..Inf { .say; when 7 {exit} }Bilbrey
@p6steve It's the use of when which makes it look more natural the most I think.Clarinda
yeah - definitely! I like to doodle with SO raku code when I don't understand and this made me dig in and learn a bit more about how cool when can be ... as a target for the for topicalizer which is a cool raku conceptBilbrey
W
4

For practical purposes it would be wise to use existing libraries, but since the question is about how to implemented lazy lists, we will do it from scratch.

Closures

Lazy iteration is a matter of producing an object that can generate the new value of a lazy sequence each time it is asked to do so. A simple approach for this is to return a closure, i.e. a function that closes over variables, which produces values while updating its state by side-effect.

If you evaluate:

(let ((a 0))
  (lambda () (incf a)))

You obtain a function object that has a local state, namely here the variable named a. This is a lexical binding to a location that is exclusive to this function, if you evaluate a second time the same expression, you'll obtain a different anonymous function that has its own local state.

When you call the closure, the value stored in a in incremented and its value is returned.

Let's bind this closure to a variable named counter, call it multiple times and store the successive results in a list:

(let ((counter (let ((a 0))
                 (lambda () (incf a)))))
  (list (funcall counter)
        (funcall counter)
        (funcall counter)
        (funcall counter)))

The resulting list is:

(1 2 3 4)

Simple iterator

In your case, you want to have an iterator that starts counting from 5 when writing:

(inf 5)

This can implemented as follows:

(defun inf (n)
  (lambda ()
    (shiftf n (1+ n))))

Here is there is no need to add a let, the lexical binding of an argument to n is done when calling the function. We assign n to a different value within the body over time. More precisely, SHIFTF assigns n to (1+ n), but returns the previous value of n. For example:

(let ((it (inf 5)))
  (list (funcall it)
        (funcall it)
        (funcall it)
        (funcall it)))

Which gives:

(5 6 7 8)

Generic iterator

The standard dolist expects a proper list as an input, there is no way you can put another kind of data and expect it to work (or maybe in an implementation-specific way). We need a similar macro to iterate over all the values in an arbitrary iterator. We also need to specify when iteration stops. There are multiple possibilities here, let's define a basic iteration protocol as follows:

  1. we can call make-iterator on any object, along with arbitrary arguments, to obtain an iterator
  2. we can call next on an iterator to obtain the next value.
  3. More precisely, if there is a value, next returns the value and T as a secondary value; otherwise, next returns NIL.

Let's define two generic functions:

(defgeneric make-iterator (object &key)
  (:documentation "create an iterator for OBJECT and arguments ARGS"))

(defgeneric next (iterator)
  (:documentation "returns the next value and T as a secondary value, or NIL"))

Using generic functions allows the user to define custom iterators, as long as they respect the specified behaviour above.

Instead of using dolist, which only works with eager sequences, we define our own macro: for. It hides calls to make-iterator and next from the user. In other words, for takes an object and iterates over it. We can skip iteration with (return v) since for is implemented with loop.

(defmacro for ((value object &rest args) &body body)
  (let ((it (gensym)) (exists (gensym)))
    `(let ((,it  (make-iterator ,object ,@args)))
       (loop
         (multiple-value-bind (,value ,exists) (next ,it)
           (unless ,exists
             (return))
           ,@body)))))

We assume any function object can act as an iterator, so we specialize next for values f of class function, so that the function f gets called:

(defmethod next ((f function))
  "A closure is an interator"
  (funcall f))

Also, we can also specialize make-iterator to make closures their own iterators (I see no other good default behaviour to provide for closures):

(defmethod make-iterator ((function function) &key)
  function)

Vector iterator

For example, we can built an iterator for vectors as follows. We specialize make-iterator for values (here named vec) of class vector. The returned iterator is a closure, so we will be able to call next on it. The method accepts a :start argument defaulting to zero:

(defmethod make-iterator ((vec vector) &key (start 0))
  "Vector iterator"
  (let ((index start))
    (lambda ()
      (when (array-in-bounds-p vec index)
        (values (aref vec (shiftf index (1+ index))) t)))))

You can now write:

(for (v "abcdefg" :start 2)
  (print v))

And this prints the following characters:

#\c 
#\d 
#\e 
#\f 
#\g

List iterator

Likewise, we can build a list iterator. Here to demonstrate other kind of iterators, let's have a custom cursor type.

(defstruct list-cursor head)

The cursor is an object which keeps a reference to the current cons-cell in the list being visited, or NIL.

(defmethod make-iterator ((list list) &key)
  "List iterator"
  (make-list-cursor :head list))

And we define next as follows, specializeing on list-cursor:

(defmethod next ((cursor list-cursor))
  (when (list-cursor-head cursor)
    (values (pop (list-cursor-head cursor)) t)))

Ranges

Common Lisp also allows methods to be specialized with EQL specializers, which means the object we give to for might be a specific keyword, for example :range.

(defmethod make-iterator ((_ (eql :range)) &key (from 0) (to :infinity) (by 1))
  (check-type from number)
  (check-type to (or number (eql :infinity)))
  (check-type by number)
  (let ((counter from))
    (case to
      (:infinity
       (lambda () (values (incf counter by) t)))
      (t
       (lambda ()
         (when (< counter to)
           (values (incf counter by) T)))))))

A possible call for make-iterator would be:

(make-iterator :range :from 0 :to 10 :by 2)

This also returns a closure. Here, for example, you would iterate over a range as follows:

(for (v :range :from 0 :to 10 :by 2)
  (print v))

The above expands as:

(let ((#:g1463 (make-iterator :range :from 0 :to 10 :by 2)))
  (loop
   (multiple-value-bind (v #:g1464)
       (next #:g1463)
     (unless #:g1464 (return))
     (print v))))

Finally, if we add small modification to inf (adding secondary value):

(defun inf (n)
  (lambda ()
    (values (shiftf n (1+ n)) T)))

We can write:

(for (v (inf 5))
  (print v)
  (when (= v 7)
    (return)))

Which prints:

5 
6 
7
Wages answered 22/1, 2021 at 11:12 Comment(0)
R
5

A good pedagogical approach to this is to define things which are sometimes called 'streams'. The single best introduction to doing this that I know of is in Structure and Interpretation of Computer Programs. Streams are introduced in section 3.5, but don't just read that: read the book, seriously: it is a book everyone interested in programming should read.

SICP uses Scheme, and this sort of thing is more natural in Scheme. But it can be done in CL reasonably easily. What I've written below is rather 'Schemy' CL: in particular I just assume tail calls are optimised. That's not a safe assumption in CL, but it's good enough to see how you can build these concepts into a language which does not already have them, if your language is competent.

First of all we need a construct which supports lazy evaluation: we need to be able to 'delay' something to create a 'promise' which will be evaluated only when it needs to be. Well, what functions do is evaluate their body only when they are asked to, so we'll use them:

(defmacro delay (form)
  (let ((stashn (make-symbol "STASH"))
        (forcedn (make-symbol "FORCED")))
    `(let ((,stashn nil)
           (,forcedn nil))
       (lambda ()
         (if ,forcedn
             ,stashn
           (setf ,forcedn t
                 ,stashn ,form))))))

(defun force (thing)
  (funcall thing))

delay is mildly fiddly, it wants to make sure that a promise is forced only once, and it also wants to make sure that the form being delayed doesn't get infected by the state it uses to do that. You can trace the expansion of delay to see what it makes:

(delay (print 1))
 -> (let ((#:stash nil) (#:forced nil))
      (lambda ()
        (if #:forced #:stash (setf #:forced t #:stash (print 1)))))

This is fine.

So now, we'll invent streams: streams are like conses (they are conses!) but their cdrs are delayed:

(defmacro cons-stream (car cdr)
  `(cons ,car (delay ,cdr)))

(defun stream-car (s)
  (car s))

(defun stream-cdr (s)
  (force (cdr s)))

OK, let's write a function to get the nth element of a stream:

(defun stream-nth (n s)
  (cond ((null s)
         nil)
        ((= n 0) (stream-car s))
        (t
         (stream-nth (1- n) (stream-cdr s)))))

And we can test this:

> (stream-nth 2
              (cons-stream 0 (cons-stream 1 (cons-stream 2 nil))))
2

And now we can write a function to enumerate an interval in the naturals, which by default will be an half-infinite interval:

(defun stream-enumerate-interval (low &optional (high nil))
  (if (and high (> low high))
      nil
      (cons-stream
       low
       (stream-enumerate-interval (1+ low) high))))

And now:

> (stream-nth 1000 (stream-enumerate-interval 0))
1000

And so on.

Well, we'd like some kind of macro which lets us traverse a stream: something like dolist, but for streams. Well we can do this by first writing a function which will call a function for each element in the stream (this is not the way I'd do this in production CL code, but it's fine here):

(defun call/stream-elements (f s)
  ;; Call f on the elements of s, returning NIL
  (if (null s)
      nil
    (progn
      (funcall f (stream-car s))
      (call/stream-elements f (stream-cdr s)))))

And now

(defmacro do-stream ((e s &optional (r 'nil)) &body forms)
  `(progn
     (call/stream-elements (lambda (,e)
                             ,@forms)
                           ,s)
     ,r))

And now, for instance

(defun look-for (v s)
  ;; look for an element of S which is EQL to V
  (do-stream (e s (values nil nil))
    (when (eql e v)
      (return-from look-for (values e t)))))

And we can then say

> (look-for 100 (stream-enumerate-interval 0))
100
t

Well, there is a lot more mechanism you need to make streams really useful: you need to be able to combine them, append them and so on. SICP has many of these functions, and they're generally easy to turn into CL, but too long here.

Radiography answered 22/1, 2021 at 11:46 Comment(1)
I have a bounty question which might interest you #59340694Clarinda
W
4

For practical purposes it would be wise to use existing libraries, but since the question is about how to implemented lazy lists, we will do it from scratch.

Closures

Lazy iteration is a matter of producing an object that can generate the new value of a lazy sequence each time it is asked to do so. A simple approach for this is to return a closure, i.e. a function that closes over variables, which produces values while updating its state by side-effect.

If you evaluate:

(let ((a 0))
  (lambda () (incf a)))

You obtain a function object that has a local state, namely here the variable named a. This is a lexical binding to a location that is exclusive to this function, if you evaluate a second time the same expression, you'll obtain a different anonymous function that has its own local state.

When you call the closure, the value stored in a in incremented and its value is returned.

Let's bind this closure to a variable named counter, call it multiple times and store the successive results in a list:

(let ((counter (let ((a 0))
                 (lambda () (incf a)))))
  (list (funcall counter)
        (funcall counter)
        (funcall counter)
        (funcall counter)))

The resulting list is:

(1 2 3 4)

Simple iterator

In your case, you want to have an iterator that starts counting from 5 when writing:

(inf 5)

This can implemented as follows:

(defun inf (n)
  (lambda ()
    (shiftf n (1+ n))))

Here is there is no need to add a let, the lexical binding of an argument to n is done when calling the function. We assign n to a different value within the body over time. More precisely, SHIFTF assigns n to (1+ n), but returns the previous value of n. For example:

(let ((it (inf 5)))
  (list (funcall it)
        (funcall it)
        (funcall it)
        (funcall it)))

Which gives:

(5 6 7 8)

Generic iterator

The standard dolist expects a proper list as an input, there is no way you can put another kind of data and expect it to work (or maybe in an implementation-specific way). We need a similar macro to iterate over all the values in an arbitrary iterator. We also need to specify when iteration stops. There are multiple possibilities here, let's define a basic iteration protocol as follows:

  1. we can call make-iterator on any object, along with arbitrary arguments, to obtain an iterator
  2. we can call next on an iterator to obtain the next value.
  3. More precisely, if there is a value, next returns the value and T as a secondary value; otherwise, next returns NIL.

Let's define two generic functions:

(defgeneric make-iterator (object &key)
  (:documentation "create an iterator for OBJECT and arguments ARGS"))

(defgeneric next (iterator)
  (:documentation "returns the next value and T as a secondary value, or NIL"))

Using generic functions allows the user to define custom iterators, as long as they respect the specified behaviour above.

Instead of using dolist, which only works with eager sequences, we define our own macro: for. It hides calls to make-iterator and next from the user. In other words, for takes an object and iterates over it. We can skip iteration with (return v) since for is implemented with loop.

(defmacro for ((value object &rest args) &body body)
  (let ((it (gensym)) (exists (gensym)))
    `(let ((,it  (make-iterator ,object ,@args)))
       (loop
         (multiple-value-bind (,value ,exists) (next ,it)
           (unless ,exists
             (return))
           ,@body)))))

We assume any function object can act as an iterator, so we specialize next for values f of class function, so that the function f gets called:

(defmethod next ((f function))
  "A closure is an interator"
  (funcall f))

Also, we can also specialize make-iterator to make closures their own iterators (I see no other good default behaviour to provide for closures):

(defmethod make-iterator ((function function) &key)
  function)

Vector iterator

For example, we can built an iterator for vectors as follows. We specialize make-iterator for values (here named vec) of class vector. The returned iterator is a closure, so we will be able to call next on it. The method accepts a :start argument defaulting to zero:

(defmethod make-iterator ((vec vector) &key (start 0))
  "Vector iterator"
  (let ((index start))
    (lambda ()
      (when (array-in-bounds-p vec index)
        (values (aref vec (shiftf index (1+ index))) t)))))

You can now write:

(for (v "abcdefg" :start 2)
  (print v))

And this prints the following characters:

#\c 
#\d 
#\e 
#\f 
#\g

List iterator

Likewise, we can build a list iterator. Here to demonstrate other kind of iterators, let's have a custom cursor type.

(defstruct list-cursor head)

The cursor is an object which keeps a reference to the current cons-cell in the list being visited, or NIL.

(defmethod make-iterator ((list list) &key)
  "List iterator"
  (make-list-cursor :head list))

And we define next as follows, specializeing on list-cursor:

(defmethod next ((cursor list-cursor))
  (when (list-cursor-head cursor)
    (values (pop (list-cursor-head cursor)) t)))

Ranges

Common Lisp also allows methods to be specialized with EQL specializers, which means the object we give to for might be a specific keyword, for example :range.

(defmethod make-iterator ((_ (eql :range)) &key (from 0) (to :infinity) (by 1))
  (check-type from number)
  (check-type to (or number (eql :infinity)))
  (check-type by number)
  (let ((counter from))
    (case to
      (:infinity
       (lambda () (values (incf counter by) t)))
      (t
       (lambda ()
         (when (< counter to)
           (values (incf counter by) T)))))))

A possible call for make-iterator would be:

(make-iterator :range :from 0 :to 10 :by 2)

This also returns a closure. Here, for example, you would iterate over a range as follows:

(for (v :range :from 0 :to 10 :by 2)
  (print v))

The above expands as:

(let ((#:g1463 (make-iterator :range :from 0 :to 10 :by 2)))
  (loop
   (multiple-value-bind (v #:g1464)
       (next #:g1463)
     (unless #:g1464 (return))
     (print v))))

Finally, if we add small modification to inf (adding secondary value):

(defun inf (n)
  (lambda ()
    (values (shiftf n (1+ n)) T)))

We can write:

(for (v (inf 5))
  (print v)
  (when (= v 7)
    (return)))

Which prints:

5 
6 
7
Wages answered 22/1, 2021 at 11:12 Comment(0)
C
2

I'll show it with a library:

How to create and consume an infinite list of integers with the GTWIWTG generators library

This library, called "Generators The Way I Want Them Generated", allows to do three things:

  • create generators (iterators)
  • combine them
  • consume them (once).

It is not unsimilar to the nearly-classic Series.

Install the lib with (ql:quickload "gtwiwtg"). I will work in its package: (in-package :gtwiwtg).

Create a generator for an infinite list of integers, start from 0:

GTWIWTG> (range)
#<RANGE-BACKED-GENERATOR! {10042B4D83}>

We can also specify its :from, :to, :by and :inclusive parameters.

Combine this generator with others: not needed here.

Iterate over it and stop:

GTWIWTG> (for x *
           (print x)
           (when (= x 7)
             (return)))

0 
1 
2 
3 
4 
5 
6 
7 
T

This solution is very practical :)

Concuss answered 4/2, 2021 at 10:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.