- •Содержание
- •1. Введение в функции
- •1.1. Чистые функции
- •1.2. Функциональность
- •2. Введение в функциональное программирование
- •2.1. О языке Лисп
- •2.2. Примеры программ
- •2.3. Символьная обработка
- •2.4. Особенности Лиспа
- •3. Основы языка лисп
- •3.1. Символы и списки
- •3. 2. Понятие функции
- •3.3. Базовые функции
- •3.4. Имя и значение символа
- •4. Определение функций
- •5. Математические основы: -исчисление
- •5. 1. Введение в синтаксис
- •Определение -термов
- •5.2. Вычисление -выражений
- •5.3. Порядок редукций и нормальные формы
- •5.4. Рекурсивные выражения
- •5.5. Чистое -исчисление
- •5.6. Ламбда-абстракции в Лиспе
- •6. Внутреннее представление списков
- •7. Рекурсия
- •7.1. Простая рекурсия
- •7.2. Другие формы рекурсии
- •8. Функции более высокого порядка
- •8.1. Функционалы
- •8.2. Способы композиции функций
- •8.3. Замыкания
- •8.4. Абстрактный подход
8.3. Замыкания
Чтобы на этапе вызова функционала можно было отличить функциональный аргумент от обычного, функциональный аргумент помечают с помощью предотвращающей вычисления формы FUNCTION:
(function функция)
Примеры:
(function (lambda (x) (list x y)))
(function first)
Форму function называют функциональной блокировкой. Краткое обозначение:
#’f <===> (function f)
Если нужно передать функции данные в том виде, как они записаны, то используется обычная форма QUOTE. Формы QUOTE достаточно и для передачи имени функции или ламбда-выражения, если в нем не используются свободные переменные. В системных функциях свободных переменных нет, поэтому все следующие формы имеют одинаковое значение:
(+ 2 3)
(funcall ‘+ 2 3)
(funcall (function +) 2 3)
(funcall #’+ 2 3)
((lambda (x y) (+ x y)) 2 3)
(funcall (function (lambda (x y) (+ x y))) 2 3)
Замыкание - это функция и контекст ее определения. Часто бывает полезным и необходимым, чтобы функция для продолжения вычислений могла запомнить связи и состояние более раннего контекста. Это достигается с помощью замыкания - функционального объекта, в котором вместе с самим описанием вычислений сохраняется контекст момента определения функции, защищенный от более позднего контекста вызова. В Коммон Лиспе замыкание создается формой FUNCTION, например,
(function (lambda (x) (+ x y)))
В замыкание из контекста определения функции включаются лишь связи свободных переменных функции (в приведенном примере глобальное значение y). Если в замыкаемой функции нет свободных переменных, то форма FUNCTION ничем не отличается от формы QUOTE.
Пример:
; результат вызова функции inc - снова функция
(defun inc (n) #’(lambda (z) (+ z n)))
(defun inc3 (z) (funcall (inc 3) z))
(funcall (inc 4) 5) => 9
8.4. Абстрактный подход
Абстракция отображения (отображение данных) основана на параметризации ламбда-выражений. Можно с помощью механизма параметризации абстрагировать сами функции, т. е. параметризация касается первого элемента вызова функции - это называется абстракцией вычислений.
Обобщение функций, имеющих одинаковый вид. Рассмотрим обобщение с помощью функционалов функций, определения которых с точки зрения абстракции вычислений сходны по строению, но которые довольно различны с точки зрения производимых действий.
Функция последовательного объединения двух списков.
(defun append1 (x y)
(if (null x) y
(cons (first x) (append1 (rest x) y))))
Функция слияния двух упорядоченных списков в один упорядоченный.
(defun merge (x y)
(if (null x) y
(insert (first x) (merge (rest x) y))))
(defun insert (a s)
(cond ((null s) (list a))
((< a (first s)) (cons a s))
(t (cons (first s) (insert a (rest s)))))
)
Добавим новый функциональный аргумент в reduce.
(defun reduce2 (g f x a)
(if (null x) a
(funcall g (funcall f (first x)) (reduce2 g f (rest x) a))
)
)
Вместо двух рекурсивных функций append1 и merge применяем обобщающий функционал reduce2
(defun append2 (x y) (reduce2 #’cons #’(lambda (z) z) x y))
(defun merge1 (x y) (reduce2 #’insert #’(lambda (z) z) x y))
Определим функциональный предикат (forall p s), который истинен т. и т.т., когда p(x) истинна для всех элементов списка s.
(defun forall (p s)
(reduce #’and (mapcar p s) t))
В этом примере нужно использовать не встроенную функцию and, а определить ее самостоятельно, например, так:
(defun and (x y)
(if x y nil))
Определим функциональный предикат (forsome p s), который истинен т. и т.т., когда p(x) истинна для некоторого элемента x списка s.
(defun forsome (p s)
(reduce #’or (mapcar p s) nil))
В этом примере также нужно использовать не встроенную функцию or, а определить ее самостоятельно, например, так:
(defun or (x y)
(if x t y ))
Функционал reduce2 можно использовать для образных вычислений.
(defun member (x s)
(reduce2 #’or #’(lambda (y) (equal y x)) s nil))
; число вхождений элемента в список
(defun count (x s)
(reduce2 #’+ #’ (lambda (y) (if (eql x y) 1 0)) s 0)
(defun map (f x)
(reduce2 #’cons f x nil))
Абстрагирование вычислений с помощью функций с функциональными значениями. Композицию двух функций можно определить так:
(defun comp (f g)
#’(lambda (x) (funcall f (funcall g x)))
)
В результате конкретного вызова функции comp возвращается одноместная функция (в Лиспе она трактуется как замыкание). Мы можем теперь строить пирамиды новых функционалов. Например, определим функцию twice:
(defun twice (f) (comp f f))
Примеры вызовов:
(funcall (twice #’ (lambda (x) (* x x x))) 2) => 512
(defun inc3 (x) (+ x 3))
(funcall (twice 'inc3) 10) => 16
(funcall (twice ( twice ‘1+)) 1) ==> 5
Определим n-ую степень функции fn <==> f(f(f(f(...f(x))))
(f применяется n раз).
(defun n-comp (f n)
(if (zerop n) #’(lambda (x) x)
(comp f (n-comp f (1- n)))))
(funcall (n-comp #’1+ 10) 20) ==> 30
Примеры использования функции n-comp:
сложение двух натуральных чисел
(defun summa (n m)
(funcall (n-comp #’1+ n) m))
(summa 4 9) ==>13
умножение двух натуральных чисел
(defun mult (n m)
(funcall (n-comp #’(lambda (x) (summa x n)) m) 0))
(mult 5 6) ==> 30
возведение натурального числа в натуральную степень
(defun power (n m)
(funcall (n-comp #’(lambda (x) (mult x n)) m) 1))
(power 2 10) ==> 1024
Автофункции
В функциональном программировании рассматриваются автоаппликативные и авторепликативные функционалы как функции, которые получают или возвращают в качестве результата самих себя или, точнее, копии самих себя. Конечно, не очевидно, что такие функции вообще существуют. Однако никаких принципиальных препятствий для их существования и определения не существует. В Лиспе их можно определить и использовать довольно просто.
Для начала рассмотрим автоаппликативный вариант факториала:
(defun factorial (f n)
(if (zerop n) 1 (* n (funcall f f (1- n)))))
Для вычисления 10! необходимо функционалу factorial в его вызове в качестве функционального аргумента передать его собственное определение.
> (factorial 'factorial 10)
3628800
Соответственно тому, как мы можем через функциональный аргумент определить автоаппликативную рекурсивную функцию, мы можем определить и возвращающую себя рекурсивно функцию с функциональным значением.
Возьмем в качестве примера простейшую возможную функцию, которая возвращает себя в качестве результата. Это лямбда-выражение, примененное к такому же лямбда-выражению с формой QUOTE, т. е. это лямбда-вызов:
((lambda (x) (list x (list ‘quote x))) ‘(lambda (x) (list x (list ‘quote x))))
Форма является одновременно автоаппликативной! Она содержит изображение самой себя, но не целиком, поскольку такое изображение было бы бесконечно длинным. Изображение получается ограниченным благодаря механизму блокировки и лямбда-механизму, а также благодаря автоаппликации.
Для проверки присвоим форму переменной self:
(setq self
‘((lambda (x) (list x (list ‘quote x))) ‘(lambda (x) (list x (list ‘quote x)))))
> (eval self)
((lambda (x) (list x (list ‘quote x))) ‘(lambda (x) (list x (list ‘quote x))))
Форму можно вычислять вновь и вновь, и все равно получается тот же результат: само определение функции (его копия). Кроме того, значение функции совпадает со значением аргумента ее вызова. Такое значение называется неподвижной точкой функции.
Форма self аналогична известному комбинатору (x. x x) (x. x x):
-редукция терма (x. x x) (x. x x) приводит к бесконечному циклу
(x. x x) (x. x x) (x. x x) (x. x x) … .
Автоаппликация как форма рекурсии с теоретической точки зрения очень интересна, но в практическом программировании не находит особого применения. С нею связаны теоретические вопросы и проблемы, которые во многом еще не изучены. В формализмах и постановке проблем, которые основаны на похожих на автоаппликацию ссылках на себя (self-reference), очень легко приходят к логическим парадоксам, подобным известному из учебников по теории множеств парадоксу Рассела. Их разрешение предполагает учет типов объектов и кратности ссылок.
Однако автоаппликация может открыть новый подход к программированию. Возможными применениями могли бы быть, например, системы, сохраняющие неизменными определенные свойства, хотя какие-то их части изменяются. Такими свойствами могли бы быть наряду с применимостью и репродуцируемостью, например, способность к самоизменениям (self-modification), таким как приспособляемость, согласованность и обучаемость, самосознание и сознательность и т. д. Самосознание предполагает существование модели мира внутри такой системы, включая саму систему в эту модель.
Автофункции - это новый класс функций, которые отличаются от обыкновенных рекурсивных функций так же, как рекурсивные функции отличаются от нерекурсивных. Чтобы их можно было использовать в программировании столь же удобно, как и рекурсию, нужно использовать механизмы, которые при программировании учитывают инвариантные свойства вычислений таким же образом, как механизмы определения, вызова и вычисления функций учитывают рекурсию.