Шаг 37.
Простая рекурсия. Рекурсия по значению

    На этом шаге мы рассмотрим примеры использования рекурсии по значению.

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

   


Пример 1. Предикат MEMBER1 (A L) возвращает ту часть списка L, в которой искомый атом A является первым элементом.
   (DEFUN MEMBER1 (LAMBDA (A L)
      (COND ( (NULL L) NIL )
            ( (EQ (CAR L) A) L )
            (   T   (MEMBER1 A (CDR L)))
      )
   ))
Текст этой функции можно взять здесь.

   


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

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

    Тогда для функции REVERSE мы получим такое определение:

   (DEFUN REVERSE (LAMBDA (L)
      (TRANCE L NIL)
   ))
   ; ---------------------------- 
   (DEFUN TRANCE (LAMBDA (L Result)
      (COND ( (NULL L) Result )
            (   T  (TRANCE (CDR L) (CONS (CAR L) Result)) )
      )
   ))
Текст этой функции можно взять здесь.

    Вспомогательная функция TRANCE рекурсивна по значению. С помощью этой функции элементы переносятся таким образом, что на каждом шаге рекурсии очередной элемент переходит из аргумента L в аргумент Result.

   


Пример 3. Очень часто критика языка LISP состоит в том, что поиск k-го элемента списка является очень длительным процессом. Приведем функцию, которая воспринимает в качестве аргументов целое положительное число I и список LST, и возвращает элемент списка, имеющий номер I. Предполагается, что список LST имеет длину не меньшую I.
   (DEFUN  NTH (LAMBDA (I LST)
      (COND ( (EQ I 1) (CAR LST) )
            (  T  (NTH (- I 1) (CDR LST)) )
      )
   ))
Текст этой функции можно взять здесь.

   


Пример 4. Определение предиката, аналогичного предикату MEMBER, для многоуровневых списков.
   (DEFUN MEMBER2 (LAMBDA (X LST)
      (COND ( (NULL LST) NIL)
            (  T  (OR (COND ( (ATOM (CAR L))
                                  (EQ X (CAR L)) )
                            (  T  (MEMBER2 X (CAR L)))
                      )
                      (MEMBER2 X (CDR L))
                  )
            )
      )
   ))
Текст этой функции можно взять здесь.

   


Пример 5. Построение списка, конгруэнтного списку LST и содержащего "глубины погружения" элементов в список LST. Например, если LST = ((1 (2) 3) 4), то функция возвращает список вида ((2 (3) 2) 1).
   (DEFUN COPY (LAMBDA (LST M)
      (COND ( (NULL LST) NIL )
            ( (ATOM LST) (POISK LST M) )
            (  T  (CONS (COPY (CAR LST) M)
                        (COPY (CDR LST) M)) )
      )
   ))
   ; ------------------------ 
   (DEFUN POISK (LAMBDA (X LST)
   ;  Нахождение "глубины погружения" X в список LST  
      (COND ( (MEMBER X LST) 1 )
            ( (MEMBER2 X (CAR LST))
                  (+ 1 (POISK X (CAR LST))) )
            (  T  (POISK X (CDR LST)) )
      )
   ))
   ; -------------------------- 
   (DEFUN MEMBER2 (LAMBDA (X LST)
   ;  Предикат MEMBER2 устанавливает вхождение 
   ;  элемента X в многоуровневый список LST   
      (COND ( (NULL LST) NIL)
            (  T  (OR (COND ( (ATOM (CAR LST))
                                  (EQ X (CAR LST)) )
                            (  T  (MEMBER2 X (CAR LST)) )
                      )
                      (MEMBER2 X (CDR LST))) )
      )
   ))
Текст этой функции можно взять здесь.

   


Пример 6. Определение функционального предиката (ALL P L), который истинен в том и только в том случае, когда являющийся функциональным аргументом предикат P истинен для всех элементов списка L.

    Определение функционального предиката (Exist P L), который истинен, когда предикат P истинен хотя бы для одного элемента списка L.

   (DEFUN ALL (LAMBDA (P L)
      (COND ( (NULL L) T )
            ( (P (CAR L)) (ALL P (CDR L)))
            ( T NIL )
      )
   ))
   ; ---------------------- 
   (DEFUN EXIST (LAMBDA (P L)
      (COND ( (NULL L) NIL )
            ( (P (CAR L)) T )
            ( T  (EXIST P (CDR L)) )
      )
   ))
Текст этой функции можно взять здесь.

   


Пример 7. Известный американский ученый Алонсо Черч, автор лямбда-нотации и тезиса Черча, создал на подступах к теории алгоритмов лямбда-исчисление, которое сейчас можно считать не чем иным, как теоретической моделью современного функционального программирования. Он бился немало месяцев над тем, чтобы запрограммировать в лямбда-исчислении операцию вычитания единицы из натурального числа:

             0 - 1    --> 0
             (n+1) - 1 --> n

    Черч так и не справился с этой задачей и уже уверился в неполноте своего исчисления, но в 1932 г. Стефан Клини, тогда молодой аспирант, предложил следующую, на первый взгляд искусственную, но на самом деле полностью отвечающую сути дела конструкцию [1, с.6]:


   F (x,y,z) = если  x=0
                 то  0
                 иначе  если  y+1=x
                          то  z
                          иначе  F(x,y+1,z+1)
   Тогда  n-1 = F (n,0,0).

    Приведем функцию, решающую данную проблему:

   (DEFUN DIFUNIT (LAMBDA (X Y Z)
      (COND ( (EQ X 0) 0 )
            ( (EQ (+ Y 1) X) Z )
            (  T  (DIFUNIT X (+ Y 1) (+ Z 1)) )
      )
   ))
Текст этой функции можно взять здесь.

   


Пример 8. Моделирование сложения целых чисел с помощью тождества: (a + 1) + (b - 1) = a + b.
   (DEFUN PLUS1 (LAMBDA (A B)
      (COND ( (EQ B 0) A )
            ( (PLUSP B) (PLUS1 (+ A 1) (- B 1)) )
            (     T     (PLUS1 (- A 1) (+ B 1)) )
      )
   ))
   ; ---------------------- 
   (DEFUN PLUS2 (LAMBDA (A B)
   ; Моделирование сложения натуральных чисел 
      (COND ( (EQ B 0) A )
            ( (PLUSP B) (PLUS2 (+ A 1) (- B 1)) )
            (    T    NIL  )
      )
   ))
Текст этой функции можно взять здесь.

   


Пример 9. Функция, которая возвращает первый атом списка.
   (DEFUN FIRSTATOM (LAMBDA (L)
      (COND ( (ATOM L) L )
            ( T  (FIRSTATOM (CAR L)) )
      )
   ))
Текст этой функции можно взять здесь.

   


Пример 10. Удаление N первых элементов одноуровневого списка LST.
   (DEFUN DELETEN (LAMBDA (N LST)
      (COND ( (EQ N 0) LST )
            (  T  (DELETEN (- N 1) (CDR LST)) )
      )
   ))
Текст этой функции можно взять здесь.

   


Пример 11. Функция LAST, возвращающая последний элемент списка.
   (DEFUN LAST (LAMBDA (L)
      (COND ( (NULL L) NIL )
            ( (NULL (CDR L)) (CAR L) )
            (  T  (LAST (CDR L)) )
      )
   ))
Текст этой функции можно взять здесь.

   


Пример 12. Предикат, который проверяет, является ли его аргумент списком (возможно, пустым), составленным лишь из атомов.
   (DEFUN ATOMLIST (LAMBDA (X)
      (COND ( (NULL X) T )
            ( (ATOM X) NIL )
            ( (ATOM (CAR X)) (ATOMLIST (CDR X)) )
            (  T  NIL )
      )
   ))
Текст этой функции можно взять здесь.

   


Пример 13. Функция APPEND, объединяющая два списка в один список, причем:


    Замечание. Как видно, APPEND копирует список, являющийся первым аргументом. Эту функцию часто так и используют в виде (APPEND LST NIL), когда необходимо сделать копию верхнего уровня списка.

    Заметим, что если список X очень длинный, то вычисления будут долгими. Создание списочных ячеек с помощью функции COND требует времени и в будущем добавляет работы "мусорщику". Если, например, список X содержит 1000 элементов, а список Y - один элемент, то во время вычисления будет создано 1000 новых ячеек, хотя вопрос состоит лишь в добавлении одного элемента к списку. Если бы последовательность аргументов была бы другой, то создалась бы одна ячейка, и списки были бы объединены приблизительно в 1000 раз быстрее.

    Если для нас несущественно, что значение переменной X изменится, то мы можем вместо функции APPEND использовать более "быструю" функцию NCONC. Функция NCONC делает то же самое, что и функция APPEND, с той лишь разницей, что она просто объединяет списки, изменяя указатель в поле CDR последней ячейки списка, являющегося первым аргументом, на начало списка, являющегося вторым аргументом. Использовав функцию NCONC, мы избежим копирование списка X, однако в качестве побочного эффекта изменилось значение переменной X. Так же изменятся и все другие структуры, использовавшие ячейки первоначального значения переменной X.





(1)Хендерсон П. Функциональное программирование: Применение и реализация. - М.: Мир, 1983. - 349 с.


    На следующем шаге мы рассмотрим рекурсию по значению и по аргументам.




Предыдущий шаг Содержание Следующий шаг