Мир Лиспа - 2
Другие функции ветвления (if условие если_истина если_ложь) (when условие форма-1 форма-2. . . ) (unless условие форма-1 форма-2. . . )
4. Числа Фибоначчи Последовательность Фибоначчи задается следующим образом: F 1=1 F 2=1 Fn=Fn-1+Fn-2 Попробуем реализовать ее на Лиспе
Первое решение (defun fib-s (n) (cond ((< n 2) 1) (t (+ (fib-s (- n 1)) (fib-s (- n 2))))))) fib-s (fib-s 8) 21 (fib-s 20) 6765 Хорошее ли это решение?
Запустим трассировку. . . (trace fib-s) fib-s (fib-s 5) Вход в функцию fib-s Аргументы: 5 Вход в функцию fib-s Аргументы: 4 Вход в функцию fib-s Аргументы: 3 Вход в функцию fib-s Аргументы: 2 Возврат из функции fib-s Результат: 1 Вход в функцию fib-s Аргументы: 1 Возврат из функции fib-s Результат: 2 Вход в функцию fib-s Аргументы: 2 Возврат из функции fib-s Результат: 1 Возврат из функции fib-s Результат: 3 Вход в функцию fib-s Аргументы: 2 Возврат из функции fib-s Результат: 1 Вход в функцию fib-s Аргументы: 1 Возврат из функции fib-s Результат: 2 Возврат из функции fib-s Результат: 5 5
Плохое решение! Видно, что алгоритм крайне неэффективен: происходит многократный пересчет функции при одних и тех же значениях аргумента. Кстати, а как сосчитать, сколько происходит рекурсивных вызовов в процессе вычисления?
Статистика выполнения В Home. Lisp есть функции stat и unstat – включение и выключение режима статистики. Включим статистику и рассчитаем 20 -е число Фибоначчи. . .
(stat) T (fib-s 20) 6765 *** Статистика обращений *** +. . . 6764 -. . . 13528 <=. . . 13529 FIB-S. . . . . 13529 COND. . . . . 13529
А теперь – рациональное решение: (defun fib-n (n &optional (curr 1) (prev 1)) (cond ((= n 2) curr) (t (fib-n (- n 1) (+ curr prev) curr)))) В функции 2 накапливающих параметра. Функция остаётся рекурсивной; посмотрим, как обстоит дело с производительностью. . .
Пробуем. . . (fib-n 20) 6765 ; ; результат верный ! *** Статистика обращений *** -. . . 18 +. . . 18 <=. . . 19 FIB-N. . . . . 19 COND. . . . . 19 Комментарии излишни. . .
Создание локальных переменных Функции LET/LET*
Общий вид Let: (let ((имя-1 значение-1) (имя-2 значение-2). . . (имя-n значение-n)) (форма-1) (форма-2). . . (форма-m)) Создаются переменные, которые “живут”в теле Let.
В процессе вычисления LET все локальные переменные вычисляются “одновременно”: (Let ((x 1) (y (* x x)) ; ; здесь будет ошибка ! …) У функции LET* локальные переменные вычисляются “последовательно”: (Let* ((x 1) (y (* x x)) ; ; все хорошо ! …)
5. Итерационные циклы Рассмотрим классическую задачу: Вычислить с заданной точностью eps при заданном x сумму ряда: Σxn/n! (кстати, чему она равна? ) правильно = ex
Как рационально решить задачу? Нерадивые студенты “кинутся” вычислять n! - и зря! Рассмотрим, чем отличаются друг от друга n-й и (n+1)-й члены ряда: Qn=xn/n! а Qn+1=x(n+1)/(n+1)! Qn+1/Qn=x/(n+1) т. е. , чтобы вычислить следующий член ряда, нужно взять предыдущий, умножить на x и разделить на (n+1)
Вот функция: (defun aexp (x eps &optional (s 0) (n 0) (q 1)) (let ((z (/ (* q x) (+ n 1))) (s (+ s z))) (cond ((< z eps) s) ; ; терм. ветвь (t (aexp x eps s (+ n 1) z))))) s, n, q – накопительные параметры. Функция рекурсивная; завершение рекурсии происходит при достижении заданной точности eps.
Пробуем функцию “в деле”: (aexp 1 1 E-14) 2. 71828459040 E+0 (exp 1) 2. 71828459050 E+0
Небольшое дополнение Некоторые устаревшие реализации Лиспа не поддерживаю &optional – параметры. Как же в таких реализациях использовать технику накапливающих параметров?
В этом случае делаем накапливающие параметры обязательными, отлаживаем функцию, задавая начальные значения накапливающих параметров явно, а затем “погружаем” нашу функцию в функцию-обертку (служащую только для запуска).
; ; Главная функция (defun exp_ (x eps s n q ) (let ((q (/ (* q x) (+ n 1))) (s (+ s q))) (cond ((< q eps) s) (t (exp_ x eps s (+ n 1) q))))) ; ; Функция - обертка (defun aexp (x eps) (exp_ x eps 0 0 1))
Безымянные функции
Это конструкция вида: (lambda (список_параметров). . . тело_функции ; ; 1 или более S-выражений ) Отличие от defun – в замене defun на lambda и отсутствии имени функции
Как этим пользоваться? Вызов обычной (именованной) функции: (Имя_функции Параметры) Вызов безымянной функции: (Лямбда-выражение Параметры)
Примеры: ((lambda (x y) (* x y)) 7 8) ? 56 ((lambda (x y &optional r) (list x y r)) ‘a ‘b) ? (a b Nil) Лямбда-выражение может стоять в любом месте, где допустим вызов функции.
Для чего нужны безымянные функции? Для экономии функций именованных. Может ли безымянная функция быть рекурсивной? Вопрос интересный…
А ответ, тем не менее, положителен! Рассмотрим сначала традиционную функцию вычисления факториала: (defun fact (n) (cond ((= n 0) 1) (t (* n (fact (- n 1))))))
Проверим: (fact 100) 93326215443944152681699238856266700490 71596826438162146859296389521759999322 99156089414639761565182862536979208272 23758251185210916864000000000
((lambda (n f) (cond ((= n 0) 1) (t (* n (funcall f (- n 1) f))))) 10 '(lambda (n f) (cond ((= n 0) 1) (t (* n (funcall f (- n 1) f)))))) 3628800
Функции высшего порядка (функционалы)
Функциональные языки (ФЯ) отличаются от нефункциональных тем, что в ФЯ функции являются полноправными значениями, т. е. могут передаваться в другие функции и возвращаться как результат вычисления. Функция, принимающая одну (или более) функций как аргумент (аргументы), называется функционалом.
Пример функционала: Есть числовой список. Нужно построить список, состоящий из квадратов исходного списка. (defun q 2 (x) (cond ((null x) Nil) (t (cons (^ (car x) 2) (q 2 (cdr x)))))) q 2 (q 2 '(1 2 3 4 5 6)) (1 4 9 16 25 36) всё хорошо. . . А если теперь нужно возводить в куб?
Попробуем “приделать” нашей функции функциональный аргумент: (defun aq (f x) (cond ((null x) Nil) (t (cons (f (car x)) (q 2 (cdr x)))))) Как вызвать такую функцию? (aq ‘(lambda (x) (* x x)) ‘(1 2 3 4 5 6)) ; ; в квадрат (aq ‘(lambda (x) (* x x x)) ‘(1 2 3 4 5 6)) ; ; в куб (aq ‘ff ‘(1 2 3 4 5 6)) ; ; ff произв. функция одного аргумента
К сожалению, есть проблема: В Лиспе нельзя применить функциональный аргумент так, как мы пытаемся: (defun aq (f x) (cond ((null x) Nil) (t (cons (f (car x)) (aq f (cdr x)))))) ; ; ошибка!!! Для применения функционального аргумента служит специальная функция FUNCALL: (defun aq (f x) (cond ((null x) Nil) (t (cons (funcall f (car x)) (aq f (cdr x)))))) ; ; верно!
Funcall Функционал funcall принимает неопределенное количество аргументов. Он применяет свой первый (функциональный) аргумент к списку, составленному из всех остальных аргументов. Естественно, количество аргументов должно быть “правильным” (соответствовать списку параметров функционального аргумента).
Испытаем функцию AQ: (defun aq (f x) (cond ((null x) Nil) (t (cons (funcall f (car x)) (aq f (cdr x)))))) aq (aq '(lambda (x) (* x x)) '(1 2 3 4 5 6)) ; ; в квадрат (1 4 9 16 25 36) (aq '(lambda (x) (* x x x)) '(1 2 3 4 5 6)) ; ; в куб (1 8 27 64 125 216) (aq 'sin '(1 2 3 4 5 6)) ; ; синусы (8. 414709848078970 E-1 9. 092974268256820 E-1 1. 411200080598670 E-1 -7. 568024953079280 E-1 9. 589242746631380 E-1 -2. 794154981989260 E-1)
Функционал, который применяет функциональный аргумент к задаваемым параметрам, называется применяющим. Funcall – применяющий функционал. Другой стандартный применяющий функционал Лиспа – это Apply
Функционал Apply: Принимает два аргумента: первый функциональный, а второй – список произвольной длины. Apply применяет свой функциональный аргумент к этому списку. Разница между Funcall и Apply состоит только в одном: Funcall требует аргументы “врассыпную”, а Apply – собранными в список.
Поскольку стандартная функции * (times) и + (plus) в Лиспе принимают неопределенное число аргументов, то самое простое решение задачи о сумме (произведении) элементов произвольного числового списка это применение Apply (вообще без рекурсии!)
(defun multlist (x) (apply '* x)) multlist (multlist '(1 2 3 4 5 6 7 8 9 10 11 12 13)) 6227020800 (defun sumlist (x) (apply '+ x)) sumlist (sumlist '(1 2 3 4 5 6 7 8 9 10 11 12 13)) 91
Сортировка выбором (defun vsort (lst) (cond ((null lst) Nil) (t (let ((min (apply ‘min lst))) (cons min (vsort (removef min lst)))))) (vsort '(1 2 3 4 1 2 3 -7)) (-7 1 1 2 2 3 3 4)
Отображающие функционалы: Отображающие функционалы применяют (определенным образом) свой функциональный аргумент к списку или его части и возвращают новый список. Этот вид функционалов “отображает” исходный список на результирующий. Отсюда и название.
Простейший отображающий функционал - mapcar Фунционал mapcar принимает два аргумента: первый функциональный, второй – список произвольной длины. Работа функционала состоит в последовательном применении функционального аргумента к первому элементу списка, второму и т. д. Полученные результаты вновь объединяются в список.
Применение mapcar значительно упрощает решение уже рассмотренной ранее задачи о списке квадратов, кубов и т. д. При этом даже не требуется явная рекурсия: (mapcar '(lambda (x) (* x x)) '(1 2 3 4 5 6)) (1 4 9 16 25 36) (mapcar '(lambda (x) (* x x x)) '(1 2 3 4 5 6)) (1 8 27 64 125 216)
Еще один отображающий функционал maplist Функционал maplist последовательно применяет свой первый функциональный аргумент ко всему списку (заданному вторым аргументом), к хвосту списка и т. д. Результаты объединяются в список. Функция, задаваемая функциональным аргументом, должна принимать список.
Пример вызова maplist: (maplist '(lambda (x) (apply '* x)) '(1 2 3 4 5 6)) (720 360 120 30 6) Здесь лямбда-выражение сначала применяется к списку (1 2 3 4 5 6) и возвращает 6!=720; далее оно применяется к списку (2 3 4 5 6) и снова возвращает 720; затем применяется к списку (3 4 5 6) и дает 360 и т. д.
Поскольку maplist написана на Лиспе, ее можно протрассировать: (trace maplist) maplist (maplist '(lambda (x) (apply '* x)) '(1 2 3 4 5 6)) Вход в функцию maplist Аргументы: (LAMBDA (x) (APPLY (QUOTE *) x)) (1 2 3 4 5 6) Вход в функцию maplist Аргументы: (LAMBDA (x) (APPLY (QUOTE *) x)) (3 4 5 6) Вход в функцию maplist Аргументы: (LAMBDA (x) (APPLY (QUOTE *) x)) (5 6) Вход в функцию maplist Аргументы: (LAMBDA (x) (APPLY (QUOTE *) x)) (6) Вход в функцию maplist Аргументы: (LAMBDA (x) (APPLY (QUOTE *) x)) NIL Возврат из функции maplist Результат: (6) Возврат из функции maplist Результат: (30 6) Возврат из функции maplist Результат: (120 30 6) Возврат из функции maplist Результат: (360 120 30 6) Возврат из функции maplist Результат: (720 720 360 120 30 6) (720 360 120 30 6)
Поскольку maplist в процессе работы получает доступ ко всем элементам списка (или его остатков), а не только к первым элементам (как mapcar), то maplist является и более универсальной функцией, чем mapcar. В частности, mapcar можно выразить через maplist.
Мощь mapcar Попробуем вычислить скалярное произведение двух векторов, заданных числовыми списками одинаковой длины: (v 1 v 2 v 3. . . ) и (w 1 w 2 w 3. . . )
Mapcar на самом деле может принимать не один, а произвольное количество списков. В этом случае он (mapcar) работает так:
Если мы напишем вызов: (mapcar ‘* ‘(v 1 v 2 v 3) ‘(w 1 w 2 w 3)) то получим: (v 1*w 1 v 2*w 2 v 3*w 3)
Теперь нужно подсчитать сумму этого списка. Как это сделать? С помощью Apply! (apply ‘+ (mapcar ‘* (v 1 v 2 v 3) (w 1 w 2 w 3)))
Окончательное решение: (defun scal-prod (v w) (apply ‘+ (mapcar ‘* v w))) Этот код будет правильно считать скалярное произведение векторов любой размерности!
Другие стандартные функционалы: remove-if - удаление из списка по соблюдению условия; remove-if-not - удаление из списка по нарушению условия; every – проверка истинности предиката на всех элементах списка; reduce – свертка;
Функционал every Этот функционал не является отображающим. Первым аргументом every должен быть предикат (логическая функция, вычисляющаяся до T/Nil). Вторым аргументом должен быть произвольный список. Если условие, задаваемое предикатом, истинно для каждого элемента списка, every вернет T, иначе – Nil.
Примеры: (oddp 1) T (oddp 2) NIL (every 'oddp '(1 2 3 4)) NIL (every 'oddp '(1 21 31 41)) T
Функционалы remove-if [-not] Этот функционал использует первый (функциональный) аргумент как предикат и применяет его последовательно к каждому элементу списка-второго аргумента. Если элемент удовлетворяет (не удовлетворяет) предикату – элемент попадает в выходной список. remove-if и remove-if-not – отображающие функционалы (аналогичные mapcar).
Элементарные примеры: (remove-if 'oddp '(1 2 3 4 5 6 7 8 9)) (2 4 6 8) (remove-if-not 'oddp '(1 2 3 4 5 6 7 8 9)) (1 3 5 7 9) (remove-if '(lambda (x) (= 0 (% x 3))) '(1 2 3 4 5 6 7 8 9)) (1 2 4 5 7 8)
Дополнительные параметры remove-if-. . . У этих функционалов есть еще два дополнительных (необязательных) ключевых параметра: : count n - сколько элементов удалять; : from-end [t/nil] – удалять с конца или с начала списка.
Примеры: (remove-if '(lambda (x) (= 0 (% x 3))) '(1 2 3 4 5 6 7 8 9) : count 1) (1 2 4 5 6 7 8 9) (remove-if '(lambda (x) (= 0 (% x 3))) '(1 2 3 4 5 6 7 8 9) : count 1 : from-end t) (1 2 3 4 5 6 7 8)
Свертка - reduce Этот функционал применяет свой первый (функциональный) аргумент сначала к первым двум элементам списка, затем к этому результату и третьему элементу и т. д. до исчерпания списка. Результат последнего вычисления возвращается, как результат свертки.
Примеры: (reduce '* '(1 2 3 4)) 24 (reduce 'max '(1 2 3 4 5 4 3 2 1)) 5 (reduce 'list '(1 2 3 4 5)) ((((1 2) 3) 4) 5) (reduce 'append '((1 2) (3 4) (5 6))) (1 2 3 4 5 6)
Необязательные параметры reduce : initial-value v - задает первое значение, c которого начинается свертка. (reduce '* '(1 2 3) : initial-value 5) 30 (reduce 'max '(1 2 3 4 5 4 3 2 1) : initial-value 100) 100
Продолжение. . . (reduce 'append '((1) (2) (3) (4) ) : initial-value '(b e g)) (b e g 1 2 3 4) Поддерживается также и параметр : from-end (reduce 'append '((1) (2) (3) (4) ) : initial-value '(b e g) : from-end t) (1 2 3 4 b e g)
Существует еще много других стандартных функционалов. . .
Замыкания
“Плохая” функция setq Функция setq позволяет явно присваивать значения переменным: (setq a ‘(1 2 3)) (1 2 3) a (1 2 3)
До сих пор нам вполне удавалось обходиться без явных присвоений. Там, где требовались рабочие переменные, мы использовали функцию let.
Общий вид Let: (let ((имя-1 значение-1) (имя-2 значение-2). . . (имя-n значение-n)) (форма-1) (форма-2). . . (форма-m)) Создаются переменные, которые “живут”в теле Let.
Что будет, если в теле let определить одну или более функций?
Попробуем: (let ((c 0)) (defun next-1 nil (setq c (+ 1 c))) ) Переменная c свободна (относительно тела nехт)
(let ((c 0)) (defun next-1 nil (setq c (+ 1 c))) ) next-1 (next-1) 2 (next-1) 3 c Assoc: Символ c не имеет значения (не связан). ERRSTATE
Еще один эксперимент: (let ((c 0)) (defun next-2 nil (setq c (+ 1 c))) ) next-2 (next-2) 1 (next-1) 4
Если в теле let определяется функция, то свободные переменные запоминаются в состоянии на тот момент, когда выполнялась defun. Эти переменные доступны в теле функции и могут модифицироваться. Функциональный объект Лиспа, включающий функцию и зафиксированные значения ее свободных переменных называется замыканием.
Чем неудобны наши функции Next-1 и Next-2? Тем, что они “шагают” только вперед. . . Нельзя ли сделать возможность “сброса” значения внутренней переменной с? Что будет, если мы просто введем: (setq c 0) ? да ровным счетом ничего – будет создана глобальная переменная с, не имеющая отношения к Next-1 и Next-2.
Исправление неудобства: (let ((c 0)) (defun next-3 nil (setq c (+ 1 c))) (defun reset-3 nil (setq c 0)) ) Теперь к переменной c имеют совместный доступ две функции. Вызов next-3 возвращает следующее значение, вызов reset-3 сбрасывает счетчик.
(next-3) 1 (next-3) 2 (next-3) 3 (reset-3) ; ; сброс счетчика 0 (next-3) ; ; счет с начала. . . 1
Давайте вернемся к безымянным функциям и попробуем ввести лямбда-выражение, не указав параметры: (lambda (x) (* x x)) (CLOSURE (x) ((* x x)) NIL) Это тоже замыкание, но, поскольку свободных переменных в теле лямбдавыражения нет, список переменных пуст.
Другой способ создания замыкания – конструкция FUNCTION: Вызов: (function Имя_ф-ции или лямбда-выражение) порождает замыкание, в которой сохраняются все переменные, свободные относительно тела функции или лямбда-выражения.
Рассмотрим функцию: (defun make-adder (x) (function (lambda (y) (+ x y)))) make-adder (setq 3+ (make-adder 3)) (CLOSURE (y) ((+ x y)) ((x 3))) Переменная x свободна относительно тела лямбда-выражения. Текущее значение x зафиксировано в замыкании.
Как вызвать функцию, с которой связана переменная 3+ ? Через funcall ! (funcall 3+ 6) 9
Поскольку конструкция function используется достаточно часто, удобно использовать краткую запись: (function НЕЧТО) то же самое, что: #’НЕЧТО
Итак, мы рассмотрели конструкции Лиспа, позволяющие использовать функции, как аргументы других функций и возвращать функции, как значения.
Две простенькие задачи 1) Дан список троек чисел вида: ((a b c) (d e f). . . ). Построить список сумм троек (a+b+c d+e+f. . . ) 2) Напишите генератор, возвращающий при очередном вызове число Фибоначчи.


