Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Lisp: Слезы радости, часть 6

Оригинал: "Lisp: Tears of Joy, Part 6 "
Автор: Vivek Shangari
Дата публикации: November 1, 2011
Перевод: Н.Ромоданов
Дата публикации перевода: 26 октября 2012 г.
Первую статью серии читайте здесь.
Предыдущую статью серии читайте здесь.

Язык Lisp был оценен, как самый мощный язык программирования в мире. Но только очень небольшой процент самых элитных программистов пользуются им из-за его загадочного синтаксиса и его академической репутации. Это весьма печально, поскольку Lisp не так уж и трудно понять. Если вы хотите быть в числе избранных, то эти статьи для вас. Это шестая статья в серии, которая началось в июне 2011 года.

Замыкания

Предположим, вы хотите написать функцию, сохраняющую некоторые значения, которые можно будет проверить в следующий раз, когда функция будет работать. Например, функции, которые генерируют случайные числа, часто для того, чтобы создать следующее случайное число, используют ранее созданное случайное число. Чтобы это сделать, вам нужно положить в сторону такое значение, которое функция может взять следующий раз.

Это рискованное предложение, если вы для хранения такого значения используете глобальную переменную или список свойств. Если вы будете неаккуратны и напишите другую функцию, которая использует те же глобальную переменную или свойства, эти две функции могут взаимодействовать таким образом, о котором вы даже никогда не предполагали. Будет еще больше проблем, если вы хотите иметь несколько воплощений той же самой функции, причем каждая должна помнить, что она вычислена ранее, но при этом функции не должны мешать друг другу. Использование одного глобального объекта для этого недостаточно.

Например, предположим, что вам нужна функция, которая при каждом следующем к ней обращении будет выдавать вам следующее четное число. Вы могли бы написать ее в виде функции, в которой для запоминания последнего вычисленного значения использовалась бы глобальная переменная *evenum*:

> (defun generate-even()
  (setq *evenum* (+ *evenum* 2)))
GENERATE-EVEN
> (setq *evenum* 0)
0
> (generate-even)
2
> (generate-even)
4
> (generate-even)
6

Это работает прекрасно, но между вызовами к evenum могут обращаться другие вызовы и могут ее менять. И мы не можем одновременно использовать generate-even для генерации двух последовательностей четных чисел, которые будут независимы друг от друга.

Решение этой проблемы состоит в создании версии функции, у которой есть переменные, к которым только она может получить доступ. Это делается с помощью функции, в которой есть несколько свободных символов, и создания новой функции, в которой эти свободные символы являются ее собственными уникальными переменным (свободными символами являются символы, используемые внутри функции для назначения переменных, но которые не являются формальными параметрами этой функции).

Это как если бы мы заменили каждый свободный символ новым символом, относительного которого мы бы были уверены, что никакая из других функций не может его использовать. Когда выполняется эта новая функция, то ее свободные символы будут ссылками на переменные, на которые никакая другая функция не может ссылаться. Если значения некоторых из этих переменных изменяются, то их новые значения будут сохранены до следующего запуска функции.

Тем не менее, изменения в этих переменных никак не повлияют за пределами этой функции; более того, значения этих переменных не будут доступны и их нельзя будет изменить вне этой функции.

В этом примере, нам бы хотелось создать версию generate-even, которая бы имела свою собственную приватную копию свободной переменной *evenum*. Когда мы создаем версию функции generate-even, мы бы хотели, чтобы каждый раз, когда есть текущая версия *evenum*, эта версия переменной *evenum* была бы инициализирована.

Что бы в дальнейшем ни случилось с оригинальной версией, это не повлияет на новую версию. Когда мы запускаем новую версию, она должна обновлять свою приватную копию переменной *evenum*. Это не должно влиять на версию *evenum*, имеющуюся в исходной функции, но новая обновленная копия *evenum* должна быть доступна новой функции следующий раз, когда эта функция будет запущена.

Другими словами, мы делает своего рода фотографию функции с текущим состоянием ее свободных символов. Затем мы работает с этим снимком, а не с самой функцией. Снимок имеет примерно такую же логическую структуру, что и оригинал, но если мы что-нибудь изменим в снимке, оригинал не изменится. На самом деле, мы должны быть в состоянии делать любое количество таких снимков и управлять каждым из них немного по-другому. Изменения в каждом снимке нужно будет сохранить как его текущее состояние. Каждый снимок можно рассматривать и изменять независимо от других снимков.

Когда мы делаем такой снимок функции, то он называется замыканием этой функции. Название происходит из того, что переменные, представленные свободными символами этой функции, обычно «открытые» для внешнего мира, теперь для него закрываются.

В Common Lisp замыкание функции создается путем предоставления этой функции специальной функции с лямбда-выражением в качестве аргумента. Если случится, что свободные символы этого лямбда-выражения должны быть параметрами функции, в которую добавлено лямбда-выражение, функция возвратить снимок функции, поименованный лямбда-выражением. Этот снимок будет содержать свои собственные переменные, соответствующие каждому свободному символу в лямбда-выражении.

Например, мы можем использовать функцию записи версии generate-even, которая создаст замыкание, содержащим свободный символ *evenum*:

> (defun generate-even (*evenum*)
      (function
          (lambda()
              (setq *evenum* (+ *evenum* 2)))))
GENERATE-EVEN
> (setq gen-even-1 (generate-even 0))
#<FUNCTION :LAMBDA NIL (SETQ *EVENUM* (+ *EVENUM* 2))>
> (funcall gen-even-1)
2
> (funcall gen-even-1)
4
> (funcall gen-even-1)
6

Когда вызывается generate-even, Lisp создает новую переменную, соответствующую *evenum*, поскольку Lisp всегда создает новые переменные, соответствующие формальным параметрам функции. Затем функция function вернет замыкание указанного лямбда-выражения. Поскольку в этот момент существует новая переменная, соответствующая *evenum*, замыкание получает эту версию *evenum* как свою собственную.

Когда мы выходим из этого вызова в generate-even, код не может ссылаться на эту переменную, которая замкнута для внешнего мира. Мы сохраняем это замыкание, назначив его переменной gen-even-1.

Далее давайте используем funcall для того, чтобы вызвать эту функцию (funcall похожа на apply, но ожидает, что справа после имени функции будут идти аргументы; в данном случае их нет, поскольку лямбда-выражение generate-even и, следовательно, замыкание, созданное из него, является функцией без аргументов).

Lisp выводит замыкание как #>FUNCTION :LAMBDA NIL (SETQ *EVENUM* (+ *EVENUM* 2))<.

Учтите, что нотация, используется для печати замыкания, не является частью стандарта Common Lisp. Это из-за того, что в действительности нет смысла печатать замыкание. Поэтому в других реализациях Common Lisp может использоваться другая нотация.

Мы запускаем это замыкание пару раз, и каждый раз оно создаст новое значение. Мы можем создать столько независимых замыканий, сколько мы захотим. Например, если мы прямо сейчас делаем еще одно замыкание generate-even ...

> (setq gen-even-2 (generate-even 0))
#<FUNCTION :LAMBDA NIL (SETQ *EVENUM* (+ *EVENUM* 2))>
> (funcall gen-even-2)
2
> (funcall gen-even-2)
4
> (funcall gen-even-1)
8
> (funcall gen-even-1)
10
> (funcall gen-even-2)
6

Это замыкание начинается с собственной версии *evenum* со значением 0. Каждое замыкание имеет свою собственную независимую переменную, соответствующую символу *evenum*. Поэтому вызов одной функции не влияет на значение переменной *evenum* в другой функции.

Замыкаем эту функцию!

Когда мы закрыть свободные символы функций, появляется новая проблема. Закрытые переменные недоступны за пределами закрытия, так что трудно было бы написать набор функций, которые одними и теми же замкнутый переменный.

Например, предположим, мы хотим написать пару функций, где одна функция возвращает следующее четное число, а другая - следующее нечетное число. Но мы хотим, чтобы они работали в тандеме, так чтобы при вызове одной функции вызывалась и другая функция. Например, если мы вызываем три раза подряд генератор четных чисел, то он должен вернуть 2, 4 и 6. Затем вызов генератора нечетных чисел должен вернуть 7. Если мы вызываем его снова, он должен вернуть 9. В следующий раз, когда мы вызываем генератор четных чисел, он должен вернуть 10.

Написать одну пару таких функций достаточно просто. Например, мы могли бы сделать следующее:

> (defun generate-even()
      (setq *seed* (cond ((evenp *seed*) (+ *seed* 2))
          (t (1+ *seed*)))))
GENERATE-EVEN
 
> (defun generate-odd()
      (setq *seed* (cond ((oddp *seed*) (+ *seed* 2))
          (t (1+ *seed*)))))
GENERATE-ODD
 
> (setq *seed* 0)
0
> (generate-even)
2
> (generate-odd)
3
> (generate-even)
4
> (generate-even)
6
> (generate-odd)
7

Однако если мы хотим создать замыкание этих функций, то у нас будут проблемы. Если мы используем замыкание для создания замыкания каждой функции, то в каждом замыкании будет своя собственная версия переменной *seed*. Замыкание для generate-even не может влиять на замыкание для generate-odd, и также верно обратное.

Но это не то, чего мы хотим. Решение этой проблемы состоит в создании в одном и том же контексте замыкания для нескольких функций. Функции замкнутые совместно могли бы обмениваться своими переменными друг с другом, но не с кем-либо еще. Например, ниже приведена функция, которая создает список из двух замыканий, одно из которых генерирует четные числа, а другое - нечетное:

> (defun generate-even-odd (*seed*)
    (list
        (function
            (lambda()
                (setq *seed* (cond ((evenp *seed*) (+ *seed* 2))
                    (t (1+ *seed*))))))
 
         (function
             (lambda()
                 (setq *seed* (cond ((oddp *seed*) (+ *seed* 2))
                     (t (1+ *seed*))))))))
GENERATE-EVEN-ODD
> (setq fns (generate-even-odd 0))
(#FUNCTION :LAMBDA NIL (SETQ *SEED* (COND ((EVENP *SEED*) (+ *SEED* 2)) (T (1+ *SEED*))))>
(#FUNCTION :LAMBDA NIL (SETQ *SEED* (COND ((ODDP *SEED*) (+ *SEED* 2)) (T (1+ *SEED*))))>)
> (funcall (car fns))
2
> (funcall (car fns))
4
> (funcall (cadr fns))
5
> (funcall (cadr fns))
7
> (funcall (car fns))
8

Некоторые читатели, которые не пользуются языком Lisp активно, возможно, забыли то, что мы рассматривали ранее в этой серии статей, поэтому далее приведено краткое объяснение ключевых слов, используемых в коде выше.

  • (cond ((test expression*)*)) - оценивает проверяемые выражения test expression до тех пор, пока одно из них не вернет истину. Если таких выражений нет, возвращается значение, полученное при проверке. В противном случае, последовательно оценивает выражения и возвращает значение последнего. Если ни одна из проверок не возвращает истину, то возвращается nil.
  • car — Примитивными функциями, извлекающими элементы из списка, являются car и cdr. car извлекает первый элемент списка, а cdr — все элементы, оставшиеся после извлечения первого элемента из списка:
    > (car `(a b c))
    A
    > (cdr `(a b c))
    (B C)
    
  • caddr — в Common Lisp определены функции, такие как caddr, которые являются сокращением от "car от cdr от cdr". Все функции вида c_x_r, где _x_ представляет собой строку, содержащую до четырех символов a или d, определены в Common Lisp. За исключением, возможно, функции cadr, которая обращается ко второму элементу, не стоит их использовать их в коде, который еще кто-нибудь будет читать.
  • (evenp i) - Возвращает истину, если i четное; (oddp i) - возвращает истину, если i нечетное.

Обращение к generate-even-odd создает пару замыканий, по одному для каждого вызова функции. Эта пара имеет разделяемый доступ к приватной копии переменной *seed*. Последующие вызовы generate-even-odd будут создавать дополнительные пары таких замыканий, причем каждая пара обеспечит разделяемый доступ ко всем своим переменным.

Продолжение следует...