Logo GenDocs.ru

Поиск по сайту:  


Загрузка...

Городняя Л.B. Основы функционального программирования - файл funkc-prog.rtf


Городняя Л.B. Основы функционального программирования
скачать (11684 kb.)

Доступные файлы (1):

funkc-prog.rtf11684kb.07.04.2011 20:20скачать

содержание
Загрузка...

funkc-prog.rtf

  1   2   3   4   5   6   7   8   9   10
Реклама MarketGid:
Загрузка...
Основы функционального программирования

Учебное пособие


Л.В.Городняя
Новосибирск, 2004

Содержание лекций стр
1. Основные идеи 3

2. Элементарный Лисп 11

3. Интерпретатор 25

4. Функционалы 40

5. Имена и контексты 52

6. Свойства атомов 60

7. Детализация определений 80

8. Компиляция программ 87

9. Реализационные детали 96

10. От ФП к ООП 104

11. Недетерминизм 115

12. Управление процессами 122

13. Функции высших порядков 130

14. Макетирование и тесты 142

15. Парадигмы программирования 147

Литература 165

Учебное пособие разработано при поддержке Интернет-Университета Информационных технологий и опубликовано в серии «Основы информационных технологий» в 2004 году.
Курс разработан на базе Факультета информационных технологий Новосибирского госуниверситета.
Содержание курса соответствует PF1, PL7, PL3 классификатора CC2001CS.

Лекция 1. ^ Основные идеи
В этой лекции дается общее представление о функциональном программировании и сфере его применения, включая анализ основных понятий и принципов и их иллюстрацию на материале истории языка Лисп, его диалектов, наследников и реализаций. Рассматривается роль функциональных программ в жизненном цикле программного обеспечения и эволюции информационных технологий, а также перспективы функционального подхода к совершенствованию информационных cистем.
Идея функционального программирования опирается на интуитивное представление о функциях как о достаточно общем механизме представления и анализа решений сложных задач. Механизм функций основательно изучен математиками, и это позволяет программистам наследовать выверенные построения, обладающие предельно высокой моделирующей силой. Систематическое применение функционального программирования впервые достаточно ярко было продемонстрировано Джоном Мак-Карти и его учениками в методах реализации языка Лисп и программирования на этом языке [1]. Наиболее очевидные из этих методов были успешно ассимилированы другими языками и системами программирования. Обычно про функциональное программирование вспоминают при смене технологий, когда возрастает роль

аналитики и исследовательских задач. В настоящее время часто употребляют термин

<функциональность> при сравнительной характеристике информационных систем, что, видимо, свидетельствует о проявлении новой метрики, заслуживающей отдельного рассмотрения [2].
Функциональный стиль объединяет разные подходы к определению процессов вычисления на основе достаточно строгих абстрактных понятий и методов символьной обработки данных. Связь функционального программирования с математическими основами позволяет в тексте программы наследовать доказательность построения результата - если она достигнута, причем с использованием разных методов абстрагирования решаемой задачи [3,4].
Сложность решения задач с помощью функциональных определений преодолевается чисто алгебраически. Это позволяет представлять классы задач и их решений строгими формулами, для наглядности упрощаемыми введением дополнительных функциональных символов. Активно используется рекурсия и символьные обозначения как данных, так и действий и любых формул, удобных при определении функций. Минимальный набор обозначений, к которым можно свести все правильные, т.е. вычислимые формулы системы, играет роль базиса системы, реализация которого является минимальной версией всей системы.
Основная трудность перехода к функциональному программированию - соблазн легкого пути, т.е. стремление быстро смоделировать привычные средства и методы программирования. Более надежный путь - исследовать функциональное программирование как незнакомый мир. Идеи функционального программирования легче воспринять как самостоятельную теорию или интеллектуальную игру, которая новыми путями непременно приведет к знакомым и интересным задачам, но обеспечит

преимущество - изящные решения и глубину понимания.
Джон Мак-Карти предложил рассматривать функции как общее базовое понятие, к которому достаточно естественно могут быть сведены все другие понятия, возникающие при программировании [1]. Идеи языка Лисп вызвали не утихающие по сей день дискуссии о приоритетах в программировании и сущности программирования. Лисп послужил эффективным инструментом экспериментальной поддержки теории программирования и развития сферы его применения. Рост интереса к Лиспу коррелирует с улучшением элементной базы, повышением эксплуатационных характеристик оборудования и появлением новых сфер применения ИТ. Существует и активно применяется более трехсот диалектов Лиспа и родственных ему языков: Interlisp, muLisp, Clisp, Sheame, ML, Cmucl, Logo, Hope, Sisal, Haskell, Miranda и др.
Изучение функционального программирования начинается с овладения техникой работы с так называемыми <чистыми>, строго математическими, идеальными функциями. Для реализации функций характерен отказ от необоснованного использования присваиваний и низкоуровневого управления вычислениями в терминах передачи управления. Такие функции удобны при отладке и тестировании благодаря независимости от контекста описания и предпочтения явно выделенного чистого результата. Трудоемкость отладки композиций из хорошо определенных функций растет аддитивно, а не мультипликативно.
Кроме того, системы из таких функций могут развиваться в любом направлении: сверху вниз и снизу вверх (а также “вширь” и “вузь”, если понадобится)[С.С.Лавров. МПСС-84]. Можно быстро продвинуться по сложности решаемой задачи, не отвлекаясь на синтаксическое разнообразие и коллизии при обработке общих данных. Концептуально близкие идеи <структурированного программирования> были сформулированы лишь более чем через десять лет.
Особый интерес представляют рекурсивные функции и методы их реализации в системах

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

решения. Функциональное программирование для поддержки этой фазы предлагает ряд обобщенных функций: в качестве результата функции допускаются варианты значений, равноправно выбираемые из конечного множества значений. Равноправие не распространяется лишь на тупиковую ситуацию, когда ни один предложенный вариант не может быть вычислен.
Существует много неимперативных моделей управления процессами, позволяющих прерывать и откладывать процессы, а потом восстанавливать их и запускать или отменять. Организация такого управления, достаточного для оптимизации и программирования параллельных процессов, реализуется с помощью так называемых <замедленных> или <ленивых> вычислений (lazy evaluation). Основная идея таких вычислений заключается в сведении вызовов функций к представлению рецептов их вычисления в определенном контексте. Вычисляться каждый такой рецепт может не более чем один раз и то если его результат действительно нужен.
Здание функционального программирования получает логическое завершение на уровне

определения функций высших порядков, удобных для синтаксически управляемого

конструирования программ на основе спецификаций, типов данных, визуальных диаграмм, формул и т.п.
Известно, что лаконичность рекурсии может скрывать нелегкий путь. А.П.Ершов в предисловии к книге П.Хендерсона [3] привел поучительный пример не поддавшегося А.Чёрчу решения задачи о рекурсивной формуле, сводящей вычитание единицы из натурального числа к прибавлению единицы {1 -1 = 0 | ( n +1 ) -1 = n }. Оно получено С.Клини лишь в 1932 году:
n –1 = F (n, 0, 0), где F (x, y, z) = если (x = 1) то 0

иначе если ((y +1) = x) то z

иначе F (x, y +1, z +1)
Решение получилось через введение формально усложненной функции F со вспомогательными аргументами, что противоречит интуитивному стремлению к монотонности и движению от простого к сложному.
История Лиспа насыщена жаркими спорами, притиворечивыми суждениями, яркими

достижениями и смелыми изобретениями, которых более чем достаточно, чтобы утверждать: «Лисп - гениальное творение Джона МакКарти». Потенциал данного языка еще предстоит раскрыть. Выразительная сила Лиспа обретает новое дыхание на каждом эволюционном витке развития информационных технологий. При сравнительном анализе информационных систем моделирование их семантики на Лиспе позволяет классифицировать функционирование по уровню сложности, зрелости, полноты, точности и организованности. Универсальность Лиспа достаточна для изучения на его основе любой парадигмы информатики и программотехники. Лисп содержит в себе эталонную семантическую систему, пригодную для измерения функциональности других систем.
Информационный мир становится все более динамичным - Лисп приспособлен к

программированию развивающихся построений и реорганизуемых конфигураций из

разносортных компонентов. Многие реализационные находки Лиспа, такие как ссылочная

организация памяти, “сборка мусора” для повторного использования памяти, частичная

компиляция программ с интерпретацией промежуточного кода, полиморфизм, длительное

хранение атрибутов объектов в период их использования и т.д. перекочевали из области

исследований и экспериментов на базе Лиспа в практику реализации систем программирования.
Диалекты Лиспа (Logo, ML, MuLisp, Scheme, Hope, AutoLisp, CommonLisp, Reduce и др.) заняли обширную нишу в области учебно-экспериментального программирования, связанного с развитием теории программирования, системного программирования, разработки и прототипирования новых компьютерных комплексов и архитектур, конструирования и исследования систем построения оптимизирующих компиляторов и организации особо точных и высокопроизводительных вычислений.
На первый взгляд, идеи Лиспа противоречат традиционным подходам к программированию. Но это противоречие отступает перед строгой логикой языка, гармонично уравновешенной полнотой и ясностью реализационных решений. Определение Лиспа дает надежную основу для развития, варьирования и расширения Лисп-систем средствами как самого Лиспа, так и его окружения.
^ Базис Лиспа предельно лаконичен - атомы и структуры из простейших бинарных узлов плюс несколько базовых функций и функционалов. Базис содержит встроенные (примитивные) функции, которые анализируют, строят и разбирают любые структурные значения (atom, eq, cons, car, cdr,), и встроенные специальные функции и функционалы, которые управляют обработкой структур, представляющих вычисляемые выражения (quote, cond, lambda, eval). Над базисом строятся предельно простые формулы в виде круглоскобочных списков, где первый элемент - функция, остальные - ее аргументы, в том числе переменные, реализуемые с помощью разных вариантов стека или ассоциативного списка. Подробнее с идеями Лиспа и его математическими основами можно ознакомиться на страницах журнала <Компьютерные инструменты в бразовании>, N 2-5 за 2002 год.
Синтаксис Лиспа не требует особых ресурсов для запоминания разделителей и/или ограничителей и напряженного внимания на распознавание синтаксических позиций в разных рамочных конструкциях. Универсальный разделитель - пробел, ограничители - круглые скобки. В скобки заключается представление функции с ее аргументами. Все остальное - вариации в зависимости от категории функций, определенности атомов и вычислимости выражений, типов значений и структур данных. Функционалы - это одна из категорий функций, используемая при организации управления вычислениями.
По современным меркам реализации Лиспа компактны и не слишком требовательны к оборудованию. Существуют свободно распространяемые версии, занимающие менее мегабайта, пригодные к применению на любом процессоре.
В нашей стране программирование соприкоснулось с Лиспом из первых рук. Джон Мак-Карти в конце 1968 года познакомил Москву и Новосибирск с Лиспом, что побудило к реализации отечественных версий языка. Две реализации на БЭСМ-6 (ВЦ АН под рук. С. С. Лаврова и ВЦ СО АН под руководством А. П. Ершова) и одна на ЕС ЭВМ (ВЦ АН под рук. С. С. Лаврова) нашли применение в отечественных проектах по системному и теоретическому программированию, в исследованиях по математической лингвистике, искусственному интеллекту и обработке химических формул.
В настоящее время наблюдается устойчивый рост рейтинга интерпретируемых языков

программирования и включения в компилируемые языки механизмов символьной обработки и средств динамического анализа, что повышает интерес к Лиспу.
История создания и развития языка программирования ЛИСП интересна как социальный феномен осуществления замысла, вызвавшего серьезные возражения и математиков, и программистов, но показавшего удивительную живучесть. Критика теоретиков была связана с так называемой "парадоксальностью" бестипового лямбда-исчисления. Практиков пугали накладные расходы на сборку мусора и интерпретацию в сравнении с компиляцией и статическим распределением памяти. (О наличии компилятора в традиционных Лисп-системах вспоминают редко.)
Наиболее общие принципы функционального программирования:
1)^ Унификация понятий <функция> и <значение>.
При символьном представлении информации нет принципиальной разницы в природе

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

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

представления функций можно строить из их частей и даже вычислять по мере поступления и обработки информации. Именно так конструируют программы компиляторы. В замкнутых системах не принято к такой технике информационных воздействий допускать обычных пользователей. Но исследователь вынужден вникать во все уровни своего экспериментального полигона.
2) Кроме функций-констант, вполне допустимы функции-переменные.
Отсутствие навыков работы с функциональными переменными говорит лишь о том, что надо осваивать такую возможность, потенциал которой может превзойти наши ожидания теперь, когда программирование становится все более аспектно и компонентно ориентированным. Объектно-ориентированная парадигма в этом плане не обладает достаточной функциональной полнотой.
3) Самоприменимость.
Первые реализации Лиспа были выполнены методом раскрутки, причем в составе системы сразу были предусмотрены и интерпретатор, и компилятор. Оба инструмента были весьма точно описаны на самом Лиспе, причем основной объем описаний не превосходил пару страниц, что позволяло активно использовать эти описания при изучении языка и программ, написанных на нем. Этот эксперимент послужил базой для определения систем программирования с помощью так называемой <операционной семантики>, получившей развитие в работах по Венской методике определения языков и систем программирования.
4) ^ Интегральность ограничений на пространственно-временные характеристики.
Если не хватает памяти, то принципиально на всю задачу, а не на отдельные блоки данных, возможно, не слишком существенных для ее решения. При недостатке памяти специальная программа- “мусорщик” пытается найти свободную память. Новые реализации этого механизма рационально учитывают преимущества восходящих процессов на больших объемах памяти.
5) ^ Уточняемость решений.
Современное применение информационных систем достаточно широко соприкасается с

вариантами доступных решений, необходимостью уточнять отдельные особенности

применения готовых решений и трудоемкостью анализа и поиска рационального выбора комплекта используемых средств. Реализация Лиспа обычно содержит списки свойств объектов, приспособленные к внешнему доопределению отдельных элементов поведения

программируемой системы.
6) ^ Множественность определений.
Наиболее концептуально полный Lisp 1.5 допускает множественные определения имен, что в рамках настраиваемой интерпретации обеспечивает кроме общеизвестного полиморфизма более общие схемы обработки ряда версий или вариантов функциональных построений.
Как авиация не соревнуется с автотранспортом в объеме грузоперевозок, так и функциональное программирование несравнимо со стандартными парадигмами в массовости применения, но это не умаляет его достоинств.
Многие современные языки и технологии программирования унаследовали опыт

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

Java берет на вооружение идеи неполной компиляции и освобождения памяти, объектно-

ориентированное программирование реализует объекты, весьма похожие на списки свойств атомов Лиспа, хэш-таблицы языка Perl напоминают применение ассоциативных списков Лиспа. Python обрабатывает программы с нетипизированными переменными.

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

Изучается ключевой метод функционального программирования – выбор семантического базиса для класса решаемых задач на примере организации информационной обработки символьными выражениями в языке Лисп. Вводятся базовые понятия, достаточные для символьного представления программ. Формализм рекурсивных функций и простые алгоритмы символьной обработки привлечены для обоснования и демонстрации функционального подхода к представлению программ. Анализируются требования к полноте и эффективности их обработки. В качестве исторической иллюстрации полномасштабного применения функционального программирования для решения достаточно сложной задачи используется символика языка Лисп, выбранная при организации символьной обработки для решения задач искусственного интеллекта [1].
Функциональный стиль программирования сложился в практике решения задач символьной обработки данных в предположении, что любая информация для компьютерной обработки может быть сведена к символьной.

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

Функциональное программирование отличается от большинства подходов к программированию тремя важными принципами:

^ 1) Природа данных
Все данные представляются в форме символьных выражений. Дж. Мак-Карти назвал их S-выражениями. Состав S-выражений и типы их элементов не ограничиваются. Это позволяет c их помощью представлять любую информацию. Система программирования над такими структурами обычно использует для их хранения всю доступную память, поэтому программист освобожден от распределения памяти под отдельные блоки данных.

^ 2) Самоописание обработки символьных выражений
Важная особенность функционального программирования состоит в том, что описание способов обработки S-выражений представляется программами, рассматриваемыми как символьные данные. Программы строятся из рекурсивных функций над S-выражениями. Определения и вызовы этих функций, как и любая информация, имеют вид S-выражений, то есть формально они могут обрабатываться как обычные данные, получаться в процессе вычислений и преобразовываться как значения.

^ 3) Подобие машинным языкам
Система функционального программирования допускает, что программа может интерпретировать и/или компилировать программы, представленные в виде S-выражений. Это сближает методы функционального программирования с методами низкоуровнего программирования и отличает от традиционной методики применения языков высокого уровня. В принципе, такая возможность достижима на любом стандартном языке, но так делать не принято.

Наиболее очевидные следствия из выбранных принципов:

  • Процесс разработки программ разбивается на две фазы: построение базиса и его пошаговое расширение.



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



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



  • Функциональное программирование активно применяется для генерации программ и систем, применяемых в областях с низкой кратностью повторения отлаженных решений (например, в учебе, проектировании, творчестве и научных исследованиях), ориентированных на оперативные изменения, уточнения, улучшения, адаптацию и т.п.


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

A

ATOM

ЛЕКЦИЯ2

ВотДлинныйАтомОченьДлинныйЕслиНадоАтомМожетБытьЕщеДлинннее

Ф4-длш139_к131б
Одинаково выглядящие атомы не различимы по своим свойствам, хотя проявления этих свойств могут быть обусловлены контекстом использования атомов. Термин “атом” выбран по аналогии с химическими атомами. Согласно этой аналогии атом может иметь достаточно сложное строение, но оно не рассматривается как обычное S-выражение. Устройство атома реализационно зависимо и лишь соответствует некоторой спецификации, содержание которой будет рассмотрено в девятой лекции. Атом не предназначен для разбора на части базовыми средствами языка.

Более сложные данные выстраиваются из унифицированных структур данных - одинаково устроенных блоков памяти. В Лиспе это бинарные узлы, содержащие пары объектов произвольного вида. Каждый бинарный узел соответствует минимальному блоку памяти, выделяемому системой программирования при организации и обработке структур данных. Выделение блока памяти и размещение в нем пары данных выполняет функция CONS (от слова consolidation), а извлечение левой и правой частей из блока выполняют функции CAR и CDR, соответственно.

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

CONS => [ CAR | CDR ]

Бинарный узел, содержащий пару атомов ATOM и^ Nil, рассматривается как

одноэлементный списо:
( ATOM ) = [ ATOM | Nil ]

Если вместо атома ATOM рекурсивно подставлять произвольные атомы, а вместо

Nil - произвольные списки, затем вместо ATOM - построенные списки и так далее,

то мы получим множество всех возможных списков. Атом Nil играет роль пустого списка и фактически эквивалентен ему. Можно сказать, что список - это заключенная в скобки последовательность из атомов, разделенных пробелами, или списков.
ATOM

(A B)

(A B C D E F G H I J K L M N O P R S T U V W X Y Z)

(C (A B))

((A B) C)

((A B) (D C))

((A B)(D(C E)))
Такая форма представления информации называется списочной записью (list-notation). Ее основные достоинства - лаконичность, удобство набора и отсутствие “синтаксического сахара”. Она достаточно отражает взаимосвязи структур данных, размещаемых в памяти, и помогает задавать процедуры доступа к их компонентам.

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

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

Функция CAR обеспечивает доступ к первому элементу списка - его “голове”, а функция CDR - к укороченному на один элемент списку - его “хвосту”, т.е. к тому, что остается после удаления головы.

Функция^ ATOM позволяет различать составны и атомарные объекты: на атомах ее значение “истина”, а на структурированных объектах – “ложь”.

Функция EQ выполняет проверку атомарных объектов на равенство.

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

*) Примечание. Во многих ЯП испльзуется 0 – 1 или идентификаторы True – False и др.

Если требуется явно изобразить истинностное значение, то для определенности в качестве значения “истина” используется константа – атом T (true) как стандартное представление, но роль такого значения может выполнить любой, отличный от пустого списка, объект.

^ Таблица 2.1. Операции над списками. Примеры соответствия между аргументами и результатами элементарных функций обработки списков .

Функция

Аргументы

Результат




^ Конструирование структур данных




CONS

A и Nil

(A )

CONS

(A B) и Nil

((A B) )

CONS

CONS

A и (B)

(Результат предыдущего CONS) и ©

(A B)

((A B) C)

CONS

A и (B C)

(A B C)













^ Доступ к компонентам структуры данных:







Слева




CAR

(A B C)

A

CAR

(A (B C))

A

CAR

((A B) C)

(A B)

CAR

A

Не определен




Справа




CDR

(A )

Nil

CDR

(A B C D)

(B C D)

CDR

(A (B C))

((B C))

CDR

((A B) C)

©

CDR

A

Не определен




^ Смешаная обработка данных:




CDR

CAR

(A B C)

Результат предыдущего CDR

(B C)

B

CAR

CAR

(A C)

Результат предыдущего CAR

A

Не определен

CONS

CAR

A и (B)

Результат предыдущего CONS

(A B)

A

CONS

CDR

A и (B)

Результат предыдущего CONS

(A B)

(B)




Предикаты:







^ Атомарность – неделимость




ATOM

VeryLongStringOfLetters

T

ATOM

( A B )

Nil - выполняет роль

ложного значения

CDR

ATOM

( A B )

Результат предыдущего CDR

(B)

Nil

ATOM

Nil

T

ATOM

( )

T




Равенство




EQ

A A

T

EQ

A B

Nil

EQ

A (A B)

Nil

EQ

(A B) (A B)

Не определен

EQ

Nil и ()

T


2.3. Точечная нотация
Исторически при реализации Лиспа в качестве единой базовой структуры для конструирования S-выражений использовалась так называемая “точечная нотация” (dot-nоtation), согласно которой левая и правая части бинарного узла равноправны и могут хранить данные любой природы.

Бинарный узел, содержащий пару атомов ATOM1 и ATOM2, можно представить в виде S-выражения вида:

( ATOM1 . ATOM2 )

Если вместо атомов ATOM1, ATOM2 рекурсивно подставлять произвольные атомы, затем построенные из них пары и так далее, то получим множество всех возможных S-выражений.

^ Можно сказать, что S-выражение - это или атом или заключенная в скобки пара из двух С-

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

ATOM

(A . B)

(C . (A . B))

((A . B) . C)

((A . B) . (D . C))

((A . B) . (D . (C . E)))
  1   2   3   4   5   6   7   8   9   10



Скачать файл (11684 kb.)

Поиск по сайту:  

© gendocs.ru
При копировании укажите ссылку.
обратиться к администрации
Рейтинг@Mail.ru