Оглавление 1. Функциональное программирование. Основы Лиспа. ............................................................... 2 Особенности функционального программирования .................................................................. 2 Основные понятия ......................................................................................................................... 3 Общее представление о языке Лисп............................................................................................. 7 2. Списки в Лиспе .............................................................................................................................. 9 Списки. Функции для работы со списками ................................................................................. 9 Управляющие структуры (предложения) .................................................................................. 12 Простая рекурсия ......................................................................................................................... 14 Другие виды рекурсии ................................................................................................................. 16 3. Функционалы................................................................................................................................ 19 Функционалы и отображения ..................................................................................................... 19 Применяющие функционалы ...................................................................................................... 20 Встроенные функционалы .......................................................................................................... 22 Лямбда выражения ....................................................................................................................... 23 Композиции функционалов, фильтры, редукции ..................................................................... 25 Выводы по применимости функционалов ................................................................................. 27 1 1. Функциональное программирование. Основы Лиспа. Особенности функционального программирования Функциональный стиль программирования сложился в практике решения задач символьной обработки данных в предположении, что любая информация для компьютерной обработки может быть сведена к символьной. Информация представляется символами, смысл которых может быть восстановлен по заранее известным правилам. Методы функционального программирования основаны на формальном математическом языке представления и преобразования формул. Функциональное программирование отличается от большинства подходов к программированию тремя важными принципами: 1) Природа данных Все данные представляются в форме символьных выражений. В Лиспе Дж. Мак-Карти назвал их S-выражениями. Состав S-выражений и типы их элементов не ограничиваются, что позволяет его использовать как древообразные структуры. Это позволяет локализовывать любые важные подвыражения. Система программирования над такими структурами обычно использует для их хранения всю доступную память, поэтому программист освобожден от распределения памяти под отдельные блоки данных. 2) Самоописание обработки символьных выражений Важная особенность функционального программирования состоит в том, что описание способов обработки S-выражений представляется программами, рассматриваемыми как символьные данные. Программы строятся из рекурсивных функций над S-выражениями. Определения и вызовы этих функций, как и любая информация, имеют вид S-выражений, то есть формально они могут обрабатываться как обычные данные, получаться в процессе вычислений и преобразовываться как значения. 3) Подобие машинным языкам Система функционального программирования допускает, что программа может интерпретировать и/или компилировать программы, представленные в виде S-выражений. Это сближает методы функционального программирования с методами низкоуровнего программирования и отличает от традиционной методики применения языков высокого уровня. Наиболее очевидные следствия из выбранных принципов: процесс разработки программ разбивается на две фазы: построение базиса и его пошаговое расширение; рассмотрение программы как реализации алгоритма естественно дополняется табличной реализацией функций, т.е. допустимо использование хранимых графиков функций в виде структур из аргументов и соответствующих результатов наряду с процедурами; прозрачность ссылок обеспечена совпадением значений одинаково выглядящих формул, вычисляемых в одинаковом контексте; нацеленность на универсальные функции и функционально полные системы, трудоемкость первичной реализации которых компенсируется надежностью определений и простотой применения. Функциональное программирование активно применяется для генерации программ и выполнения динамически конструируемых прототипов программ, а также для систем, применяемых в областях с низкой кратностью повторения отлаженных решений (например, в учебе, проектировании, творчестве и научных исследованиях), ориентированных на оперативные изменения, уточнения, улучшения, адаптацию и т.п. 2 Основные понятия Функцией называется правило, по которому каждому значению одного или нескольких аргументов ставится в соответствие конкретное значение результата. Способы определения правила и методов получения результата функции по заданному правилу при известных аргументах могут быть различны, например: Алгоритм (поиск наибольшего общего делителя). Таблица (сложение или умножение для целых чисел). Процесс (взвешивание или измерение). Устройство (вольтметр, термометр, часы) Формализованный текст (процедура, подпрограмма, макрос и т.п.). Различаются обозначения и определения соответствия между аргументами и результатами. Интуитивно понятие функции содержит концепцию времени: сначала вычисляются аргументы в порядке перечисления, затем строится значение функции - ее результат. Процессы обработки информации организуются как применение функций к их аргументам - вычисления. Концепция функции является одним из фундаментальных понятий математики. Чтобы задать функцию y=f(x), связывающую две переменные величины х и у, необходимо указать: 1) множество А значений, которые может принимать x (область определения функции), 2) множество В значений, которые может принимать v (область значения функции), 3) правило/(.у), по которому значениям x из А соотносятся значения v из В Каждому элементу из области определения однозначная функция ставит в соответствие а точности один элемент из области значения. Правило в простейшем случае можно изобразить с помощью графа или с помощью таблицы (рис. 1). Рис. 1 Чаще всего правило задается формулой, устанавливающей, какие вычислительные операции надо произвести над х, чтобы найти у: квадрат(х)=хг. В этом определении область определения и область значений функции даны неявно и не всегда очевидны. Например, функция квадрат отображает множество вещественных чисел во множество неотрицательных вещественных чисел, а целые числа в неотрицательные целые числа. Если есть элементы из области определения, для которых отображение посредством этой функции не определено, то функция называется частичной или частично определенной. Например, функция обратное(х)=1/х является частично определенной над множеством вещественных чисел. Функция, не являющаяся частичной, называется общей или всюду определенной. Функцию нескольких аргументов можно рассматривать как функцию одного аргумента (кортежа), множество значений которого является декартовым произведением множеств значений всех аргументов функции. Другая интерпретация функции нескольких аргументов заключается в том, что применение функции f к аргументам (а, b) рассматривается как выражение (f(a))(b), где результатом вызова функции f с аргументом а является новая функция с одним аргументом, которая затем вычисляется для аргумента b. 3 Некоторые функции на разных частях своей области определения задаются различными формулами, например: x, x y max( x, y ) y, x y Это правило удобнее записывать как: max(х,у) = если х>у то х, иначе у Условное предложение "если то иначе" позволяет выбрать одну из двух формул для вычисления результата функции в зависимости от некоторого условия. Часть предложения "иначе` не может быть опущена. При применении функции max к аргументам (1,3) вычисление по данному правилу происходит так. параметрам функции х и у сопоставляются значения 1 и 3, затем вычисляется значение выражения х>у (т.е. 1>3), что дает ложь, и в качестве результата функции возвращается значение у (т.е. 3). Таким образом, вызов функции макс(1 ,3) дает результат 3. Важнейшим приемом в функциональном программировании является композиция функций, позволяющая определять одни функции через другие, например: max3(x,y,z)= max(max(х,у),z). Результат вызова функции max(х,у) используется в качестве аргумента для функции max. Функциональная программа состоит из совокупности определений функций. Функции, в свою очередь, представляют собой вызовы других функций и предложения, управляющие последовательностью вызовов. Вычисления начинаются с вызова некоторой функции, которая, в свою очередь, вызывает функции, входящие в ее определение и т. д. в соответствии с иерархией определений и структурой условных предложений. Функции часто либо прямо, либо косвенно вызывают сами себя. В конечном итоге при вычислении функции должны ссылаться на основные базовые функции. Каждый вызов возвращает некоторое значение в вызвавшую его функцию, вычисление которой после этого продолжается; этот процесс повторяется до тех пор, пока запустившая вычисления функция не вернет конечный результат пользователю. Рассмотрим следующую программу на языке Си: double max(double x, if(x>=y) return x; else return y; } double у) { С помощью данной функции легко найти максимум из трех значений, используя композицию: m=max(max(a,b),c); Можно определить функцию max и другим способом: void max(double x, if(x>=y) *z=x; else *z=y; } double у, double *z) { Понятие функции при этом не нарушается, но результат возвращается через один из аргументов. В этом случае для вычисления максимума из трех чисел невозможно использовать композицию, результат может быть вычислен с помощью последовательности вызовов: max(a,b,&m); max(m,с,&m); 4 То же самое произойдет, если результат функции будет возвращаться через глобальную переменную. Такие особенности работы функции называются "побочными эффектами". Функция, не имеющая побочных эффектов, называется строгой. Строго функциональный язык не допускает побочных эффектов, не признает присваивание и передачу управления. Разветвление вычислений основано на механизме обработки частей условного предложения. Повторяющиеся вычисления осуществляются через рекурсию. Единственным "эффектом" выполнения любой функции является вычисленный результат. Так как нет присваивания, переменные, получив значение в начале блока вычислений, например, при вызове функции, больше никогда его не меняют. Таким образом, переменные - это просто сокращенная запись их значений, и на место переменных в программе всегда можно вставить выражения, по которым они вычисляются. Порядок проведения вычислений становится несущественным. Так как на результат вычислений не влияют никакие побочные эффекты, то неважно, когда вычислять этот результат. Кроме того, функциональный язык обладает двумя мощными механизмами, позволяющими писать более краткие и универсальные программы. Во-первых, аргументами или результатом функции могут быть сложные структуры данных. Однажды построенная структура не может изменяться, но может включаться в другие Во-вторых, можно определить функцию высшего порядка, аргументами или результатом которой является функции. Функция называется рекурсивной, если ее определение прямо или косвенно (через другие функции) содержит обращение к самой себе. Система программирования – это комплекс средств и методов, используемых при подготовке и применении программ на одном или нескольких языках программирования. Система программирования может быть задана как правило интерпретации или компиляции программ. Вычисление – процесс решения задачи, сводимой к обработке чисел, кодов или символов, рассматриваемых как модели реальных объектов. Соответствие между моделью и объектом часто называют интерпретацией. Список – основная структура данных в ФП. Список может быть пустым или содержать произвольное число объектов любой природы. Пустой список используется в качестве значения "ложь" - все, что отлично от пустого списка может выполнять роль значения "истина". Символ в ФП аналогичен переменной в традиционном языке программирования – это имя, состоящее из букв латиницы, цифр и некоторых специальных литер. Символ, как и переменная, может иметь какое-либо значение, то есть представлять какой-либо объект. Наряду с символами, в ФП используются также: числа (целые и вещественные); специальные константы t и nil, обозначающие логические значения true (истина) и false (ложь); списки. Символы, числа и специальные константы представляют простейшие объекты, на основе которых строятся все прочие объекты данных. Поэтому для них используется обобщающее название – атомы. Атомы являются элементарными S-выражениями (последовательность из букв и цифр, начинающаяся с буквы). Атомы могут иметь вид имен, чисел или других объектов, неделимых базовыми средствами языка. Атомы, выглядящие как имена, могут обладать свойствами, задаваемыми системой или программой. Значения переменных и определения функций – примеры свойств. 5 Истинностные значения – конечный набор различимых данных, используемых как характеристика логического высказывания, сравнения, успешности процесса, актуальности события, соответствия допустимым границам и т.п. Кроме списков имеются более общие структуры данных – символьные выражения (Sвыражения), реализуемые как двоичные деревья, а Лисп-системы поддерживают обработку различных специальных структур данных, таких как вектора, массивы, строки, хэш-таблицы, файлы, потоки ввода-вывода и др. Все вышеперечисленные объекты (атомы и списки) в совокупности называют символьными выражениями. Областью определения и областью значений для функций в функциональном программировании являются S-выражения. Отношения между различными символьными выражениями можно представить следующим образом: Символьные выражения Символы Атомы Числа t nil Списки Length=Le ngthT+1 2=1+1 Список из функции и перечня ее аргументов называется "форма" - синоним термина "выражение". Программа – это последовательность вычисляемых форм. Рекурсия – сведение к себе – позволяет такие правила записывать достаточно лаконично и ясно. Стек - набор данных, в котором элементы обрабатываются согласно дисциплине "Первым пришел – последним ушел." (англ. Stack пачка, стопка). Стек обеспечивает работу с рекурсивными функциями. Переменная – именованная часть памяти, предназначенная для многократного доступа к изменяющимся данным. Константа – именованная часть памяти, предназначенная для многократного доступа к фиксированным, не изменяющимся данным. Тип данных – множество данных с соответствующим ему набором допустимых операций. В языках программирования, ориентированных на компиляцию, принято переменные классифицировать по типам данных, а значения в памяти хранить без информации о типе данных. 6 Общее представление о языке Лисп Лисп появился как язык символьной обработки информации. К середине семидесятых годов на Лиспе решались наиболее сложные в практике программирования задачи из области дискретной и вычислительной математики, экспериментального программирования, лингвистики, химии, биологии, медицины и инженерного проектирования. На Лиспе реализована система AutoCAD - автоматизация инженерных расчетов, дизайна и комплектации изделий из доступных элементов, и Emacs – весьма популярный текстовый редактор в мире UNIX/Linux. Приверженцы Лиспа ценят его за элегантность, гибкость, а, главное, за способность к точному представлению программистских идей и удобство отладки. Методы программирования на Лиспе потребовали от авторов Лиспа большого числа нетрадиционных решений и соглашений, основа которых предложена и опробована Дж. Мак-Карти с его коллегами и учениками в определении этого языка (Lisp - list processing) и в первых реализациях Lisp 1.0 и Lisp 1.5. Наиболее общие из них признаны как принципы функционального программирования: 1. Унификация понятий <функция> и <значение> При символьном представлении информации нет принципиальной разницы в природе изображения значений и функций. Следовательно нет и препятствий для обработки представлений функций теми же средствами, какими обрабатываются значения, т.е. представления функций можно строить из их частей и даже вычислять по мере поступления и обработки информации. Так конструируют программы компиляторы. 2. Кроме функций-констант вполне допустимы функции-переменные Отсутствие навыков работы с функциональными переменными говорит лишь о том, что надо осваивать такую возможность, потенциал которой может превзойти наши ожидания теперь, когда программирование становится все более компонентно ориентированным. 3. Самоприменимость Первые реализации Лиспа были выполнены методом раскрутки, причем в составе системы сразу были предусмотрены и интерпретатор и компилятор. Оба эти инструмента были весьма точно описаны на самом Лиспе, причем основной объем описаний не превосходил пару страниц. 4. Интегральность ограничений на пространственно-временные характеристики Если не хватает памяти, то принципиально на всю задачу, а не на отдельные блоки данных, возможно мало существенных для ее решения. При недостатке памяти специальная программа "мусорщик" пытается найти свободную память. Новые реализации этого механизма рационально учитывают преимущества восходящих процессов на больших объемах памяти. 5. Уточняемость решений Реализация Лиспа содержит списки свойств объектов, приспособленные к внешнему доопределению отдельных элементов поведения программируемой системы. 6. Динамическое управление вычислениями и конструированием программ В стандартных языках программирования принята императивная организация вычислений по принципу немедленного и обязательного выполнения каждой очередной команды. Это не всегда оправдано и эффективно. Существует много неимперативных моделей управления процессами, позволяющих прерывать и откладывать процессы, а потом их восстанавливать и запускать или отменять, что обеспечено в Лиспе средствами конструирования функций, блокировки вычислений и их явного выполнения. 7 Многие реализационные находки Лиспа, такие как ссылочная организация памяти, "сборка мусора" - автоматизация повторного использования памяти, частичная компиляция программ с интерпретацией промежуточного кода, длительное хранение атрибутов объектов в период их использования и др., перекочевали из области исследований и экспериментов на базе Лиспа в практику реализации операционных систем и систем программирования. Абстрактный подход к представлению информации, лаконичный, универсальный стиль построения функций, ясность обстановки исполнения для разных категорий функций, свобода рекурсивных построений, доверие интуиции математика и исследователя, уклонение от бремени преждевременного решения непринципиальных проблем распределения памяти, отказ от необоснованных ограничений на область действия определений — все это увязано Джоном Мак-Карти в идее языка Лисп. Продуманность и методическая обоснованность первых реализаций Лиспа позволила быстро накопить опыт решения новых задач, подготовить их для прикладного и теоретического программирования. В настоящее время наблюдается устойчивый рост рейтинга интерпретируемых языков программирования и включение в компилируемые языки механизмов >символьной обработки и средств динамического анализа, что повышает интерес к Лиспу и функциональному программированию. Современные языки и технологии программирования унаследовали опыт реализации и применения Лиспа и других языков символьной обработки. Так, например, Java берет на вооружение идеи неполной компиляции и освобождения памяти, объектноориентированное программирование реализует объекты похоже на списки свойств атомов Лиспа, хэш-таблицы языка Perl созвучны по применению ассоциативным спискам Лиспа. Python обрабатывает программы с нетипизированными переменными. Наследие Лиспа в информатике достойно отдельного изложения. Существуют и активно применяются более трехсот диалектов Лиспа и родственных ему языков (Interlisp, muLisp, Clisp, Scheme, Ml, Cmucl, Logo, Hope, Sisal, Haskell, Miranda и т.д.) По современным меркам реализации Лиспа компактны и непритязательны к оборудованию. Существуют свободно распространяемые версии, занимающие менее Мегабайта, пригодные к применению на любом процессоре. 8 2. Списки в Лиспе Списки. Функции для работы со списками Список – это основной тип данных в Lisp. Списки - последовательности S-выражений, построенные из атомов, заключенных в скобки и разделенных пробелами, например: ((имя Джон Смит) (возраст 99) (женат #Т) (дети (Том Мэри))) Особым S-выражением является пустой список (), который считается атомом. Для обозначения пустого списка используется также специальная константа nil. Примеры списков: ;пятиэлементный список (1 2 3 4 5) ;четырехэлементный список (1 2 ((3) 4) 5) ;одноэлементный список ((1 2 3 4 5)) Первый элемент списка называется головой списка, все прочие элементы, кроме первого, представленные как список, называются хвостом списка. Примеры разделения списка на голову и хвост: Список (1 2 3 4 5) (1 2 ((3) 4) 5) ((1 2 3 4 5)) (1) Голова 1 1 (1 2 3 4 5) 1 Хвост (2 3 4 5) (2 ((3) 4) 5) () () Списки легко реализовать с помощью структуры из двух указателей (для примера выше): Для обработки S-выражений достаточно пяти функций: Функция CAR обеспечивает доступ к первому элементу списка — его "голове". `X (A B C) (A) ((A B)(C D)) (CAR X) A A (A B) 9 Функция CDR — возвращает укороченный на один элемент список (отбрасывая его первый элемент, «голову») — его «хвост», т.е. что, что остается после удаления головы. `X (A B C) (A) ((A B)(C D)) (CDR X) (B C) () ((C D)) Функция CONS соединяет два S-выражения в единое S-выражение, так что первым элементом результата является первый аргумент функции, а хвостом -второй аргумент. Она строит списки из бинарных узлов, заполняя их парами объектов, являющихся значениями пары ее аргументов. Первый аргумент произвольного вида размещается в левой части бинарного узла, а второй, являющийся списком, — в правой. (*) Чтобы отличать константные S-выражения от вычисляемых выражений, перед константой будем ставить апостроф (например, `х - символьная константа, а х – переменная). При сцеплении двух атомов cons(`A,`B) получается S-выражение, которое не является списком. Будем его записывать как точечную пару (А . B). Результат cons(`А,0) можно также записать либо как список (А) либо как точечную пару (А . ()). S-выражение (А . (B . (С . D))) можно записывать в более простой форме (A B C. D). `X A A (A B) (A B) A `Y (B C) () ((C D)) (C D) B (CONS X Y) (A B C) (A) ((A B) (C D)) ((A B) C D) (A . B) Функция ATOM позволяет различать составные и атомарные объекты: на атомах ее значение "истина", а на структурированных объектах — "ложь". `X (ATOM X) A (A) () 123 NIL T NIL T T T Функция EQ выполняет проверку атомарных объектов на равенство. Если оба аргумента функция являются списками, то результат не определен, так как анализируется только значение указателя. `X A (A) A `Y A (A) (A) (EQ X Y) T NIL NIL Для многошагового доступа к отдельным элементам списка удобно пользоваться мнемоничными обозначениями композиций из многократных CAR-CDR. Имена таких композиций устроены как цепочки из "a" или "d", задающие маршрут движения из шагов CAR и CDR, соответственно, расположенный между "c" и "r". Указанные таким способом CAR-CDR исполняются с ближайшего к аргументу шага, т.е. в порядке, обратном записи. 10 Примеры многошагового доступа к элементам структуры CAAR CADR CADDR CADADR Многократные CAR-CDR Вычисляются в порядке, обратном записи: ((A) B C) (A B C) (A B C) (A (B C) D) A B — CDR затем CAR C — (дважды CDR) затем CAR C — два раза (CDR затем CAR) (*) Имена функций car и car происходят от имен регистров компьютера, на котором был реализован первый компилятор языка LISP. Эти имена сохранились, так как позволяют использовать вместо car{cdr(cdr(x))) сокращенную запись caddr(x) (третий элемент списка х), см. примеры далее. Любой список может быть построен из пустого списка и атомов с помощью CONS, и любая его часть может быть выделена с помощью подходящей композиции CAR-CDR. Различие истинностных значений в Лиспе принято отождествлять с разницей между пустым списком и остальными объектами, которым программист может придать в программе некоторый другой смысл. Таким образом, значение "ложь" — это всегда Nil. (Во многих языках программирования используется 0 – 1 или идентификаторы True — False и др.) Если требуется явно изобразить истинностное значение, то для определенности в качестве значения "истина" используется константа — атом T (true) как стандартное представление, но роль такого значения может выполнить любой, отличный от пустого списка, объект. 11 Управляющие структуры (предложения) Для организации различных видов программ в Lisp`е служат разнообразные управляющие структуры, которые позволяют организовывать ветвление, циклы, последовательные вычисления. Перечислим их: 1. предложение let – служит для одновременного присваивания значений нескольким символам; 2. предло жения prog1, prog2, prong – используются для организации последовательных вычислений; 3. предложение cond – используется для организации ветвления, и, следовательно, очень важно для организации рекурсии; 4. предложение do – традиционный цикл. Рассмотрим перечисленные предложения подробнее. Предложение let служит для одновременного присваивания значений нескольким переменным. Формат предложения let: (let ((var_1 value_1) (var_2 value_2) … (var_n value_n)) form_1 form_2 … form_m) Работает предложение следующим образом: переменным var_1, var_2, … var_n присваиваются (параллельно!) значения value_1, value_2, … value_n, а затем вычисляются (последовательно!) значения форм form_1 form_2 … form_m. В качестве значения всего предложения let возвращается значение последней вычисленной формы form_m. (let ((x 3) (y 4)) (sqrt (+ (* x x) (* y y)))) ; 5.0 Так как присваивание значений переменным выполняется параллельно, то в следующем примере будет выдано сообщение об ошибке. (let ((x 3) (y (+ 1 x))) (sqrt (+ (* x x) (* y y)))) ; error: unbound variable - X После завершения выполнения предложения let переменные var_1, var_2, … var_n получают значения, которые они имели до использования в этом предложении, то есть предложение let выполняет локальные присваивания. (setq x `three) THREE (setq y `four) FOUR (let ((x 3) (y 4)) (sqrt (+ (* x x) (* y y)))) 5.0 x ; THREE y ; FOUR Для выполнения последовательных действий можно использовать предложения prog1, prog2, prong. Их общий формат: (prog* form_1 form_2 … form_n) Все три предложения работают одинаково, последовательно вычисляются значения форм form_1, form_2, …, form_n. Различие между предложениями проявляется только в тех значениях, которые они возвращают: предложение prog1 возвратит значение первой формы form_1, предложение prog2 возвратит значение второй формы form_2, а предложение progn 12 возвратит значение последней формы form_n. Во всем остальном эти предложения ничем не отличаются. > (prog1 (setq x 2) (setq x (* x 2)) (setq x (* x 2)) (setq x (* x 2))) 2 > (prog2 (setq x 2) (setq x (* x 2)) (setq x (* x 2)) (setq x (* x 2))) 4 > (progn (setq x 2) (setq x (* x 2)) (setq x (* x 2)) (setq x (* x 2))) 16 Предложение cond предназначено для организации ветвления (это предложение является аналогом оператора выбора – переключателя switch в языке C). Формат предложения cond выглядит следующим образом: (cond (predicate1 form1) (predicate2 form21 form22 … form2M) (predicate3) … (predicateN formN) ) При выполнении предложения cond последовательно вычисляются значения предикатов, обозначенных как predicate. Если предикат возвращает значение t, тогда вычисляется значение вычислимой формы form и полученное значение возвращается в качестве значения всего предложения cond. Другими словами, идет последовательный перебор предикатов до тех пор, пока не встретиться предикат, который будет истинен. Для некоторого предиката может отсутствовать вычислимая форма. В таком случае предложение cond возвратит значение самого предиката. Для некоторого предиката вычислимых форм может быть несколько. В таком случае формы будут вычислены последовательно, и значение последней формы будет возвращено как значение всего предложения cond. Пример для предложения cond – определение отрицательного, равного нулю или положительного числа (в этом примере предложения cond вложены одно в другое): (defun snumberp (num) (cond ((numberp num) (cond ((< num 0) `neg_number) ((= num 0) `zero) ((> num 0) `pos_number) )) (t `not_number) ) ) Результат работы программы: (snumberp 1) ;POS_NUMBER (snumberp -1) ;NEG_NUMBER (snumberp 0) ;ZERO (snumberp `a) ;NOT_NUMBER Как быть в случае, если ни один из предикатов predicate в предложении cond не вернет значение, отличное от nil? Тогда используется прием, как в последнем примере. В качестве последнего предиката predicate используется константа t, что гарантирует выход из предложения cond с помощью последней ветви (использование константы t в качестве предиката predicate аналогично использованию метки default в переключателе switch). 13 Предложение do, также как и предложение cond, является аналогом оператора цикла for в языке C. Формат предложения do: (do ((var_1 value_1) (var_2 value_2) … (var_n value_n)) (condition form_yes_1 form_yes_2 … form_yes_m) form_no_1 form_no_2 … form_yes_k ) Предложение do работает следующим образом: первоначально переменным var_1, var_2, …, var_n присваиваются значения value_1, value_2, …, value_n (параллельно, как в предложении let). Затем проверяется условие выхода из цикла condition. Если условие выполняется, последовательно вычисляются формы form_yes_1, form_yes_2, …, form_yes_m, и значение последней вычисленной формы form_yes_m возвращается в качестве значения всего предложения do. Если же условие condition не выполняется, последовательно вычисляются формы form_no_1, form_no_2, …, form_yes_k, и вновь выполняется переход в проверке условия выхода из цикла condition. Пример использования предложения do: для возведения x в степень n с помощью умножения определена функция power с двумя аргументами x и n: x – основание степени, n – показатель степени. (defun power (x n) (do ;присваивание начального значения переменной result ((result 1)) ;условие выхода их цикла ((= n 0) result) ;повторяющиеся действия (setq result (* result x)) (setq n (- n 1)))) ;POWER (power 2 3) ;8 Простая рекурсия Несмотря на то, что в языке Lisp есть предложение для организации циклических действий, все же основным методом решения задач остается метод с использованием рекурсии, то есть с применением рекурсивных функций. Функция является рекурсивной, если в ее определении содержится вызов этой же функции. Рекурсия является простой, если вызов функции встречается в некоторой ветви лишь один раз. Простой рекурсии в процедурном программировании соответствует обыкновенный цикл. Например, задача нахождения значения факториала n! сводится к нахождению значения факториала (n-1)! и умножения найденного значения на n. Пример: нахождение значения факториала n!. (defun factorial (n) (cond ;факториал 0! равен 1 ((= n 0) 1) ;факториал n! равен (n-1)!*n (t (* (factorial (- n 1)) n)))) ;FACTORIAL (factorial 3) ;6 Для отладки программы можно использовать возможности трассировки. Трассировка позволяет проследить процесс нахождения решения. 14 Для того чтобы включить трассировку можно воспользоваться функцией trace, например: (trace factorial) ;(FACTORIAL) (factorial 3) Entering: FACTORIAL, Argument list: (3) Entering: FACTORIAL, Argument list: (2) Entering: FACTORIAL, Argument list: (1) Entering: FACTORIAL, Argument list: (0) Exiting: FACTORIAL, Value: 1 Exiting: FACTORIAL, Value: 1 Exiting: FACTORIAL, Value: 2 Exiting: FACTORIAL, Value: 6 6 Для отключения трассировки можно воспользоваться функцией untrace: Например: (untrace factorial) ;NIL Можно говорить о двух видах рекурсии: рекурсии по значению и рекурсии по аргументу. Рекурсия по значению определяется в случае, если рекурсивный вызов является выражением, определяющим результат функции. Рекурсия по аргументу существует в функции, возвращаемое значение которой формирует некоторая нерекурсивная функция, в качестве аргумента которой используется рекурсивный вызов. Приведенный выше пример рекурсивной функции вычисления факториала является примером рекурсии по аргументу, так как возвращаемый результат формирует функция умножения, в качестве аргумента которой используется рекурсивный вызов. Вот несколько примеров простой рекурсии. Возведение числа x в степень n с помощью умножения (рекурсия по аргументу): (defun power (x n) (cond ;x0=1 (любое число в нулевой степени равно 1) ((= n 0) 1) ;xn=x(n-1)*n (значение x в степени n вычисляется возведением x в степень n-1 ;и умножением результата на n) (t (* (power x (- n 1)) x)))) (power 2 3) ;8 (power 10 0) ;1 Копирование списка (рекурсия по аргументу): > (defun copy_list (list) (cond ;копией пустого списка является пустой список ((null list) nil) ;копией непустого списка является список, полученный из головы и копии ;хвоста исходного списка (t (cons (car list) (copy_list (cdr list)))))) COPY_LIST >(copy_list `(1 2 3)) (1 2 3) >(copy_list ()) NIL 15 Определение принадлежности элемента списку (рекурсия по значению): (defun member (el list) (cond ;список просмотрен до конца, элемент не найден ((null list) nil) ;очередная голова списка равна искомому элементу, элемент найден ((equal el (car list)) t) ;если элемент не найден, продолжить его поиск в хвосте списка (t (member el (cdr list))))) ;MEMBER (member 2 `(1 2 3)) ;T (member 22 `(1 2 3)) ;NIL Соединение двух списков (рекурсия по аргументу): (defun append (list1 list2) (cond ;соединение пустого списка и непустого дает непустой список ((null list1) list2) ;соединить голову первого списка и хвост первого списка, ;соединенный со вторым списком (t (cons (car list1) (append (cdr list1) list2))))) ;APPEND (append ;(1 2 3 (append ;(1 2) (append ;(3 4) (append ;NIL `(1 2) `(3 4)) 4) `(1 2) ()) () `(3 4)) () ()) Реверс списка (рекурсия по аргументу): (defun reverse (list) (cond ;реверс пустого списка дает пустой список ((null list) nil) ;соединить реверсированный хвост списка и голову списка (t (append (reverse (cdr list)) (cons (car list) nil))))) ;REVERSE (reverse `(one two three)) ;(THREE TWO ONE) (reverse ()) ;NIL Другие виды рекурсии Рекурсию можно назвать простой, если в функции присутствует лишь один рекурсивный вызов. Такую рекурсию можно назвать еще рекурсией первого порядка. Но рекурсивный вызов может появляться в функции более, чем один раз. В таких случаях можно выделить следующие виды рекурсии: 1. параллельная рекурсия – тело определения функции function_1 содержит вызов некоторой функции function_2, несколько аргументов которой являются рекурсивными вызовами функции function_1. (defun function_1 … (function_2 … (function_1 …) … (function_1 …) … ) … ) 16 2. взаимная рекурсия – в теле определения функции function_1 вызывается некоторая функции function_2, которая, в свою очередь, содержит вызов функции function_1. (defun function_1 … (function_2 … ) … ) (defun function_2 … (function_1 … ) … ) 3. рекурсия более высокого порядка – в теле определения функции аргументом рекурсивного вызова является рекурсивный вызов. (defun function_1 … (function_1 … (function_1 …) … ) … ) Рассмотрим примеры параллельной рекурсии. В разделе, посвященном простой рекурсии, уже рассматривался пример копирования списка (функция copy_list), но эта функция не выполняет копирования элементов списка в случае, если они являются, в свою очередь также списками. Для записи функции, которая будет копировать список в глубину, придется воспользоваться параллельной рекурсией. (defun full_copy_list (list) (cond ;копией пустого списка является пустой список ((null list) nil) ;копией элемента-атома является элемент-атом ((atom list) list) ;копией непустого списка является список, полученный из копии головы ;и копии хвоста исходного списка (t (cons (full_copy_list (car list)) (full_copy_list (cdr list)))))) ;FULL_COPY_LIST (full_copy_list `(((1) 2) 3)) ;(((1) 2) 3) (full_copy_list ()) ;NIL Не обойтись без параллельной рекурсии при работе c бинарными деревьями. Бинарное дерево, как и все прочие данные, представляются в Lisp`е в виде списков. Например, упорядоченное бинарное дерево: 4 2 5 nil 1 nil 3 nil nil nil nil можно представить в виде списка (4 (2 (1 nil nil) (3 nil nil)) (5 nil nil)). Константы nil представляют пустые деревья. В таком представлении первый элемент списка – это узел дерева, второй элемент списка – левое поддерево и третий элемент списка – правое поддерево. Другой вариант представления дерева– (((nil 1 nil) 2 (nil 3 nil)) 4 (nil 5 nil)). В таком представлении первый элемент списка – это левое поддерево, второй элемент списка – узел дерева и третий элемент списка – правое поддерево. Можно использовать и другие варианты представления деревьев. Рассмотрим простой пример работы с бинарным деревом – обход дерева и подсчет числа узлов дерева. Для работы с элементами дерева, которые являются, по сути, элементами списка, очень удобно использовать стандартные функции Lisp`а, для получения первого, второго и третьего элементов списка – fist, second и third, соответственно. > (defun node_counter (tree) (cond ;число узлов пустого дерева равно 0 ((null tree) 0) ;число узлов непустого дерева складывается из: одного корня, ;числа узлов левого поддерева и числа узлов правого поддерева (t (+ 1 (node_counter (second tree)) (node_counter (third tree)))))) ;NODE_COUNTER (node_counter `(4 (2 (1 nil nil) (3 nil nil)) (5 nil nil))) ;5 17 Пример взаимной рекурсии – реверс списка. Так как рекурсия взаимная, в примере определены две функции: reverse и rearrange. Функция rearrange рекурсивна сама по себе. (defun reverse (list) (cond ((atom list) list) (t (rearrange list nil))))) (defun rearrange (list result) (cond ((null list) result) (t (rearrange (cdr list) (cons (reverse (car list)) result))))) (reverse `(((1 2 3) 4 5) 6 7)) ;(7 6 (5 4 (3 2 1))) Пример рекурсии более высокого порядка – второго. Классический пример функции с рекурсией второго порядка – функция Аккермана. Функция Аккермана определяется следующим образом: B (0, n) = n+1 B (m, 0) = B (m-1, 0) B (m, n) = B (m-1, B (m, n-1)) где m>=0 и n>=0. > (defun ackerman (cond ((= n 0) (+ n 1)) ((= m 0) (ackerman (- m 1) 1)) (t (ackerman (- m 1) (ackerman m (- n 1)))))) ACKERMAN > (ackerman 2 2) 7 > (ackerman 2 3) 9 > (ackerman 3 2) 29 18 3. Функционалы Функционалы и отображения Понятие функционала и отображения Отображения - ключевой механизм информатики. Построение любой информационной системы сопровождается определением и реализацией большого числа отображений. Сначала выбираются данные, с помощью которых представляется информация. В результате по данным можно восстановить представленную ими информацию - извлечь информацию из данных (по записи числа восстановить его величину). Потом конструируется набор структур, достаточный для размещения и обработки данных и программ в памяти компьютера (по коду команды можно выбрать хранимую в памяти подпрограмму, которая построит новые коды чисел или структур данных). Говорят, что отображение существует, если задана пара множеств и отображающая функция, для которой первое множество - область определения, а второе - область значения. При определении отображений, прежде всего, должны быть ясны следующие вопросы: что представляет собой отображающая функция; как организовано данное, представляющее отображаемое множество; каким способом выделяются элементы отображаемого множества, передаваемые в качестве аргументов отображающей функции. Это позволяет задать порядок перебора множества и метод передачи аргументов для вычисления отображающей функции. При обходе структуры, представляющей множество, отображающая функция будет применена к каждому элементу множества. Проще всего выработать структуру множества результатов, подобную исходной структуре. Но возможно не все полученные результаты нужны или требуется собрать их в иную структуру, поэтому целесообразно прояснить заранее еще ряд вопросов: где размещается множество полученных результатов; чем отличаются нужные результаты от полученных попутно; как строится итоговое данное из отобранных результатов. При функциональном стиле программирования ответ на каждый из таких вопросов может быть дан в виде отдельной функции, причем роль каждой функции в схеме реализации отображения четко фиксирована. Схема реализации отображения может быть представлена в виде определения, формальными параметрами которого являются обозначения функций, выполняющих эти роли. Такое определение называется функционалом. Более точно, функционал может оперировать функциями в качестве аргументов или результатов. Функции, выполняющие конкретные роли, могут быть достаточно общими, полезными при определении разных отображений, - они получают имена для многократного использования в разных системах определений. Но могут быть и разовыми, нужными лишь в данном конкретном случае - тогда можно обойтись без их имен, использовать определение непосредственно в точке вызова функции. Таким образом, определение отображения может быть разбито на части (функции и функционалы) разного назначения, типичного для многих схем информационной обработки. Это позволяет упрощать отладку систем определений, повышать коэффициент повторного использования отлаженных функций. Можно сказать, что отображения - эффективный механизм абстрагирования, моделирования, проектирования и формализации крупномасштабной обработки информации. Возможности отображений в информатике значительно шире, чем освоено практическим программированием, но их применение требует дополнительных пояснений, которые и являются предметом этой лекции. 19 Функционалы - это функции, которые используют в качестве аргументов или результатов другие функции. При построении функционалов переменные могут играть роль имен функций, определения которых находятся во внешних формулах, использующих функционалы. Рассмотрим технику использования функционалов на упражнениях с числами и покажем, как от простых задач перейти к более сложным. Пример: Построить список из <голов> элементов списка (defun 1st (xl) (cond ; "головы" элементов = CAR пока список не пуст (xl (cons (caar xl); выбираем CAR от его головы (1st (cdr xl)) ; и переходим к остальным, ) ) ) ) ; собирая результаты в список (1st`((один два)(one two)(1 2)) ) ; = (один one 1) Пример: Выяснить длины элементов списка (defun lens (xl) ; Длины элементов (cond ; Пока список не пуст (xl (cons (length (car xl)) ; вычисляем длину его головы (lens (cdr xl)); и переходим к остальным, ) ) ) ) ; собирая результаты в список (lens `((1 2)()(a b c d)(1(a b c d)3))) ; = (2 0 4 3) Внешние отличия в записи этих трех функций малосущественны, что позволяет ввести более общую функцию mapcar, в определении которой имена <car> и <length> могут быть заданы как значения параметра fn: (defun mapcar(fn xl) (cond ; Поэлементное преобразование XL с помощью функции FN пока XL не пуст (xl (cons(funcall fn (car xl)) ; применяем FN как функцию голове XL (mapcar fn (cdr xl)) ; и переходим к остальным, ) ) ) ) ; собирая результаты в список Эффект функций 1st и lens можно получить выражениями: ; (mapcar `car xl) ; (mapcar `length xl) ; "головы" элементов = CAR ; Длины элементов (mapcar `car `((один два)(one two)(1 2)) ) (mapcar `length `((1 2)()(a b c d)(1(a b c d)3)) ) ; = (один one 1) ; = (2 0 4 3) Оба примера можно решить с помощью таких определяющих выражений: (defun 1st(xl) (mapcar `car xl)) (defun lens(xl) (mapcar `length xl)) ; "головы" элементов = CAR ; Длины элементов Эти определения функций формально эквивалентны ранее приведенным - они сохраняют отношение между аргументами и результатами. Параметром функционала может быть любая вспомогательная функция. Применяющие функционалы Одним из видов функционалов, используемых в Лиспе являются применяющие функционалы. Они применяют функциональный аргумент к его параметрам. Так как применяющие 20 функционалы вычисляют значение функции, в этом смысле они аналогичны функции EVAL, вычисляющей значение выражения. Функционал APPLY Предположим мы хотим объединить в один список несколько вложенных списков, т.е. из ((a b c) (d e f) (k l)) получить (a b c d e f k l). Для этой задачи используем функцию apply, которая имеет два аргумента: имя функции и список, и применяет названную функцию к элементам списка, как к аргументам функции. Пример: Определим функцию, которая рассчитывает среднее списка чисел (defun list-mean (x) (/ (apply `+ x) (length x)) ) (list-mean `(1 2 3 4)) ; 2.5 Часто apply используют вместе c марсаr. Пример: найти общее число элемент в списках (defun countall (lis) (apply `+ (mapcar ` length lis)) ) (countall `((a b c) (d e f) (k l))) ; 8 Можно определить более сложную функцию countatom, которая считает элементы-атомы в любом списке. (defun countatom (lis) (cond ((null lis) 0) ((atom lis) 1) (t (apply `+ (mapcar `countatom lis))) ) ) (countatom `(a (a (b) c) (d) e (f g))) ; 8 Функционал FUNCALL Применяющий функционал FUNCALL аналогичен APPLY, но аргументы он принимает, не в списке, а по отдельности: (funcall fn x1 x2 ... xN) <=> (fn x1 x2 ... xN) Здесь fn - функция с n aргументами. (funcall '+ 1 2) ; <=> * (+ 1 2) ; 3 21 (funcall (car '(+ - / *)) 1 2) ; 3 Пример: Рассмотрим использование funcall для построения функции map2, которая действует аналогично mapcar, но берет в качестве аргументов два элемента из списка, а не один. Эта функция имеет вид: (defun map2 (f2 lst) (cond ((null lst) nil) (t (cons (funcall f2 (car lst) (cadr lst)) ; выполняем кнкатенацию результаты вызова в-ии f2 для головы и 2го элемента (map2 f2 (cddr lst))) ; с результатом рекурсивного вызова исходной ф-ии для «укороченного» хвоста ) ) ) (map2 'list '(A Christie V Nabokov K Vonnegut)) ; ((A Christie) (V Nabokov) (K Vonnegut)) Встроенные функционалы Отображающий функционал можно написать самим, а можно и воспользоваться одним из встроенных. Согласно стандарту, в базовую реализацию языка Лисп обычно включены функционалы: map, mapcar, maplist, mapcan, mapcon, mapc, mapl. Каждый из них покомпонентно обработает любой набор списков. Отличаются они схемами выбора аргументов для отображающей функции, характером воздействия на исходные данные и оформлением результатов, передаваемых объемлющим формулам. Map ( map result-type function sequences ... ) Функция function вызывается на всех первых элементах последовательностей, затем на всех вторых и т.д. Из полученных результатов function формируется результирующая последовательность, строение которой задается параметром result-type с допустимыми значениями cons, list, array, string, NIL. (map `list `+ `(1 2 3) `(4 5 6)) ; = (5 7 9) Mapcar ( mapcar function list ... ) Функция function применяется к первым элементам списков, затем ко вторым и т.д. Другими словами, function применяется к <головам> методично сокращающихся списков, и результаты применения собираются в результирующий список. (mapcar `list `(1 2 3) `(4 5 6)) ; = ((1 4)(2 5)(3 6)) 22 Maplist ( maplist function list ... ) Функционал аналогичен mapcar, но function применяется к <<хвостам>> списков list, начиная с полного списка. (maplist `list `(1 2 3) `(4 5 6)) ; = (((1 2 3) (4 5 6)) ((2 3) (5 6)) ((3) (6))) Mapc и Mapl Оба функционала работают как mapcar и maplist, соответственно, за исключением того, что они в качестве формального результата выдают первый список (своеобразная аналогия с формальными аргументами). (mapc `list `(1 2 3) `(4 5 6)) ; = (1 2 3) (mapl `list `(1 2 3) `(4 5 6)) ; = (1 2 3) Mapcan и Mapcon И эти два функционала аналогичны mapcar и maplist, но формирование результатов происходит не с помощью операции cons, которая строит данные в новых блоках памяти, а с помощью деструктивной функции nconc, которая при построении новых данных использует память исходных данных, из-за чего исходные данные могут быть искажены. (mapcan `list `(1 2 3 4)) ; (1 2 3 4) (mapcon `list `(1 2 3 4)) ; ((1 2 3 4) (2 3 4) (3 4) (4)) Map-into Функционал отображает результат в конкретную последовательность. (setq a (list 1 2 3 4) b (list 10 10 10 10)) ; (10 10 10 10) (map-into a `+ a b) ; (11 12 13 14) Лямбда выражения Структура МАР функций ограничивает формы отображаемых функций. Так, если мы желаем получить список с элементами x * x + 1 мы должны определить функцию (defun f1 (x) (+ 1 (* x x))) (mapcar `f1 `(1 2 3)) Таким образом определяется специальная функция, которая используется только в MAPCAR. Было бы удобнее вспомогательные определения вкладывать непосредственно в определения целевых функций и обходиться при этом вообще без имен. Конструктор функций lambda обеспечивает такой стиль построения определений. Этот конструктор любое выражение expr превращает в функцию с заданным списком аргументов (x1. .. xK) в форме так называемых lambda-выражений: (lambda (x1 ... xK) expr) Т.о., более эффективно в последнем случае использовать, т.н. лямбда выражения: (mapcar `(lambda (x) (+ 1 (* x x))) `(1 2 3)) 23 Имени такая лямбда-функция не имеет, поэтому может быть применена лишь непосредственно. DEFUN использует данный конструктор, но требует дать функциям имена. Т.о. лямбда-выражения позволяют определять функцию, не имеющую имени, внутри другой функции. Любую систему взаимосвязанных функций можно преобразовать к одной функции, используя вызовы безымянных функций. Общая форма записи лямбда-выражений: (lambda (параметры) <тело функции>) Пример: Пусть дана вспомогательная функция sqw, возводящая числа в квадрат (defun sqw (x)(* x x)) ; Возведение числа в квадрат (sqw 3) ; = 9 Построить список квадратов чисел, используя функцию sqw: (defun sqware (xl) ; Возведение списка чисел в квадрат (cond ; Пока аргумент не пуст, (xl (cons (sqw (car xl)) ; применяем sqw к его голове (sqware(cdr xl)) ; и переходим к остальным, ) ) ) ) ; собирая результаты в список (sqware`(1 2 5 7)) ; = (1 4 25 49 ) Можно использовать mapcar: (defun sqware (xl) (mapcar `sqw xl)) Ниже приведено определение функции sqware без вспомогательной функции, выполняющее умножение непосредственно. Оно влечет за собой двойное вычисление (CAR xl), т.е. такая техника не вполне эффективна: (defun sqware (xl) (cond (xl (cons (* (car xl) (car xl) ) ; квадрат головы списка ; голову вычислять приходится дважды (sqware (cdr xl)) ) ) ) ) Определение функции sqware – вычисляющей квадраты элементов числового списка, без использования имен и вспомогательных функций (оцените краткость!): (defun sqware (xl) (mapcar `(lambda (x) (* x x)) xl) ) 24 Композиции функционалов, фильтры, редукции Вызовы функционалов можно объединять в более сложные структуры таким же образом, как и вызовы обычных функций, а их композиции можно использовать в определениях новых функций. Композиции функционалов позволяют создавать и более мощные построения, достаточно ясные, но требующие некоторого внимания. С их помощью, можно применять серии функций к списку общих аргументов или к параллельно заданной последовательности списков их аргументов. Естественно, и серии, и последовательности представляются списками. Пример: Для заданного списка вычислим ряд его атрибутов, а именно - длина, первый элемент, остальные элементы списка без первого. (defun mapf (fl el) (cond ; Пока первый аргумент не пуст, (fl (cons (funcall (car fl) el) ; применяем очередную функцию ; ко второму аргументу (mapf (cdr fl) el) ; и переходим к остальным функциям, ) ) ) ) ; собирая их результаты в общий список (mapf `(length car cdr) `(a b c d)) ; = (4 a (b c d)) Пример: Декартово произведение хочется получить определением вида: (defun decart (x y) (mapcar `(lambda (i) (mapcar `(lambda (j) (list i j)) y ) ) x ) ) Но результат вызова (decart `(a s d) `(e r t)) дает (((A E) (A R) (A T)) ((S E) (S R) (S T)) ((D E) (D R) (D T))) вместо ожидаемого ((A E) (A R) (A T) (S E) (S R) (S T) (D E) (D R) (D T)) Дело в том, что функционал mapcar, собирает результаты отображающей функции в общий список с помощью операции cons так, что каждый результат функции образует отдельный элемент. А по смыслу задачи требуется, чтобы список был одноуровневым. Посмотрим, что получится, если вместо cons при сборе результатов воспользоваться функцией append. Пусть дан список списков. Нужно их все сцепить в один общий список. (defun list-ap (ll) (cond (ll (append (car ll) (list-ap (cdr ll)) ) ) ) ) 25 (list-ap `((1 2)(3 (4)))) ; = (1 2 3 (4)) Тогда по аналогии можно построить определение функционала map-ap: (defun map-ap (fn ll) (cond (ll (append (funcall fn (car ll) ) (map-ap fn (cdr ll) ) ) ) ) ) (map-ap `cdr `((1 2 3 4) (2 4 6 8) (3 6 9 12))) ; = (2 3 4 4 6 8 6 9 12) Следовательно, интересующая нас форма результата может быть получена: (defun decart(x y) (map-ap `(lambda(i) (mapcar `(lambda(j)(list i j)) y) ) x ) ) (decart `(a s d) `(e r t)) ; = ((A E)(A R)(A T)(S E)(S R)(S T)(D E)(D R)(D T)) Сцепление результатов отображения с помощью append обладает еще одним полезным свойством: при таком сцеплении исчезают вхождения пустых списков в результат. А в Лиспе пустой список используется как ложное значение, следовательно, такая схема отображения пригодна для организации фильтров. Фильтр отличается от обычного отображения тем, что окончательно собирает не все результаты, а лишь удовлетворяющие заданному предикату. Пример: Построить список голов непустых списков: ; временно голова размещается в список, чтобы потом списки сцепить (defun heads (xl) (map-ap `(lambda (x) (cond (x (cons (car x) NIL) ) ) ) xl ) ) (heads `((1 2) () (3 4) () (5 6)) ) ; = (1 3 5) Рассмотрим еще один типичный вариант применения функционалов. Представим, что нас интересуют некие интегральные характеристики результатов, полученных при отображении, например, сумма полученных чисел, наименьшее или наибольшее из них и т.п. В таком случае говорят о свертке результата или его редукции. Редукция заключается в сведении множества элементов к одному элементу, в вычислении которого задействованы все элементы множества. Пример: Подсчитать сумму элементов заданного списка. 26 (defun sum-el (xl) (cond ((null xl) 0 ) (xl (+ (car xl) (sum-el (cdr xl) ) ) ) ) ) (sum-el `(1 2 3 4) ) ; = 10 Перестроим такое определение, чтобы вместо <+> можно было использовать произвольную бинарную функцию: (defun red-el (fn xl) (cond ((null xl) 0 ) (xl (funcall fn (car xl) (red-el fn (cdr xl)) ) ) ) ) (red-el `+ `(1 2 3 4) ) ; = 10 В какой-то мере map-ap ведет себя как свертка - она сцепляет результаты без сохранения границ между ними. Такие формулы удобны при моделировании множеств, графов и металингвистических формул, а к их обработке сводится широкий класс задач не только в информатике. Выводы по применимости функционалов В общем случае, отображающие функционалы представляют собой различные виды структурной итерации или итерации по структуре данных. При решении сложных задач полезно использовать отображения и их композиции, а также иметь в виду возможность создания своих функционалов. Показанные построения достаточно разнообразны, чтобы можно было сформулировать, в чем преимущества применения функционалов: отображающие функционалы позволяют строить программы из крупных действий; функционалы обеспечивают гибкость отображений; определение функции может совсем не зависеть от конкретных имен; с помощью функционалов можно управлять выбором формы результатов; параметром функционала может быть любая функция, преобразующая элементы структуры; функционалы позволяют формировать серии функций от общих данных; встроенные в Лисп базовые функционалы приспособлены к покомпонентной обработке произвольного числа параметров; любую систему взаимосвязанных функций можно преобразовать к одной функции, используя вызовы безымянных функций. 27