- •2. Описание лабораторных работ по функциональному программированию на языке лисп
- •2.1. Краткие теоретические сведения по программированию на языке Лисп
- •Основные функции обработки списков
- •Присваивание значений
- •Предикаты
- •Определение функций
- •Ввод-вывод
- •2.2. Лабораторная работа по теме «Знакомство с основными понятиями в системе программирования Лисп»
- •2.3. Лабораторная работа по теме «Решение задач с ветвлением в системе программирования Лисп»
- •2.4. Лабораторная работа по теме «Реализация рекурсии на языке Лисп»
- •2.5. Лабораторная работа по теме «Обработка списков на языке программирования Лисп»
Определение функций
Чтобы определить функцию, необходимо указать количество аргументов функций и их тип, а также правило f, устанавливающее суть отображения.
В языке Лисп для этого применяется лямбда-выражение:
(lambda ({переменная}*){форма}*)
Здесь конструкция ({переменная}*) называется лямбда ─ списком и представляет собой список формальных параметров, которые соответствуют аргументам функции. Последовательность форм в лямбда выражении определяет правило формирования значения функции и называется телом функции. Пусть требуется определить функцию, вычисляющую х5. Тогда лямбда-выражение будет иметь вид:
(lambda (х) ( * х х х х х))
Чтобы задать функцию, строящую список из двух аргументов х и у, можно использовать выражение
(lambda (x у) (cons x (cons у nil)))
Для того, чтобы применить функцию, определенную лямбда-выражением, к некоторым аргументам, необходимо в вызове функции записать вместо имени функции лямбда-выражение. К примеру:
((lambda (x)(* x x x x x)) 4) ─> 1024
((lambda (x y) (cons x (cons у nil))) 'a 'b) ─ > (A B)
В первом примере формальному параметру X лямбда ─ списка присваивается фактическое значение 4, а затем вычисляется тело лямбда ─ выражения. Во втором примере формальным параметрам X и У назначаются значения фактических параметров, заданных атомами А и В, из которых строится список (А В). После произведенных вычислений связи между формальными параметрами и значениями фактических параметров разрушаются.
Вычисление с помощью лямбда ─ вызовов требует каждый раз при очередном вызове заново переписывать тело функции. Проблема решается, если с лямбда ─ выражением связать некоторое имя (символ). Для связи имени и лямбда ─ выражения в Лиспе применяется форма DEFUN:
( defun имя лямбда ─ выражение )
При этом для упрощения записи в лямбда ─ выражении опускается слово LAMBDA и скобки. В итоге получаем:
(defun имя лямбда ─ список {форма}*)
Результатом вызова формы DEFUN является имя определенной функции. Например:
(defun stepen 5 ( х ) (* х х х х х)) ─ > STEPEN 5
(defun spisok ( х у) (cons x (cons у nil))) ─ > SPISOK
Теперь указанные выше лямбда ─ вызовы можно заменить следующими вызовами:
(stepen 5 4) ─ > 1024
(spisok 'а b) ─ > (АВ)
Отметим, что имя функции представляется символом. Поэтому можно говорить о том, что DEFUN устанавливает связь между символом и определением функции. Создавать функции можно в любом текстовом редакторе и хранить в файле с расширением .lsp. Определения функций могут храниться в файлах и загружаться используя функцию LOAD:
(load <имя файла>)
Эта функция загружает файл выражений и выполняет эти выражения.
<Имя файла> - это строковая константа, которая представляет собой имя
файла без расширения (подразумевается расширение ".lsp"). Если
операция успешно завершена, LOAD возвращает имя последней функции, определенной в файле. Если операция не выполнена, LOAD возвращает имя файла в виде строкового выражения.
После запуска интерпретатора необходимо функцией load загрузить файл с библиотекой, к примеру, созданных пользователем функций на диске С: в каталоге PROG.
>(load “C:\\prog\\bibl.lsp”)
Связывание и область действия переменных
Формы группы SET выполняют связывание в результате побочных эффектов. Возникающие при этом связи значений и переменных остаются в силе после завершения SET-вызовов.
Если требуется установить локальные Связи, то используется форма LET:
(let ({( переменная форма-инициализатор )}*){ форма }*)
Здесь форма-инициализатор задает новое значение переменной, которое доступно только в переделах LET. По окончании LET-вызова восстанавливается старое значение переменной в соответствии со связями, которые имела переменная во внешнем окружении LET. Значение, возвращаемое LET-вызовом, соответствует последней вычисленной форме, входящей в LET. Например,
(setf х 5) ─ >5
(set у 7) ─ >7
let ((х 10) (z 6))(+ х у z)) ─ >23
х ─ > 5
z ─ > Error: UNBOUND ─ VARIABLE
В этом примере в LET ─ вызове устанавливаются локальные связи для переменных X и Z. Переменная Y в LET ─ вызове является свободной. Значение свободной переменной определяется связями внешнего окружения LET, т.е. Y=7. После завершения LET─ вызова переменная X восстанавливает свое значение, так как была ранее связана с, помощью SETF. Переменная Z не имела связей во внешнем окружении LET, поэтому попытка определить ее значение за пределами LET завершается ошибкой - UNBOUND VARIABLE (несвязанная переменная).
Связывание переменных внутри формы LET выполняется параллельно. Поэтому в следующем примере результат LET ─ вызова будет пять, а не три:
(setf а 3) ─ > 3
(let (( а 1 ) ( b ( + а 2)) b) ─ > 5
Иными словами, при связывании переменной В здесь используется не локальное значение переменной А, а глобальное, присвоенное вызовом SETF. .
Если необходимо, чтобы связи локальных переменных устанавливались последовательно, то применяется форма LET*. Форма LET представляет собой видоизмененный лямбда-вызов:
(let ((x1 a1) (x2 a2) ... ( )) .{форма }*) <─>
((lambda (x1 х2...хn) ; формальные параметры
{форма }*)
a1 а2 ...an) ; фактические параметры
Отсюда следует, что в LET─ форме формальные параметры представляются переменными x1 х2 ... хn, а фактические параметры a1 а2 ... an задают значения этих переменных. Связь формальных и фактических параметров является локальной и действует только в пределах этих форм. Так как лямбда ─ выражение подставляется вместо имени функции, то и при обычном вызове функции устанавливается связь фактических и формальных параметров, которая действительна только в пределах вызываемой функции.
Определим функцию, вычисляющую выражение
Воспользуемся формой LET*:
(defun f1 (y z)
(let* (( a ( sqrt ( + у z )))
( b ( 4- (* у у ) a))
(с (+ у (/ z 2)))
(x (/с b ))) ; конец списка инициализаторов
х )); возвращаемое значение
В функции F1 значения формальных параметров Y и Z будут локальными. Их начальные значения будут соответствовать значениям фактических параметров, переданных в момент вызова функции. Никакие изменения значений формальных параметров внутри функции не могут отразиться на значениях фактических параметров, так как их связь является локальной. Иными словами, передача значений от фактических параметров к формальным выполняется копированием (т.е. по значению).
Последовательные и условные вычислния
Последовательные вычисления в Лиспе выполняются с помощью форм PROG1, PROG2, PROGN. Данные формы соответствуют составным операторам алгоритмических языков и имеют схожий формат вызова:
(progn {форма}*)
(progl форма1 {форма}*)
(prog2 форма! форма2 {форма}*)
Здесь формы, выступающие в роли аргументов PROG ─ вызовов, вычисляются последовательно, и в качестве результата возвращается значение последней формы (PROGN), первой формы (PROG1) или второй формы (PROG2). Рассмотрим пример моделирования стекового механизма:
(self stack '(a b с d e f)) ─ > (А В С D E F)
(progl (first stack) (setf stack (rest stack))) ─ > A
stack ─ > (BCDEF)
Последовательные вычисления можно также выполнять с помощью рассмотренной выше формы LET*. Эта форма позволяет использовать локальные переменные и в качестве результата возвращает значение последней вычисленной формы, входящей в LET ─ вызов.
Для выполнения условных вычислений в Лиспе могут применяться формы COND, IF, WHEN, UNLESS и др. Форма COND представляет собой макрос и позволяет выбрать одну из n-ветвей алгоритма. Формат вызова формы COND:
(cond {{тест-форма { форма }*)}*)
В этой записи тест-форма представляет собой логическое выражение, построенное с помощью различных предикатов и логических функций: AND (логическое И), OR (логическое ИЛИ), NOT (логическое отрицание). Конструкцию (тест ─ форма {форма}*) называют клозом (clause). Клоз представляет собой список форм.
Приведенный формат вызова формы COND является обобщением следующей записи:
(cond (тест-1 форма-11 форма-12 ... )
(тест-2 форма-21 форма-22 ... )
…
(mecm-i форма-il форма-i2 ... )
…
(mecm-N форма-N1 форма-N2...))
Значение, возвращаемое формой COND, определяется следующими правилами. Последовательно вычисляются значения тест-форм, пока не встретится тест-форма, например mecm-i, значением которой является Т. Затем последовательно вычисляются формы, соответствующего клоза, т.е. форма-i1, форма-i2 и т.д. Значение последней вычисленной формы клоза будет представлять результат, возвращаемый формой COND в точку вызова. Если ни одна из тест-форм не получила значение Т, то результатом вызова формы COND будет NIL.
Обычно в качестве последней тест-формы используется символ Т. Соответствующие ей формы клоза будут вычисляться, когда ни одна из предшествующих тест-проверок не выполняется. К примеру:
(cond ((eq x 'Bern) 'Switzerland)
((eq x 'Paris) 'France)
((eq x 'Berlin) 'Germany)
((eq x 'Kiev) 'Ukraine)
(t 'unknown))
В рассмотренном примере выбиралась одна из пяти ветвей алгоритма. Когда требуется выбрать одну из двух ветвей алгоритма, то используют специальную форму IF. Формат вызова формы:
(if тест-форма then-форма [else-форма])
Если результатом вычисления тест-формы является значение Т, то IF возвращает значение, определяемое then-формой, иначе ─ else-формой, которая может отсутствовать. Условные вычисления можно также организовать с помощью макроформ WHEN и UNLESS. Форма WHEN имеет следующий формат вызова:
( when тест-форма { форма }*)
Здесь формы вычисляются лишь в том случае, если результат вычисления тест-формы ─ Т. Результатом вызова макроса WHEN является значение последней вычисленной формы. Если тест-форма имеет значение NIL, то и вызов макроса WHEN возвращает NIL. Формат макровызова UNLESS аналогичен формату WHEN, но формы будут вычисляться лишь в том случае, если результат вычисления тест-формы ─ NIL.
Итерационные циклические вычисления
В Лиспе имеется средства для организации циклических вычислений. Одним из них является макроформа LOOP. Различают два варианта записи вызова LOOP - простой и расширенный. Рассмотрим простой вызов LOOP:
(loop {форма}*)
Здесь конструкция {форма}* образует тело цикла. Формы, входящие в тело цикла, вычисляются циклически. Цикл завершается, когда управление, в одной из форм, передается макровызову возврата RETURN. При этом RETURN вычисляет значение, возвращаемое в точку вызова LOOP.
Рассмотрим пример. Пусть требуется найти сумму чисел от 1 до N. Ниже приведен текст функции, решающий эту задачу, с помощью макровызова LOOP:
(defun nsum (n)
(setf sum 0)
(loop (cond ((zerop n) (return sum))
(t (setf sum (+ sum n)) (setf n (- n 1))))))
Другая конструкция, применяемая для организации итерационных циклов, - макроформа DO. Эта форма позволяет задавать: локальные (лексические) переменные цикла, их начальные значения и правила обновления; условие окончания цикла и значение, которое возвращает форма DO; формы, образующие тело цикла.
Общий формат макровызова DO:
(do (({переменная} [начальное_значение [обновление]])}*)
(условие_окончания [форма]*)
{форма}*)
Первый аргумент DO-вызова представляет собой список, состоящий из подсписков. Элементами подсписков являются: переменные цикла; формы, задающие начальные значения переменных; выражения, определяющие правила обновления переменных. Если форма, устанавливающая начальное значение переменной, не задана, то значение переменной равно NIL. Если не задана форма обновления, то значение переменной может обновляться в теле цикла.
На каждой итерации вычисляется условие окончания цикла. Когда условие окончания цикла получает значение "истина", то последовательно вычисляются формы, входящие во второй список DO-вызова. Значение последней вычисленной формы и будет результатом. Если условие окончания цикла ложно, то вычисляются формы, образующие тело цикла.
Пусть требуется вычислить факториал числа N. Определим для этого функцию FACTORIAL-DO:
(defun factorial-do (n)
(do ((counter 1 (+ counter 1)) ; управление counter
(result 1 (* result counter))) ; управление result
((= counter (+ n 1)) result))) ; условие окончания
В этом примере для вычисления факториала используется формула n!=(n-1)! n, т.е. факториал каждого следующего числа вычисляется через факториал предыдущего числа. Результаты вычислений на каждом шаге сохраняются в локальной переменной RESULT. Количество выполненных итераций запоминается в счетчике COUNTER. Выход из цикла происходит при условии COUNTER = (n+1).
Часто необходимо выполнять циклическую обработку каждого элемента списка. В этом случае применяется макроформа DOLIST:
(dolist (переменная список {результат]) { форма }*)
Если требуется выполнить некоторые действия N раз, то используется макроформа DOTIMES:
(dotimes ( переменная счетчик {результат}) {форма}*)
Здесь действия выполняются для целочисленных значений переменной, задаваемых формой счетчик и лежащих в диапазоне от 0 до N-1. Когда переменная цикла станет равной N, цикл завершится, возвратив значение формы результат.
Циклические вычисления, использующие передачу управления операторам с метками, можно выполнить с помощью макроформы PROG. Упрощенный формат PROG-вызова имеет вид:
(prog ({переменная}*) {форма}*)
Указанные в начале PROG-вызова переменные являются локальными. Их начальные значения равны NIL. При необходимости они могут быть инициализированы другими значениями аналогично LET-вызову. Даже если список переменных PROG-вызова пустой, его необходимо всё равно задавать. Формы, записываемые внутри PROG-вызова, называются операторами. Если какая-либо форма представлена символом или числом, то она рассматривается как метка. Передать управление на метку можно с помощью оператора GO:
(go метка)
Для окончания вычислений и возврата результатов из PROG-вызова применяется форма RETURN. В следующем примере PROG-форма применяется для вычисления факториала:
(defun factorial-prog (n)
(prog ((counter 1)(result 1))
A (setq result (* result counter))
(setq counter (+ counter 1))
(if (= counter (+ n 1)) (return result))
(go A)))
Здесь переменные COUNTER и RESULT получают начальные значения, равные единице.
Рекурсивные функции
Лисп специально создавался для рекурсивных вычислений, и вся сила языка как раз и заключена в возможности широкого использования рекурсивных функций. К примеру вычисления факториала:
(defun factorial (n)
(cond ((zerop n) 1)
(t (* (factorial (- n i)) n))))
Процесс рекурсивных вызовов можно исследовать, используя возможности трассировки. Включение и выключение механизма трассировки выполняется с помощью входящих в состав интерпретатора директив TRACE и UNTRACE:
Рис. 1. Схема рекурсивных вызовов при вычислении факториала числа 4 .
Лисп позволяет также эффективно определять рекурсивные функции для обработки символьных данных. Определим упрощенную версию функции MEMBER. Чтобы отличать ее от функции, встроенной в систему назовём ее MY-MEMBER. Функция проверяет, входит ли элемент X в список Y и возвращает подсписок Y, который начинается с обнаруженного элемента:
(defun my-member (x у)
(cond ((null у) nil)
((equal x (first у)) у)
(t (my-member x (rest у)))))
В функции выполняется сравнение элемента X с первым элементом списка Y. Если обнаруживается совпадение, то функция возвращает список У. Если совпадение не обнаружено, то продолжается поиск элемента X в хвосте списка У. Для этого рекурсивно вызывается функция MY-MEMBER. Первое условие формы COND на каждом шаге рекурсии контролирует список У. Если этот список исчерпан, то определяемая функция MY-MEMBER возвратит значение NIL. Функция MY-MEMBER использует простую рекурсию.
Примером функции, выполняющей символьную обработку и основанной на параллельной рекурсии, может служить рекурсивная версия функции MY-INTERSECTION:
(defun my-intersection (a b) (cond
((null a) nil) ;1)
((member (first a) b) ;2)
(cons (first a)(my-intersection (rest a) b))) (t (my-intersection (rest a) b)))) ;3)
Данная функция вычисляет список общих элементов двух списков А и В. В определении функции реализованы три правила:
если список А исчерпан, то список общих элементов пустой;
если первый элемент списка А входит в список В, то этот элемент
добавляется к списку общих элементов списков (REST А) и В; такой
список формируется рекурсивным вызовом функции MY- INTERSECTION, применяемой к хвосту списка А и исходному списку В;
если первый элемент списка А не входит в список В, то строится список общих элементов из хвоста списка А и списка В с помощью рекурсивного вызова функции MY-INTERSECTION.