Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

книги / Функциональное программирование

..pdf
Скачиваний:
0
Добавлен:
12.11.2023
Размер:
14.84 Mб
Скачать

1.22. Упражнение для самоконтроля 2

Задача 1.10

Используя функцию COND, напишите функцию, которая спрашивает у пользователя ФИО двух студентов из списка группы, для которых:

а) сравнивает возраст и выдает результат; б) сравнивает средний балл и выдает сообщение о резуль-

татах сравнения; с) проверяет родственные связи и выдает об этом сооб-

щение.

Вывод информации осуществить при помощи функции

PRINT.

Задача 1.11

Решите задачу 1.10 с использованием функций IF, WHEN, UNLESS.

51

2. ПРЕДСТАВЛЕНИЕ ДАННЫХ, МЕТОДОВ И ПРИЕМОВ ФУНКЦИОНАЛЬНОГО ПРОГРАММИРОВАНИЯ

Очень часто в учебниках можно встретить жесткое разделение языков программирования на отдельные категории. На самом деле положительные качества одних языков переходят вдругие, не свойственные им, парадигмы программирования. Например, в языках функционального программирования можно встретить циклы, ав императивных – функциональное разделение и т.п. Точнее было бы вместо типа языка говорить о стиле или методике программирования. Различные языки программирования могут поддерживать различныепарадигмыпрограммированиявразномобъеме.

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

«Лисп» такжене является исключением иактивноперенимает элементыи методыиздругихпарадигмпрограммирования.Опишем этинетипичныедляфункциональногопрограммированияфункции.

2.1. Представление функций через аналоги

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

(LET ((xl знач1) (x2 знач2) …)

форма1 форма2

)

52

В начале статическим переменным x1, x2, … назначается локальная связь с соответствующими значениями знач1, знач2 … из аргументов, причем это назначение происходит параллельно. Затем вычисляются значения форма1, форма2, … в порядке слева направо, сверху вниз.

Значением всей функции LET становится значение последней вычисленной формы. После вычисления сборщик мусора освобождает, т.е. уничтожает, статические переменные x1, x2, ….

Функция LET очень похожа на λ-вызов с описанием в начале формальных и фактических параметров. Сравним запись LET и λ-вызова:

(LET ((xl al) (x2 a2) … (xn an)) форма1 форма2 …), ((LAMBDA (xl x2 … xn) форма1 форма2 …) а1 а2 … an.

Значением λ-вызова после завершения вычислений также становится значение последней формы.

Теперь рассмотрим особенности работы разветвления вычислений на примере предложения COND.

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

Подобным образом можно выразить практически все логические операторы из логики высказываний. Например, логическое «и», логическое «или», логическое «не», импликацию и тождество:

>(defun лог_и (х у) (cond (x у) (t nil)))

>(defun лог_или (х у) (cond (x t)

(t y)))

53

>(defun лог_не (х) (not x))

>(defun импликац (x y) (cond (x y)

(t t)

Импликацию можно определить и через другие операции: >(defun импликац (x y)

(лог_или x (лог_не y))) >(defun тожд (x y)

(лог_и (импликац x y) (импликац y x)))

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

>(and (atom nil) (null nil) (eq nil nil)) T

>(and (atom nil) (+ 2 3)) 5

Выразить действие предиката AND можно через упрощение условного предложения COND. Например:

(COND

(AND условие1 условие2 … условиеN-1 условие N) (t nil))

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

>(defun исключающее_или (х у) (cond (x (cond (y nil)

(t t)

(t y)))

54

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

Подобным образом в «Лиспе» имеется возможность выражать одни условные предложения через другие.

Следующие две записи условия равнозначны: (IF условие ТО-форма ИНАЧЕ-форма), (COND (условие то-форма) (Т иначе-форма)).

Можно заметить, что в первом предложении скобок гораздо меньше, а значит, и читаемость выше. Например:

>(if (atom t) 'атом 'список),

АТОМ.

Рассмотрим теперь принцип выражения предложений

WHEN и UNLESS через COND и IF: (WHEN условие форма1 форма2 …)

(UNLESS (NOT условие) форма1 форма2 …)

(COND (условие форма1 форма2 …))

(IF условие (PROGN форма1 форма2 …) NIL)

В «Лиспе» кроме условных предложений есть конструкция множественного выбора CASE, так же как в языке императивного программирования «Паскаль»:

(CASE ключ

(список-ключей1 form11 form12 …) (список-ключей2 form21 form22 …)

Принцип действия предложения CASE очень прост. Сначала вычисляется значение ключевой формы, а затем полученный ключ сравнивают со значением из списка ключей. Если найден соответствующий ключ в списке, то вычисляются соответствующие формы. Значение последней возвращается в качестве значения всей функции CASE. Данную конструкцию тоже можно определить как неявно заданное предложение PROGN.

55

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

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

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

Самым простым примером является создание пользовательской функции возведения числа X в степень N, тем более что в «Лиспе» такой стандартной функции нет. Например,

>(defun степень (x n)

 

(PROG (результат)

 

(setq результат x)

m1

; метка

 

(if (= n 1)

(RETURN результат)

)

(setq результат (* результат x)) (setq n (– n 1))

(GO m1))) >(степень 2 3)

8 > результат

Error: Unbound atom РЕЗУЛЬТАТ

Как видно из примера, этот текст намного больше, чем предложение DO, но зато четко прослеживается особенность работы по передаче управления GO и RETURN и предложения PROG. Формы GO и RETURN являются статическими формами, т.е. они управляют в пределах текста определения.

56

Рассмотрим теперь рекурсивный вариант реализации функции возведения в степень.

>(defun степень (x n) (if (= n 1)

x

(* x (степень x (– n 1)))))

В качестве граничного условия выбрано значение n = 1, т.е. число X умножается N раз на само себя. Как это происходит: функция определила, что необходимо умножить X на число X, но уже в степени (N – 1); потом получится, что Х умножаем на X и на результат числа X, но уже в степени (N – 2); …, когда будет число X, но уже в степени (N – N), рекурсия соберет все эти умножения в одно выражение.

Рекурсивное определение, как видим, много короче. Рассмотрим еще один стандартный пример – нахождение

факториала числа, который легко и просто определяется через рекурсию.

Например, математически факториал через рекурсию понимается как два правила:

n! = 1

, если n = 0;

n! = n*(n – 1)

, если n > 0.

Наязыке«Лисп»этафункциявыглядит следующимобразом: >(defun factorial (n)

(if (= n 0) 1

(* n (factorial (– n 1))))) >(factorial 5)

120

Особенностью «Лиспа» является то обстоятельство, что это беcтиповый язык программирования, а это значит, чтобы посчитать значение 10000, например, на языке С++, у вас просто не хватит ни одного типа данных, а значит, нужно пользоваться нестандартными механизмами вычисления, что приведет к колоссальному увеличению программного кода. В «Лиспе» же

57

нужно просто задать (print (factorial 10000)) – и все. Правда результат не сможет поместиться на экран, но это нестрашно, «Лисп» его запишет в несколько десятков строк.

Рекурсивное решение хорошо подходит для работы со списками, так как списки могут рекурсивно содержать подсписки.

2.2.Формы динамического прекращения вычислений – CATCH и THROW

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

Такое динамическое прерывание вычислений можно запрограммировать в «Коммон Лиспе» с помощью форм CATCH и THROW, которые, как это видно из их имен (ПОЙМАТЬ, БРОСИТЬ), передают управление. Подготовка к прерыванию осуществляется специальной формой CATCH:

(CATCH метка форма1 форма2 …).

Например: (CATCH 'возврат1

(делай-раз) (делай-два) (делай-три))

58

При вычислении формы сначала вычисляется метка, а затем формы «форма1, форма2, …» слева направо. Значением формы будет последнее значение (неявный &PROG) при условии, что во время вычисления непосредственно этих форм или форм, вызванных из них, не встретится предложение THROW:

(THROW метка значение).

Если аргумент «метка» вызова THROW представляет собой тот же лисповский объект, что и метка в форме CATCH, то управление передается обратно в форму CATCH и его значением станет значение второго аргумента формы THROW. В приведенном ранее примере в результате вычисления формы

(THROW возврат1 'сделано)

вызов CATCH получает значение СДЕЛАНО, и если это произошло во время вычисления функции ДЕЛАЙ-ДВА, то вычисление ДЕЛАЙ-ТРИ отменяется. Механизм CATCH-THROW позволяет осуществлять возврат управления из динамического окружения, вложенного на любую глубину.

2.3. Внутреннее представление списков

Лисповская память состоит из списочных ячеек. Оперативная память машины, на которой работает Лисп-

система, логически разбивается на маленькие области, которые называются списочными ячейками (memory cell, list cell, cons cell или просто cons). Списочная ячейка состоит из двух частей, полей CAR и CDR. Каждое из полей содержит указатель. Указатель может ссылаться на другую списочную ячейку или на некоторый другой лисповский объект, как, например, атом. Указатели между ячейками образуют как бы цепочку, по которой можно из предыдущей ячейки попасть в следующую и так, наконец, до атомарных объектов. Каждый известный системе атом записан в определенном месте памяти лишь одни раз.

59

Рис. 6. Списочная ячейка

Графически списочная ячейка представляется прямоугольником (рис. 6), разделенным на части (поля) CAR и CDR. Указатель изображается в виде стрелки, начинающейся в одной из частей прямоугольника и заканчивающейся на изображении другой ячейки или атоме, на которые ссылается указатель.

Указателем списка является указатель на первую ячейку списка. На ячейку могут указывать не только поля CAR и CDR других ячеек, но и используемый в качестве переменной символ, указатель из которого ссылается на объект, являющийся значением символа. Указатель на значение хранится вместе с символом в качестве его системного свойства. Кроме значения, системными свойствами символа могут быть определение функции, представленное в виде лямбда-выражения, последовательность знаков, задающая внешний вид переменной, выражение, определяющее пространство, в которое входит имя, и список свойств.

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

(setq список '(а b с)) (А В С)

создает в качестве побочного эффекта изображенную на рис. 7 штриховую стрелку.

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

60