Опубликован: 19.03.2004 | Уровень: специалист | Доступ: платный
Лекция 5:

Имена, определения и контексты

< Лекция 4 || Лекция 5: 123 || Лекция 6 >

Функции

Ситуация, когда атом обозначает функцию, реализационно подобна той, в которой атом обозначает аргумент. Если функция рекурсивна, то ей надо дать имя. Теоретически это делается с помощью формы LABEL, которая связывает название с определением функции в ассоциативном списке (а-списке). Название связано с определением функции точно так же, как переменная связана со своим значением. На практике LABEL используется редко. Удобнее связывать название с определением другим способом. Это делается путем размещения определения функции в списке свойств атома ( р-список ), символизирующего ее название. Выполняет данную операцию псевдо-функция DEFUN, описанная в начале этой лекции. Когда APPLY интерпретирует функцию, представленную атомом, она исследует р-список до поиска в а-списке. Таким образом, DEFUN будет опережать LABEL.

Тот факт, что большинство функций — константы, определенные программистом, а не переменные, изменяемые программой, вызван отнюдь не каким-либо недостатком функционального программирования. Напротив, он указывает на потенциал подхода, который мы не научились использовать лучшим образом. (Работы по теории компиляции, оптимизации и верификации программ, смешанным вычислениям, суперпрограммированию и т.п. активно используют средства функционального программирования.)

Системы функционального программирования обеспечивают возможность манипулирования функциональными переменными так же, как и обычными. Но организуется это с помощью ряда специальных функций, осуществляющих переход от символов, изображающих функции, к функциональным объектам, представленным этими символами. Такой переход может быть реализован в рамках любого языка программирования, но на Лиспе он выглядит естественно и поддерживается в любой Лисп-системе. Рассмотрим средства Clisp, обеспечивающие структурирование функциональных объектов.

LABELS — позволяет из списка определений функций формировать контекст, в котором вычисляются выражения.

FLETспециальная функция, позволяющая вводить локальные нерекурсивные функции.

DEFUN — позволяет вводить новые определения на текущем уровне.

(LABELS ( (INTERSECTION (x y)

                 (LET* ( (N-X (NULL x))
                           (MEM-CAR (MEMBER (CAR x) y))
                           (INT #'INTERSECTION) 
                         ) ; конец списка локальных выражений let*

                     (FLET ((F-TAIL (FN sx sy) 
                                      (APPLY FN (LIST (CDR sx) sy)) )
                             (CONS-F-TAIL (FN sx sy)
                                   (CONS (CAR sx) 
                                            (APPLY FN (LIST (CDR sx) sy)) 
                             ))    ) ; конец списка нерекурсивных функций FLET

      (COND (N-X NIL)                                     ; выражение, использующее 
                 (MEM-CAR (cons-f-tail INT x y) )    ; локальные определения функций
                 (T (f-tail INT x y)) )                       ; из контекстов FLET  и
                                                                    ; LABELS 
                     ) ; выход из контекста FLET
                 ) ;  выход из контекста LET*
            )  ; завершено определение INTERSECTION
          )  ; конец списка локальных рекурсивных функций

  (DEFUN UNION (x y)
      (LET ( (a-x (CAR x))
              (d-x (CDR x))
            )
          (COND ((NULL x) y)
                     ((MEMBER a-x y) (UNION d-x y) )
                     (T (CONS a-x (UNION d-x y)) ) 
  )   )    ) ; завершено определение на текущем уровне

  (INTERSECTION '(A1 A2 A3) '(A1 A3 A5))
  (UNION '(X Y Z) '(U V W X))
)  ; выход из контекста LABELS

Функции на машинном языке (низкоуровневые)

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

  1. Подпрограмма закодирована внутри Лисп-системы.
  2. Функция кодируется пользователем вручную на языке типа ассемблера.
  3. Функция сначала определяется с помощью S-выражения, затем транслируется компилятором. Компилированные функции могут выполняться в 2100 раз быстрее, чем интерпретироваться.

Специальные формы

Обычно EVAL вычисляет аргументы функций до применения к ним функций с помощью APPLY. Таким образом, если EVAL задано (CONS X Y), то сначала вычисляются X и Y, а потом над полученными значениями работает CONS. Но если EVAL задано (QUOTE X), то X не будет вычисляться. QUOTE — специальная форма, которая препятствует вычислению своих аргументов.

Специальная форма отличается от других функций двумя свойствами. Ее аргументы не вычисляются, пока она сама не просмотрит свои аргументы. COND, например, имеет свой особый способ вычисления аргументов с использованием EVCON. Второе отличие заключается в том, что специальные формы могут иметь неограниченное число аргументов.

Неподвижная точка и самоприменимость функций

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

Пример (предложен В.А. Потапенко).

Преобразуем определение факториала в самоприменимую безымянную форму.

Для этого нужно:

  • преобразовать исходное определение в самоприменимую форму;
  • избавиться от собственного имени функции.

Традиционное определение факториала:

(DEFUN N! (n)
   (COND
      ((EQ n 0) 1)
      (T (* n (N! (- n 1))))     ; Факториал

   ; Выход из рекурсии
 
; Рекурсивный виток с редукцией аргумента
) )   ; и умножением на результат предыдущего витка

Строим самоприменимое определение факториала:

(DEFUN N!_self (f n)   ; Обобщенная функция,
   ; равносильная факториалу при f = N!_self
   (COND ((EQ n 0)1)
      (T (* n (APPLY f (list f (- n 1)))))
   ; применение функции f
   ; к списку из уменьшенного аргумента
) )

Использовать это определение можно следующим образом:

(N!_self #'N!_self 3)   ; = 6 =

или

(APPLY 'N!_self '(N!_self 4))   ; = 24 =

При таких аргументах оно эквивалентно исходному определению факториала. Теперь избавимся от названия функции:

((LAMBDA (f n )
   ; безымянная функция, равносильная факториалу
      ; при f = N!_self

     (COND ((EQ n 0)1) 
      (T (* n ( f (list f (- n 1)))))
))

Использовать это определение можно в следующей форме:

((LAMBDA (f n )
      (COND ((EQ n 0)1) (T (* n (apply f (list f 
         (- n 1))))) ))   ; функция

   (LAMBDA (f n )
      (COND ((EQ n 0)1) (T (* n (apply f (list f 
         (- n 1))))) ))   ;первый аргумент — f 
   5   ; второй аргумент — n 
)   ; = 120 - результат самоприменения факториала

или

(APPLY

   #'(LAMBDA (f n )
      (COND ((EQ n 0)1)(T (* n (apply f (list f 
         (- n 1))))) ) )   ; функция
   '((LAMBDA (f n )
      (COND ((EQ n 0)1) (T (* n (apply f (list f 
         (- n 1))))) ))
   6 )   ; список аргументов
))   ; = 720

Можно записать этот текст программы (код) без дублирования определения функции:

(LAMBDA (n)
   ( (LAMBDA (f) (APPLY f (list f n))))
      #'(LAMBDA (f n ) (COND ((EQ n 0)1) (T (* n 
         (APPLY f (list f (- n 1)))) ) )
      ; внутренняя функция f
   ) ))

И использовать полученное определение следующим образом:

((LAMBDA (n)
   ((LAMBDA (f) (APPLY f (list f n)))
      #'(LAMBDA (f n ) (COND ((EQ n 0)1) (T (* n 
         (APPLY f (list f (- n 1)))))
) ) ))   5 )   ; = 120 )

Сокращаем совпадающие подфункции и получаем форму, в которой основные содержательные компоненты локализованы:

((LAMBDA  (n) (flet ((afl (f n) 
	(apply f (list f n)) ))

   ((LAMBDA  (f) (afl f n))
      #'(LAMBDA  (f n ) (COND ((EQ n 0)1) (T (* n
         (afl f (- n 1)))) 
)))))
   6 )   ; = 720
)

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

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

Специальная функция FUNCTION (#') обеспечивает доступ к функциональному объекту, связанному с атомом, а функция FUNCALL обеспечивает применение функции к произвольному числу ее аргументов.

(FUNCALL F a1 a2 ... ) = (APPLY F (list a1 a2 ...))

Разрастание числа функций, манипулирующих применением функций в языке Clisp, связано с реализационным различием структурного представления данных и представляемых ими функций.

Программы для Лисп-интерпретатора.

Цель этой части — помочь избежать некоторых общих ошибок при отладке программ.

(CAR '(A B)) = (CAR (QUOTE(A B))
5.2.

Функция: CAR

Аргументы: ((A B))

Значение есть A. Заметим, что интерпретатор ожидает список аргументов. Единственным аргументом для CAR является (A B). Добавочная пара скобок возникает, т.к. APPLY подается список аргументов.

Можно написать (LAMBDA(X)(CAR X)) вместо просто CAR. Это корректно, но не является необходимым.

(CONS 'A '(B . C))
5.3.

Функция: CONS

Аргументы: (A (B . C))

Результат (A . (B . C)) программа печати выведет как (A B . C)

((CAR (QUOTE (A . B))) CDR (QUOTE (C . D)))
5.4.

Функция: CONS

Аргументы: ((CAR (QUOTE (A . B))) (CDR (QUOTE (C . D))))

Значением такого вычисления будет

((CAR (QUOTE (A . B))) . (CDR (QUOTE (C . D))))

Скорее всего, это совсем не то, чего ожидал новичок. Он рассчитывал вместо (CAR (QUOTE (A . B)) получить A и увидеть (A . D) в качестве итогового значения CONS. Интерпретатор настроен не на список уже готовых значений аргументов, а на список выражений, которые будут вычисляться до вызова функции. Кроме очевидного стирания апострофов

(CONS (CAR (QUOTE (A . B))) (CDR (QUOTE (C . D))) )

ниже приведены еще три правильных способа записи нужной формы. Первый состоит в том, что CAR и CDR части функции задаются с помощью LAMBDA в определении функции. Второй заключается в переносе CONS в аргументы и вычислении их с помощью EVAL при пустом а-списке. Третий — в принудительном выполнении константных действий в представлении аргументов.

((LAMBDA (X Y) (CONS (CAR X) (CDR Y))) 
				'(A . B) '(C . D))

Функция: (LAMBDA (X Y) (CONS (CAR X) (CDR Y)))

Аргументы: ((A . B)(C . D))

(EVAL '(CONS (CAR (QUOTE (A . B))) 
            (CDR (QUOTE (C . D)))) Nil)

Функция: EVAL

Аргументы: ((CONS (CAR (QUOTE (A . B))) (CDR (QUOTE (C . D)))) Nil)

Значением того и другого является (A . D)

((LAMBDA (X Y) (CONS (EVAL X) 
	(EVAL Y))) '(CAR (QUOTE (A . B)))'
	(CDR (QUOTE (C . D))) )

Функция: (LAMBDA (X Y) (CONS (EVAL X) (EVAL Y)))

Аргументы: ((CAR (QUOTE (A . B))) (CDR (QUOTE (C . D))))

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

< Лекция 4 || Лекция 5: 123 || Лекция 6 >
Дарья Федотова
Дарья Федотова
Сергей Березовский
Сергей Березовский

В рамках проф. переподготовки по программе "Программирование"

Есть курсы, которые я уже прошел. Но войдя в курс я вижу, что они не зачтены (Язык Ассемблера и архитектура ЭВМ, Программирование на С++ для профессионалов). Это как?

Алина Ленкова
Алина Ленкова
Россия, Ставрополь, СФ МГУПИ, 2014
Валерий Ромашов
Валерий Ромашов
Россия