Is there a simple lisp equivalent of Python generators?

In Python you can write this:

def firstn(n): num = 0 while num < n: yield num num += 1 

What is the lisp equivalent of this?

+8
source share
1 answer

Existing package

Download, install and download the GENERATORS system from Quicklisp . Then use package :generators (or, preferably, define your own package first).

 (ql:quickload :generators) (use-package :generators) 

Define an infinite generator for random values:

 (defun dice (n) (make-generator () ;; repeatedly return a random value between 1 and N (loop (yield (1+ (random n)))))) 

Use a generator:

 (loop with dice = (dice 6) repeat 20 collect (next dice)) => (1 2 6 1 1 4 4 2 4 3 6 2 1 5 6 5 1 5 1 2) 

However, pay attention to what the author of the library says:

This library is a more interesting toy, although, as far as I know, it works. I do not think I have ever used this in application code, although I think with caution, this may be.

see also

  • The ITERATE package provides a way to define generators for use within its iteration facility.

  • The SERIES package provides streaming data structures and operations with them.

  • Snake Library (as far as I know, the same approach as for GENERATORS ).

Shutters

In practice, the CL is not so much dependent on the generators that are popularized by Python. Instead, what happens is that when people need lazy sequences, they rely on closures:

 (defun dice (n) (lambda () (1+ (random n)))) 

Then the next equivalent would be just a thunk call generated by dice :

 (loop with dice = (dice 6) repeat 20 collect (funcall dice)) 

This approach is preferable, in particular, because there is no need to rely on continuations with delimiters, as with generators. Your example includes a state that is not required in the cubes example (there is a hidden state that affects random , but that's a different story). This is how your counter is usually implemented:

 (defun first-n (n) (let ((counter -1)) (lambda () (when (< counter n) (incf counter))))) 

Higher Order Functions

In addition, you are developing a generator that accepts a callback function that is called by your generator for each value. Any funcallable can be used that allows the caller to retain control over code execution:

 (defun repeatedly-throw-dice (n callback) (loop (funcall callback (1+ (random n))))) 

Then you can use it as follows:

 (prog ((counter 0) stack) (repeatedly-throw-dice 6 (lambda (value) (if (<= (incf counter) 20) (push value stack) (return (nreverse stack)))))) 

See the documentation for PROG .

do-traversal idiom do-traversal

Instead of creating a function, data sources that provide a custom way to generate values ​​(for example, matching regular expressions in a string) also regularly provide a macro that abstracts their control flow. You would use it as follows:

 (block 'outer (let ((counter 0) stack) (do-repeatedly-throw-dice (value 6) ;; For each iteration of the infinite loop, ;; VALUE is bound to a new random value. (if (<= (incf counter) 20) (push value stack) (return-from 'outer (nreverse stack)))))) 

The difference with the above is that the block has an exact name. This is because do-X usually defines a NIL block around its body, which means that any enclosing NIL block is shaded. NIL block allows you to easily exit the iteration:

  (let ((counter 0) stack) (do-repeatedly-throw-dice (value 6) (if (<= (incf counter) 20) (push value stack) (return (nreverse stack)))))) 

A possible implementation for a macro is to wrap the body in a lambda form and use the version based on the callback defined above:

 (defmacro do-repeatedly-throw-dice ((var n) &body body) '(block nil (repeatedly-throw-dice ,n (lambda (,var) ,@body)))) 

Direct expansion into the cycle is also possible:

 (defmacro do-repeatedly-throw-dice ((var n) &body body) (let ((max (gensym)) (label (make-symbol "NEXT"))) '(prog ((,max ,n) ,var) ,label (setf ,var (1+ (random ,max))) (progn ,@body) (go ,label)))) 

One step macrodecomposition for the above form:

 (BLOCK 'OUTER (LET ((COUNTER 0) STACK) (PROG ((#:G16053 6) VALUE) #:NEXT (SETF VALUE (1+ (RANDOM #:G16053))) (PROGN (IF (<= (INCF COUNTER) 20) (PUSH VALUE STACK) (RETURN-FROM 'OUTER (NREVERSE STACK)))) (GO #:NEXT)))) 

Handcuffs

Generally speaking, creating a generator with higher-order functions or directly with do- gives the same result. You can implement one with the other (personally, I prefer to define a macro first and then a function using a macro, but doing the opposite is also interesting, since you can redefine a function without recompiling all uses of the macro).

However, there is still a difference: a macro reuses the same variable during iterations, while a closure introduces a new binding each time. For instance:

 (let ((list)) (dotimes (i 10) (push (lambda () i) list)) (mapcar #'funcall list)) 

.... returns:

 (10 10 10 10 10 10 10 10 10 10) 

Most (if not all) iterators in Common Lisp usually work this way, and this should not be surprising for experienced users (in fact, on the contrary, it would be amazing). If dotimes were implemented by repeatedly calling a closure, the result would be different:

 (defmacro my-dotimes ((var count-form &optional result-form) &body body) '(block nil (alexandria:map-iota (lambda (,var) ,@body) ,count-form) ,result-form)) 

With the above definition, we can see that:

 (let ((list)) (my-dotimes (i 10) (push (lambda () i) list)) (mapcar #'funcall list)) 

... returns:

 (9 8 7 6 5 4 3 2 1 0) 

To get the same result with standard dotimes , you only need to evaluate the iteration variable before constructing the closure:

 (let ((list)) (dotimes (i 10) (let ((ji)) (push (lambda () j) list)))) 

If you want, you can always enter this internal let from the macro, but this is rarely done.

+22
source

Source: https://habr.com/ru/post/1232997/


All Articles