Есть ли простой эквивалент lisp-генераторов Python?
В Python вы можете написать это:
def firstn(n):
num = 0
while num < n:
yield num
num += 1
Какой это эквивалент?
2 ответа
Существующий пакет
Скачайте, установите и загрузите GENERATORS
система с QuickLisp. Затем используйте пакет :generators
(или предпочтительно, сначала определите свой собственный пакет).
(ql:quickload :generators)
(use-package :generators)
Определите бесконечный генератор для случайных значений:
(defun dice (n)
(make-generator ()
;; repeatedly return a random value between 1 and N
(loop (yield (1+ (random n))))))
Используйте генератор:
(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)
Однако обратите внимание на то, что говорит автор библиотеки:
Эта библиотека - более интересная игрушка, хотя, насколько я знаю, она работает. Я не думаю, что когда-либо использовал это в коде приложения, хотя я думаю, что с осторожностью, это может быть.
Смотрите также
ITERATE
Пакет предоставляет способ определить генераторы для использования внутри своего средства итерации.SERIES
Пакет предоставляет потоковые структуры данных и операции над ними.Библиотека Змей (такой же подход, как
GENERATORS
насколько я знаю).
Затворы
На практике генераторы, как правило, не популяризируются Python. Что происходит, если ленивые последовательности нужны, люди полагаются на замыкания:
(defun dice (n)
(lambda ()
(1+ (random n))))
Тогда эквивалент next
это просто вызов Thunk, генерируемый dice
:
(loop
with dice = (dice 6)
repeat 20
collect (funcall dice))
Этот подход предпочтителен, в частности, потому что нет необходимости полагаться на продолжения с разделителями, как с генераторами. Ваш пример включает состояние, которое не требуется для примера игры в кости (ну, есть скрытое состояние, которое влияет random
, но это уже другая история). Вот как обычно реализуется ваш счетчик:
(defun first-n (n)
(let ((counter -1))
(lambda ()
(when (< counter n)
(incf counter)))))
Функции высшего порядка
Кроме того, вы разрабатываете генератор, который принимает функцию обратного вызова, которая вызывается вашим генератором для каждого значения. Может использоваться любой funcallable, который позволяет вызывающей стороне сохранять контроль над выполнением кода:
(defun repeatedly-throw-dice (n callback)
(loop (funcall callback (1+ (random n)))))
Затем вы можете использовать его следующим образом:
(prog ((counter 0) stack)
(repeatedly-throw-dice 6
(lambda (value)
(if (<= (incf counter) 20)
(push value stack)
(return (nreverse stack))))))
Смотрите документацию для PROG
,
do-traversal
идиома
Вместо создания функции источники данных, которые предоставляют пользовательский способ генерирования значений (например, совпадения регулярных выражений в строке), также регулярно предоставляют макрос, который абстрагирует их поток управления. Вы бы использовали его следующим образом:
(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))))))
Разница с вышесказанным заключается в том, что блок имеет точное имя. Это потому что DO-X
обычно можно определить NIL
блок вокруг их тела, что означает, что любой вмещающий NIL
блок затенен. Неявный NIL
Блок позволяет легко выйти из итерации:
(let ((counter 0) stack)
(do-repeatedly-throw-dice (value 6)
(if (<= (incf counter) 20)
(push value stack)
(return (nreverse stack))))))
Возможная реализация макроса - заключить тело в лямбда-форму и использовать версию, основанную на обратном вызове, определенную выше:
(defmacro do-repeatedly-throw-dice ((var n) &body body)
`(block nil (repeatedly-throw-dice ,n (lambda (,var) ,@body))))
Непосредственное расширение в цикл тоже возможно:
(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))))
Один шаг макроразложения для вышеуказанной формы:
(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))))
Наручники
Вообще говоря, создание генератора с функциями более высокого порядка или непосредственно с do-
макрос дает тот же результат. Вы можете реализовать одно с другим.
Тем не менее, есть еще различие: макрос повторно использует одну и ту же переменную в течение итераций, тогда как замыкание вводит новое связывание каждый раз. Например:
(let ((list))
(dotimes (i 10) (push (lambda () i) list))
(mapcar #'funcall list))
.... возвращает:
(10 10 10 10 10 10 10 10 10 10)
Большинство (если не все) итераторы в Common Lisp, как правило, работают так, и это не должно вызывать удивления у опытных пользователей (на самом деле, это было бы неожиданно). Если dotimes
был реализован путем многократного вызова замыкания, результат был бы другим:
(defmacro my-dotimes ((var count-form &optional result-form) &body body)
`(block nil
(alexandria:map-iota (lambda (,var) ,@body) ,count-form)
,result-form))
С приведенным выше определением мы можем видеть, что:
(let ((list))
(my-dotimes (i 10) (push (lambda () i) list))
(mapcar #'funcall list))
... возвращает:
(9 8 7 6 5 4 3 2 1 0)
Чтобы получить тот же результат со стандартом dotimes
Вам нужно только оценить переменную итерации перед построением замыкания:
(let ((list))
(dotimes (i 10)
(let ((j i))
(push (lambda () j) list))))
Если вы хотите, вы всегда можете представить let
из макроса, но это делается редко.
Отличный пост. Но в ответе есть небольшие проблемы
а) в последнем случае нужен mapcar следующим образом:
(let ((list))
(dotimes (i 10)
; new bindings
(let ((j i))
(push (lambda () j) list)))
; need this line
(mapcar #'funcall list))
б) в целом пропустить
(ql:quickload :alexandria)
(ql:quickload :generators)
(defpackage :alex-gen
(:use :alexandria :generators :common-lisp))
(in-package :alex-gen)