Практика функционального...

60
ISSN 2075-8456

Transcript of Практика функционального...

Page 1: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

ISSN 2075-8456

9 772075 845008

Page 2: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

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

Последняя ревизия этого выпуска журнала, а также другие выпуски могут быть загружены с сайта fprog.ru.

Журнал «Практика функционального программирования»Авторы статей: Александр Самойлович

Алексей ОттВлад БалинДмитрий АстаповДмитрий ЗуйковРоман ДушкинСергей Зефиров

Редактор: Лев Валкин

Корректор: Ольга Боброва

Иллюстрации: Обложка© UN Photo/Andrea Brizzi© iStockPhoto/sx70

Шрифты: ТекстMinion Pro © Adobe Systems Inc.ОбложкаDays © Александр Калачёв, Алексей МасловCuprum © Jovanny LemonadИллюстрацииGOST type A, GOST type B © ЗАО «АСКОН», используются с разрешенияправообладателя

Ревизия: 710 (2009-11-17)

Сайт журнала: http://fprog.ru/

Свидетельство о регистрации СМИЭл № ФС77–37373 от 03 сентября 2009 г.

Журнал «Практика функционального программирования» распространяется в со-ответствии с условиями Creative Commons Attribution-Noncommercial-No DerivativeWorks 3.0 License.Копирование и распространение приветствуется.

© 2009 «Практика функционального программирования»

Page 3: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

Оглавление

От редактора 5

1. История разработки одного компилятора. Дмитрий Зуйков 71.1. Предпосылки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81.2. Первое приближение: Форт . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91.3. Второе приближение — Бип /Beep/ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91.4. Окончательный вид языка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161.5. Примеры скриптов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171.6. Итоги . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18

2. ИспользованиеHaskell при поддержке критически важной для бизнеса информационной системы. ДмитрийАстапов 212.1. Обзор системы управления услугами . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222.2. Используемый в системе язык программирования и связанные с ним проблемы . . . . . . . . . . . . . . . . . . . . . 222.3. Постановка задачи . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 242.4. Написание инструментальных средств на Haskell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 242.5. Достигнутые результаты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262.6. Постскриптум . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27

3. Прототипирование с помощью функциональных языков. Сергей Зефиров, Владислав Балин 283.1. Введение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 293.2. Инструменты прототипирования компонентов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 293.3. Моделирование аппаратуры с помощью функциональных языков . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 303.4. Результаты применения подхода в жизни . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343.5. Заключение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343.6. Краткий обзор библиотек моделирования

аппаратуры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34

4. Использование Scheme в разработке семейства продуктов «Дозор-Джет». Алексей Отт 364.1. Что такое «Дозор-Джет»? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374.2. Архитектура систем . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374.3. Почему Scheme? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 384.4. Использование DSL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 384.5. Реализация СМАП . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 394.6. Основные результаты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40

5. Как украсть миллиард. Александр Самойлович 425.1. Введение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 435.2. Разработка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 445.3. Заключение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48

6. Алгебраические типы данных и их использование в программировании. Роман Душкин 496.1. Мотивация . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 506.2. Теоретические основы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 526.3. АТД в языке программирования Haskell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 556.4. АТД в других языках программирования . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58

Page 4: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

Оглавление Оглавление

ii © 2009 «Практика функционального программирования»

Page 5: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

От редактора

Спасибо вам за интерес ко второму выпуску журнала! Пла-нируя первый выпуск, мы не имели никакого представленияо масштабе интереса к декларативному программированию вобщем и к русскоязычному ресурсу о нём в частности. На-ши оптимистичные оценки потенциальной аудитории журна-ла находились в районе полутора тысяч читателей. Каково жебыло наше удивление, когда оказалось, что только за первыедве недели с момента выхода в свет первого номера журналаколичество уникальных читателей нашего электронного вы-пуска зашкалило за десять тысяч!

Вот бы ещё иметь возможность узнать, кто сумел дочитатьвыпуск до конца…

Мы получили от вас большое количество отзывов и идей.Конечно, было получено имножество противоречивых откли-ков, от «в первом выпуске статьи — для самых маленьких» до«слишком запутанно, умерна двадцатой странице».Напослед-нееможно заметить, что практические задачии способыих ре-шения у всех авторов разные.Поэтому ожидайте статей самогоразного уровня, от начального до теории категорий. Если однастатья «не идёт», за ней есть следующая, другого автора, и такдалее.

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

Мы не хотим делать журнал для сотни-другой человек, по-нимающих, что такое комонада и прочий «матан». Эти люди,как правило, знают английский и интересуются темой доста-точно глубоко, чтобы читать первоисточники (так сложилось,что практически все первоисточники в настоящее время — наанглийском). Рассчитывать журнал исключительно на них¹ —значит тратить огромное количество усилий ради исчезающемалого эффекта. Поэтому и в этом выпуске мы снова…

Займёмся популяризациейЦентральная тема второго выпуска журнала — демонстра-

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

Первые четыре статьи — Дмитрия Зуйкова, Дмитрия Аста-пова, Сергея Зефирова в соавторстве с Владиславом Бали-ным, и Алексея Отта — вытаскивают на поверхность «кухню»нескольких компаний. Статьи демонстрируют, что функцио-нальные языки находят применение в промышленном про-

¹Если вы узнали в этом портрете себя, лучше станьте автором или рецен-зентом.Напишите в редакцию:[email protected].Материалырецензируют-ся и перед публикацией проходят корректуру, так что вы ничем не рискуете,пробуя себя в этом амплуа впервые.

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

СтатьяАлександраСамойловича рассматривает созданиенаязыке Erlang игрушечного, но практичного проекта — рекур-сивного многопоточного обходчика сайтов. К третьему выпус-ку журнала мы планируем подготовить ещё несколько статейпро Erlang.

Завершающая статья Романа Душкина в большей степениориентирована на теорию: она познакомит вас с концепциейалгебраических типов данных (АТД) в Haskell и других функ-циональных языках.

Языки разные, языки прекрасныеТе, кто уже успел прикоснуться к функциональному или ло-

гическому программированию в эпоху их экстенсивного роста(например, занимались Lisp-ом или Prolog-ом в восьмидеся-тые), иногда относятся к практической применимости функ-ционального подхода и инструментария со скепсисом. И неда-ром: компиляторы тогда были наивными, компьютеры — ма-ломощными, а аппаратные Lisp-машины² — редкими и доро-гими. В то время «практическое применение» функциональ-ного инструментария естественным образом ограничивалосьисследовательскими задачами.

Сейчас эти детские болезни по большей части уже в про-шлом. С практической стороны, усовершенствования в техни-ках компиляции и интерпретации программ позволили уско-рить функциональные языки так, что становится неочевид-ным, кто победит по скорости в той или иной задаче по обра-ботке данных. Одними из «самых быстрых» языков програм-мирования, часто обгоняющими результаты компиляции C иC++, признаются языки Stalin [2] и ATS [1] — диалекты функ-циональных языков Lisp и ML, соответственно. Даже такиераспространённые реализации функциональных языков, какOCaml или GHC (Haskell), иной раз показывают ускорение от-носительно эквивалентных программ на C/C++ в десять и бо-лее раз³, и редко отстают более чем в два-три раза.

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

²Lisp-машины, реализованные «в железе», были первопроходцами, сделав-шими коммерчески доступными лазерную печать, оконные системы, компью-терную мышь, растровую графику высокого разрешения и другие инновации.Таких машин было произведено всего несколько тысяч штук.

³За счёт более простых (быстрых) алгоритмов управления памятью.

Page 6: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

Литература Литература

сти, бесконтрольным использованием программистами воз-можностей изменения состояния [3].

В отношении безопасности программ теория языков за по-следние двадцать лет продвинулась далеко вперёд. Фокус на-учных исследований сместился с Lisp-а и его реализаций настатически типизируемые языки, такие как Haskell и Epigram,а также на системы доказательства теорем Agda и Coq. Со-временные исследования преимущественно нацелены на со-здание и теоретическое обоснование таких систем статиче-ской типизации, которые позволяли бы по максимуму воору-жить компилятор возможностью раннего обнаружения оши-бок в программах, сохранив при этом максимальную гибкостьдля программиста. Настороженное отношение к декларатив-ным языкам программирования, сформировавшееся под вли-янием негативного опыта восьмидесятых, в настоящее времятребует переосмысления.

Профессиональные программисты часто имеют устоявшие-ся представления о градациях языков программирования, вы-ражающиеся в терминах «лучше-хуже» и дополняемые некимпрактическим контекстом— «лучше для веба», «лучше для си-стемного программирования» и т. д. Такая двухмерная матри-ца оценки языков довольно полезна (несмотря на субъектив-ность), но я рискну ввести ещё один ортогональный крите-рий — дидактичность языка, возможность с использованиемязыка научиться максимуму интересных концепций⁴. Дидак-тичности можно противопоставить практичность, то есть по-лезность языка в качестве инструментального средства, опре-деляемую как степень удобства разработки на нём промыш-ленных систем. Так язык Haskell обладает большей дидактич-ностью, чем OCaml⁵, из-за ленивости, более развитой систе-мы типов и большего количества принятых в нём интересныхидиом. С другой стороны, OCaml может оказаться чуть болеепрактичным инструментальным средством для промышлен-ного программирования, благодаря возможности «срезать уг-лы» и использовать элементы императивного стиля, а такжечасто существенно более шустрым результатам (и процессу!)компиляции.

В результате, мне представляется — конечно же, субъектив-но — следующая картинка для наиболее распространённыхязыков функционального программирования (см. таблицу 1).

Дидактичность Практичность Лёгкость освоения

Haskell OCaml ErlangLisp Erlang OCaml

Erlang Haskell LispOCaml Lisp Haskell

Таблица 1. Некоторые характеристики функциональных язы-ков

С учётом этого, старые, взращенные на Lisp-ах, инстинк-ты по поводу использования (или неиспользования) функци-онального программирования должны быть переосмыслены:Lisp уже давно не является фронтиром, «лицом» функцио-нальной парадигмы, мы должны иметь смелость с ним попро-щаться. А если вы совсемне испорченыфункциональнымпро-граммированием, могу порекомендовать начинать сразу с язы-ка Haskell.

⁴И применять их в любых других языках программирования, см. [4].⁵И Haskell, и OCaml являются наследниками языка ML.

Впрочем, есть и другие мнения. Читайте статьюАлексеяОт-та «Использование Scheme в разработке семейства продуктов„Дозор-Джет“», в которой описывается использование диа-лекта языка Lisp, зарекомендовавшего себя с хорошей сторонына решаемых в «Дозор-Джет» задачах.

Ну и вообще — читайте!Лев Валкин, [email protected]

P. S. Мы хотим, по возможности, продолжать распростра-нять журнал бесплатно. Но создавать его бесплатно у нассовершенно не получается. Поэтому мы будем признательнывсем, ктоимеет возможностьматериальнопомочьжурналу: настранице fprog.ru/donate вы можете найти детали перевода че-рез системы WebMoney и Яндекс.Деньги. Даже сто рублей —эквивалент чашки кофе — имеет шанс сделать наши материа-лы ещё чуточку качественнее.

Такжемыобращаемся к организациями частнымлицам, ко-торые хотели бы видеть свои услуги или товары, интересныепрограммистам и менеджерам софтверных проектов, в нашемжурнале. Свободное место у нас имеется, добро пожаловать!

Литература[1] Benchmarking java against c and fortran for scientific applica-

tions / J. M. Bull, L. A. Smith, L. Pottage, R. Freeman // In Pro-ceedings of ACM Java Grande/ISCOPE Conference. — ACMPress, 2001. — Pp. 97–105.

[2] Cowell-Shah C. W. Nine language performance round-up:Benchmarking math & file i/o, URL: http://www.osnews.com/story/5602/Nine_Language_Performance_Round-up_Benchmarking_Math_File_I_O (датаобращения: 28 сентября 2009 г.). — 2004.

[3] Gat E. Lisp as an alternative to java // Intelligence. — 2000. —Vol. 11. — P. 2000.

[4] Hudak P., Jones M. P. vs. ada vs. c++ vs. awk vs. ... an ex-periment in soware prototyping productivity available fromURL: http://www.haskell.org/papers/NSWC/jfp.ps (дата обращения: 28 сентября 2009 г.): Tech. rep.: YaleUniversity, 1994.

[5] Kernighan B. W., Wyk C. J. V. Timing trials, or the trialsof timing: experiments with scripting and user-interface lan-guages, URL: http://cm.bell-labs.com/cm/cs/who/bwk/interps/pap.html (дата обращения: 28 сентября2009 г.). — 1998.

[6] Prechelt L., Informatik F. F. Comparing java vs. c/c++ efficiencydifferences to inter-personal differences. — 1999.

[7] Prechelt L., Java C. C. An empirical comparison of c, c++, java,perl, python, rexx, and tcl for a search/string-processing pro-gram. — 2000.

[8] Ray tracer language comparison, URL: http://www.ffconsultancy.com/languages/ray_tracer/ (датаобращения: 28 сентября 2009 г.). — 2005-2007.

[9] Zeigler S. F. Comparing development costs of c and ada, URL:http://www.adaic.com/whyada/ada-vs-c/cada_art.html (дата обращения: 28 сентября 2009 г.). — 1995.

6 © 2009 «Практика функционального программирования»

Page 7: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

История разработки одного компилятора

Дмитрий Зуйков[email protected]

Аннотация

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

is article is not a tutorial on how to write a compiler and does not aim to describe the type inference algorithms in detail.is is just a history which tells how big, complex and scary task turns out to be small and quite simple, if proper instruments areutilized. Nevertheless, it is expected that the reader knows at least some basics about compilers.

Обсуждение статьи ведётся по адресуhttp://community.livejournal.com/fprog/1605.html.

Page 8: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

1.1. Предпосылки

1.1. ПредпосылкиПриоритетное направление деятельности нашей компа-

нии¹ — разработка и внедрение решений, связанных с си-стемами глобального позиционирования и навигации. Основ-ным продуктом является сервисная система для предоставле-ния услуг мониторинга автотранспорта, ориентированная, впервую очередь, на операторов сотовой связи.

В настоящий момент сервис на базе данной системы нахо-дится в стадии запуска в северо-западном регионе РФ в ка-честве совместного проекта с одним из операторов большойтройки, планируется развёртывание и в других регионах. По-мимо этого, разрабатываются совместные проекты с несколь-кими государственными структурами.

Основные компоненты сервисной системы:

Инфраструктурные сервисы: данный слой обеспечивает вза-имодействие системы с операторами связи и централи-зованное управление мобильными терминалами. Для ре-ализации используется платформа Erlang/OTP, а в каче-стве СУБД — Mnesia.

Прикладные приложения: данный слой реализует различ-ные приложения для пользователей системы; в настоя-щиймомент это решения, предназначенные для контроляличного автотранспорта, а также планирования и мони-торинга грузов (логистики). Данные приложения предо-ставляют веб-интерфейс, но его использование необяза-тельно — может использоваться как «толстый клиент»,так и доступ с мобильного телефона посредством IVR.Для реализации приложений также используется Erlang.

Мобильные терминалы: GSM/GPS и GSM/GLONASS треке-ры — автономные модули на базе микроконтроллеров,GSM модема и приемника системы глобального позицио-нирования. Данные устройства устанавливаются на кон-тролируемых системой объектах и могут взаимодейство-вать с ней посредством SMS или GPRS. Устройства имеютразличные варианты исполнения и могут дистанционноперепрограммироваться в зависимости от решаемых за-дач.

Применяемые в системе мобильные терминалы должны об-ладать возможностью очень гибкой удалённой настройки, таккак они могут быть использованы в самых различных прило-жениях, условиях эксплуатации и географических регионах:использование устройств на личных автомобилях с питаниемот бортовой сети на дорогах в пределах Московской области,практически полностью покрытой сетями GSM, имеет совер-шенно иную специфику, чем использование модулей, распо-лагающих только собственным источником питания, переме-щающихся вместе с железнодорожными контейнерами черезУрал в условиях длительного отсутствия связи и получающихтехническое обслуживание (включая зарядку и замену бата-рей) не чаще, чем раз в полгода.

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

¹http://trxline.ru/reeline_LLC/Development.html

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

Установка регистров осуществлялась посредством сообще-ний SMS сети GSM, для инициализации трекера (достаточночасто происходящий процесс) требовалось послать более сот-ни сообщений, порядок которых был иногда критически ва-жен.

Стоит ли упоминать, что сеть GSM не гарантирует порядокдоставки SMS, и задержавшееся и повторно посланное шлю-зом сообщение могло сломать весь и без того небыстрый про-цесс инициализации.

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

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

• Они должны быть очень компактными.

• Они должны с разумной скоростью выполняться на вось-мибитных микроконтроллерах (мы начинали с устройствна базе микроконтроллера семейства PIC18), c неболь-шим объемом RAM (1–3КБ), ограниченным объемом пе-резаписываемой постоянной памяти, доступ к которойможет быть достаточно дорогим (например, по шине I2Cс частотой максимум 400 кГц).

• Среда исполнения для них должна обходиться минималь-ным объемом памяти.

• Они должны быть безопасными: никакой загруженныйизвне скрипт не должен приводить к падению системы спотерей связи с ней.

Было рассмотрено достаточное число готовых реализацийразличных языков: форты, лиспы, Joy, Cat, Raven, Tcl, Staapl,Squeak, Lua, Haxe, Python, Java и другие. Был рассмотрен вари-ант написания своего рантайма для существующего компиля-тора.

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

По плотности байткода в финал вышли форты и Java (насамом деле squeak, но он отсеялся по другим причинам), носпецификация JVM весьма сложна, и были серьезные сомне-ния, что за разумное время удастся реализовать её для нашейплатформы. При этом пришлось бы научиться дорабатыватькомпилятор Java самостоятельно, а это сводило преимуществаиспользования чужого решения к нулю. Разрабатывать свойкомпилятор Java в планы точно не входило.

В результате было принято решение реализовать скрипто-вый язык и его рантайм самостоятельно.

8 © 2009 «Практика функционального программирования»

Page 9: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

1.3. Второе приближение — Бип /Beep/

1.2. Первое приближение: ФортИтак, в качестве скриптового языка в первом приближе-

нии был выбранФорт. Его преимущества очевидны: он крайнепрост в реализации, но при этом довольно мощен. ПрограмманаФорте представляет собой поток токенов, разделённыхпро-бельными символами — таким образом, транслятор Форта нетребует парсера, нужен лишь лексический анализатор, он жетокенайзер.

В качестве языка реализации транслятора был выбранPython по причине его широкого, на тот момент, примененияв проекте.

Рантайм представлял собой типичную двухстековую Форт-машину, без какого-либо управления динамической памятью,реализованную по мотивам F21².

Инструкции и литералы (константы) имеют разрядность 8бит. Используется token threaded code, то есть код, представ-ленный потоком чисел, каждое из которых соответствует сме-щению в таблице обработчиков³.

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

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

Главная проблема — отсутствие типизации. Ошибка типовможет привести к падению всей прошивки устройства, что на-рушает одно из важнейших требований к нашему скриптово-му языку — безопасность.

Еще одна проблема — наиболее компактный код на Фортеполучается, когда все вычисления проводятся на стеке. Но до-ступная глубина стека для вычислений в общем случае (не рас-сматривая Форты с командами типа pickn) ограничена при-близительно четырьмя его верхними ячейками. Более того,циклы со счетчиком организуются либо путем организациитретьего «программного» стека для переменных циклов, чторазрушает всю изящность языка, либо хранением переменнойцикла в стеке адресов, что приводит к различным казусам вслучае попытки возврата из слова, вызываемого внутри цикла.Данная проблема приводит к нарушению двух важных требо-ваний сразу: безопасности и компактности байткода. Если дляреализации некоторого алгоритма не хватает двух-трех пере-менных, то приходится прибегать к различным ухищрениям:использованию стека адресов для промежуточного храненияданных, неуправляемой памяти — для хранения переменных,а также примитивов типа rot, swap или over. Это раздувает коди, в случае использования стека адресов, может приводить кнепредсказуемымошибкам времениисполнения, а также весь-ма затрудняет написание и прочтение кода. Как правило, что-бы понять некий алгоритм, реализованный наФорте, требует-ся его мысленно выполнить, что удаётся не всем.

Теперь можно сказать пару слов о применимости Pythonв качестве инструмента для реализации трансляторов. Осо-

²Известный, даже можно сказать, культовый в Форт-среде процессор, см.например: http://www.ultratechnology.com/f21data.pdf.

³На самом деле, конкретный способ обработки может варьироваться в за-висимости от реализации виртуальной машины.

бенности Python — динамическая типизация, неявное введе-ние переменных и «утиная типизация»⁴. Все это означает, чтомногие ошибки будут обнаружены только во время испол-нения. Разработка сколько-нибудь нетривиального кода зача-стую требует многократного рефакторинга и попросту пере-писывания кода, с частыми откатами назад. Большое количе-ство юнит-тестов в такой ситуации — это не просто признаккультуры разработки, а вопрос выживания проекта; при этомтестами мы пытаемся поймать не только функциональные де-фекты, но и элементарные ошибки и опечатки, которых прирефакторинге возникает масса. В случае разработки компи-лятора, придумывать юнит-тесты (в отличие от прочих видовтестов) может быть очень непросто, и этот фактор со време-нем сильно сокращает преимущество в производительностиот применения высокоуровневого языка программирования.

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

Итак, поставленные цели были в основном достигнуты, нокак результат, так и процесс разработки оставляли впечатле-ние, что всё могло бы быть гораздо лучше.

1.3. Второе приближение — Бип /Beep/Несмотря на то, что нами была разработана версия прошив-

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

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

Для их реализации был выбранмикроконтроллер семействаMSP430, который выгодно отличается от PIC простой и удоб-ной архитектурой, а также является 16-разрядным.

Предыдущий рантайм был реализован на ассемблере дляPIC18 и не подлежал портированию, так что предстояло реа-лизовывать его с нуля. Учитывая описанные выше недостаткиФорта как прикладного языка, а также бóльшие возможностиMSP430, было решено попробовать разработать более удоб-ный, безопасный и доступный скриптовый язык, который быдавал возможность прямого программирования устройств ихконечными пользователями.

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

1.3.1. Требования и дизайн языкаДизайн языка определялся следующими первоначальными

требованиями:

«Скриптовость», трактуемая как:

⁴Duck typing.

© 2009 «Практика функционального программирования» 9

Page 10: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

1.3. Второе приближение — Бип /Beep/

• Простота использования.• Отсутствие необходимости явной декларации ти-

пов.• Быстрая компиляция скрипта в байткоди его запуск.• Управление памятью. Для сколько-нибудь серьёз-

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

Высокоуровневость: Поддержка в языке структур данных,применимых для прикладного программирования: пар,списков, структур, массивов и строк.

Универсальность: Так как границы области применения язы-ка заранее не ясны, то язык должен быть универсальным,использование DSL нецелесообразно.

Императивность: По предыдущему опыту, типичные реали-зуемые алгоритмы выглядели императивными, так чтологично делать язык императивным.

Простой синтаксис: Чтобы язык можно было быстро изу-чить, и чтобы его можно было быстро реализовать.

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

Типизация: Отсутствие типизации делает невозможнымпростую разработку скриптов, так как даже незначи-тельная ошибка может привести к критическим дляустройства последствиям — разрушению памяти истеков, краху прошивки и последующей недоступностиустройства.

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

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

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

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

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

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

Таким образом, разрабатываемый язык должен обладатьследующими свойствами:

• императивность,• простой, привычный синтаксис,• автоматически управляемая динамическая память (сбор-

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

1.3.2. Выбор инструмента разработкиВкачестве инструментов разработки рассматривались толь-

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

Рассматривались два варианта — Haskell и OCaml. Оба язы-ка весьма зрелые, имеют давнюю историю и большое сообще-ство пользователей, а также большое количество библиотек иразличных вспомогательных средств.

Несмотря на то, что Haskell выглядел более выигрышно: бо-гатая, хорошо организованная и единообразная встроеннаябиблиотека, имеется много большее количество сторонних ре-шений, больше доступных онлайн руководств, примеров икниг, начать получать результаты оказалось проще с OCaml.Он и был выбран в итоге.

Существует большое количество примеров компиляторов,реализованных на OCaml, с которыми оказалось интересноознакомиться перед тем, как начать разрабатывать свой. Вотнекоторые из них:

Haxe⁵ Высокоуровневый компилируемый язык со статиче-ской типизацией и выводом типов. Компилируется в коддля виртуальной машины NekoVM⁶, которая являетсявесьма интересным проектом сама по себе.

MinCaml⁷ Подмножество ML, компилируется в оптимизиро-ванный машинный код для SPARC, обгоняющий на неко-торых тестах gcc. Являясь частью учебного курса япон-ского Tohoku University, интересен как пример генерациикода и реализации различных оптимизаций.

e Programming Language Zoo⁸ Учебное пособие по курсуразработки трансляторов от Andrej Bauer, включающеереализацию интерпретаторов нескольких языков про-граммирования. Хороший пример базовых техник, при-меняемых при разработке трансляторов, демонстрирую-щий использование ocamlyacc и ocamllex.

⁵Haxe: http://haxe.org/⁶NekoVM: http://nekovm.org/⁷MinCaml: http://min-caml.sourceforge.net/index-e.html⁸e Programming Language Zoo: http://andrej.com/plzoo/

10 © 2009 «Практика функционального программирования»

Page 11: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

1.3. Второе приближение — Бип /Beep/

1.3.3. Инфраструктура проектаПосле того, как было принято решение использовать

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

Для сборки удобно использовать ocamlbuild — эта утилитав простейшем случае вообще не требует конфигурирования исобирает проект в текущем каталоге, самостоятельно опреде-ляя зависимости. Кроме того, она понимает входные файлыocamlyacc и ocamllex, и примеры из Language Zoo используютименно её.

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

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

Синтаксический разбор и построение AST

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

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

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

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

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

⁹Abstract syntax tree, «абстрактное синтаксическое дерево».

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

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

Бип на текущий момент состоит из следующих основныхэлементов:

Модули , каждый из которых представляет собой списокопределений.

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

Операторы — основные примитивы языка. Они определяютсинтаксические конструкции языка и не возвращают зна-чений.

Блоки представляют собой последовательности операторов,разделённые символом «;» (точка с запятой).

Выражения Выражение есть некая операция, возвращающаязначение и, следовательно, имеющая тип.

Макроопределения Макроопределение есть идентифициро-ванный блок кода.На этапе раскрытиямакроподстановокидентификатор кода в AST заменяется самим кодом. Натекущий момент таким образом реализуются только име-нованные константы, но инфраструктура для макроопре-делений вполне настоящая, более сложные макроопреде-ления оставлены на следующие фазы развития языка.

Исходный код описания AST:

type ast_top = Module of mod_props * parser_ctxand block = Block of blk_props * parser_ctxand definition =

| FuncDef of func_props * parser_ctx| ExternFunc of (name * beep_type)| TypeDef of (name * beep_type) * parser_ctx| MacroDef of macro * parser_ctx

and statement =| StEmpty of parser_ctx| StLocal of (name * beep_type) * parser_ctx| StArg of (name * beep_type) * parser_ctx| StAssign of expression * expression * parser_ctx| StWhile of (expression * block) * parser_ctx| StIf of if_props * parser_ctx| StBranch of (expression * block) * parser_ctx| StBranchElse of block * parser_ctx| StCall of expression * parser_ctx| StRet of expression * parser_ctx| StBreak of parser_ctx| StContinue of parser_ctx| StEmit of Opcodes.opcode list

and expression =| ELiteral of literal * parser_ctx| EIdent of name * parser_ctx| ECall of (expression * expression list) * parser_ctx

© 2009 «Практика функционального программирования» 11

Page 12: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

1.3. Второе приближение — Бип /Beep/

| EAriphBin of operation * (expression * expression) *parser_ctx

| EAriphUn of operation * expression * parser_ctx| ECmp of operation * (expression * expression) *

parser_ctx| EBoolBin of operation * (expression * expression) *

parser_ctx| EBoolUn of operation * expression * parser_ctx| EListNil of parser_ctx| EList of (expression * expression ) * parser_ctx| EPair of (expression * expression ) * parser_ctx| ERecord of (name * expression list) * parser_ctx| ERecordFieldInit of (name * expression ) * parser_ctx| ERecordField of rec_desc * rec_field| ENothing| EVoid of expression| EQuot of name * parser_ctx

and lvalue = Named of name * parser_ctxand rec_desc = Rec of expressionand rec_field = RecField of name * parser_ctxand operation = Plus | Minus | Mul | Div | Mod | BAnd |

BOr | BXor | BShl | BShr | BInv| Less | More | LessEq | MoreEq | Equal | Unequal | And |

Or | Notand literal = | LInt of int | LBool of bool | LString

of stringand mod_props = { mod_defs:definition list }and func_props = { func_name:name; func_type:beep_type;

func_code:block }and blk_props = { blk_code:statement list; }and if_props = { if_then:statement; if_elif:statement

list; if_else:statement }

and macro = MacroLiteral of (name * expression)

Валидация AST

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

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

Те конструкции, которые неудобно проверять таким обра-зом, проверяются путем обычного сопоставления с образцомпри генерации промежуточного кода из AST.

Типичные примеры валидации: проверка корректностиуправляющих конструкций (например, того, что операторыcontinue и break находятся внутри циклов) и проверка кор-ректности выражений (например, того, что в левой части опе-ратора присваивания находится lvalue, т.е. значение, которомуможно что-либо присваивать).

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

and assignment e1 e2 ct =match e1 with| EIdent(n,c) → ct |> add_expr e2

|> add_code (store (get_var ct (n, id_of c)))| ERecordField(Rec(re),RecField(fn,c2)) →let fi = typeof_name (dot fn,id_of c2) ctxin let rt = field_rec_type fiin let off = rec_field_offset rt fnin ct |> add_expr re

|> add_expr e2|> add_code (op (TBCWD(off)) ~comment:(dot fn)

:: [])| other → raise (Type_error(”lvalue required”))

Оператор |> можно назвать «конвейерным оператором»,который определен как

let (|>) f x = x f

Это очень часто встречающаяся в программах на OCamlидиома, которая позволяет представить последовательностьдействий в виде конвейера, где результаты предыдущего этапапередаются на вход текущему, аналогично тому, как это можноделать в командной строке при помощи оператора | («пайп»):

find . -name ’*.c’ | xargs cat | wc -lc

В нашем случае, запись

st |> push_loop e |> nest

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

Приведённая функция генерирует промежуточный код дляоператора присваивания, принимая два узла AST типа «выра-жение» и контекст. В левой части присваивания в Бипе в на-стоящий момент может быть только выражение типа «иден-тификатор» или «ссылка на поле структуры», остальные типывыражений приведут к генерации ошибки.

Пример использования данной функции при генерации ко-да (и весь верхний уровень генератора кода):

in let code_fold st code =match code with| StArg((n,t),c) → st |> add_arg n| StLocal((n,t),c) → st |> add_loc n| StAssign(e1,e2,c) → st |> assignment e1 e2| StWhile((e,_),_) → st |> push_loop e |> nest| StCall(e,_) → st |> add_expr (EVoid(e))| StIf({if_elif=ef},_) → st |> push_if |> nest| StBranch((e,_),c) → st |> branch (Some(e)) CBr

(id_of c) |> nest| StBranchElse(_,c) → st |> branch None CBrElse

(id_of c) |> nest| StRet(e, _) → st |> ret e| StContinue _ → st |> continue| StBreak _ → st |> break| StEmpty _ → st| StEmit(l) → st |> add_emit l

Можно заметить, что генератор кода ориентирован на сте-ковую машину.

Делать какие-либо дополнительные проверки необходимо-сти не возникло.

Очень часто упоминается сложность отладки парсеров, ге-нерируемых автоматическими построителями. Могу сказать,что, благодаря сильной статической типизации и дизайну ти-па AST, удалось практически совсем избежать отладки. Во вся-ком случае, на анализ логов парсера ушло пренебрежимо ма-ло времени. После разработки сходного проекта на динамиче-ском языке такой результат кажется неправдоподобным.Построение словаря

Бип обладает следующими областями видимости:

Модуль Имена модуля (функции, типы, макроопределения).

12 © 2009 «Практика функционального программирования»

Page 13: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

1.3. Второе приближение — Бип /Beep/

Функция Формальные аргументы функции и переменныеверхнего блока.

Блок Каждый блок может содержать объявления перемен-ных.

Подход к областям видимости совершенно традиционен ипохож на Си, Java и другие подобные им языки. Любая об-ласть видимости может содержать объявления, перекрываю-щие имена вышестоящих областей видимости.

Для того, чтобы различать одинаковые имена, принадлежа-щие разным областям видимости, пришлось добавить уни-кальные идентификаторы на уровне AST, которые генериру-ются во время разбора. Таким образом, словарь состоит из парвида:

((имя, идентификатор), дескриптор)

где дескриптор содержит необходимую компилятору ин-формацию об идентификаторе.

Вывод и проверка типов

Для вывода типов в Бипе используется алгоритмХиндли–Милнера, как наиболее простой и хорошо опи-санный в литературе и сети. Существует большое количествопримеров его реализации на различных языках.

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

Структура типов данных в языке описывается при помо-щи алгебраических типов OCaml. Например, атомарные типыданных:

| TInt | TString | TBool

или составные типы данных:

| TPair of beep_type * beep_type| TList of beep_type| TVect of beep_type

или тип «полиморфный параметр»:

| TAny of int

или тип «неизвестный тип данных»:

| TVar of int

ТипыTVar иTAny очень похожи, наличие обоих типов в ре-ализации типизации обусловлено необходимостью отличатьавтоматически введённые переменные типов от параметровполиморфных функций.

Ограничения представляют собой равенства вида:

xi = A,

где слева находится автоматически введённый «неопределён-ный тип данных», а справа — условие, полученное из анализавыражений, в которых данный тип участвует, и явных декла-раций типов, которые язык также допускает.

Операторы и выражения накладывают определённые огра-ничения на типы операндов. Арифметические и битовые опе-рации подразумевают, что их операнды и возвращаемые зна-чения являются целыми числами. Логические операции под-

разумевают булев тип данных, операции и функции над спис-ками требуют аргумента типа список (и являются полиморф-ными, так как список — составной тип данных). Циклы и опе-ратор ветвления требуют булева типа в условии, а операторприсваивания декларирует, что типы переменных в левой иправой частях присваивания одинаковы.

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

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

Система уравнений может быть решена не всегда, как на-пример, в случае типов, определения которых содержат цик-лические ссылки (таких как рекурсивные типы данных) либопротиворечивые условия:

x1 = A

x2 = x1

x2 = B

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

Второй класс ошибок — отсутствие достаточной информа-ции для выведения типов, т.е. случаи, когда не удаётся устра-нить все типы TVar и TAny.

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

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

Жёсткая типизация операций, например, арифметических.Если у нас есть целочисленные операции

+ − * /

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

+. −. *. /.

либо сделать их полиморфными. При этом они станутбесполезны для выведения типов своих операндов, чтосократит количество случаев успешного автоматическо-го выведения типов и потребует большего количества ан-нотаций. Можно ввести классы типов и применять дру-гой, более сложный алгоритм выведения. С подобнойпроблемой сталкиваются и «большие» языки. При этомOCaml, например, выбирает наиболее простое решение—

© 2009 «Практика функционального программирования» 13

Page 14: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

1.3. Второе приближение — Бип /Beep/

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

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

Подробно теория систем типов изложена в книгеBenjamin C. Pierce Types and Programming Languages¹⁰. При-меры из этой книги исключительно полезны для пониманияразличных аспектов типизации.

Оптимизация на уровне AST

Многие оптимизации удобно проводить на уровне AST вслучае, если дерево сохраняет семантику языка, и её не требу-ется реконструировать.

Например, на этом уровне очень несложными видятся оп-тимизация хвостовых вызовов (фактически на уровне клозовсопоставления с образцом) и вычисление константных выра-жений, котороеможнорассматривать как интерпретациюASTс заменой вычислимых во время компиляции выражений наих значения.

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

• Замена последовательности операций сложения с едини-цей и присваивания на инкремент.

• Замена последовательности вычитания единицы и при-сваивания на декремент.

• Замена целого литерала ’0’ на команду VM ’FALSE’ разме-ром один байт.

• Замена целого литерала ’1’ на команду VM ’TRUE’ (анало-гична предыдущей).

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

Типичные примеры — замена сложения инкрементом и вы-читания декрементом:

| EAriphBin(Plus, (_,ELiteral(LInt(1),_)),_) → repl_inc st| EAriphBin(Plus, _, _) → st @ [op ADD]

| EAriphBin(Minus,(_,ELiteral(LInt(1),_)),_) → repl_dec st| EAriphBin(Minus,_, _) → st @ [op SUB]

операция ’@’ в OCaml — конкатенация списков

Генерация промежуточного кода

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

¹⁰http://www.cis.upenn.edu/ bcpierce/tapl/

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

Как уже упоминалось выше, на этом же этапе происходятпроверки определённости типов данных, а также те семанти-ческие проверки, которые невозможно реализовать на уровнепарсера.

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

Оптимизация на уровне кода

Полученное представление кода тоже требует оптимизации,включая устранение артефактов генерации кода, таких каклишние команды NOP, устранение бессмысленного кода, та-кого как сочетание команд вида:

JMP XJMP Y

а также оптимизации, которые просто удобнее делать наэтом уровне. Например, на этапе генерации кода удобнее трак-товать все вызовы как вызовы по переменному адресу, кото-рый помещается на вершину стека. Поскольку виртуальнаямашина поддерживает вызов по константному адресу однойкомандой, для увеличения производительности и уменьше-ния объема кода лучше преобразовать последовательность ко-манд:

LIT XCALLT

в:

CALL X

Генерация выходных файлов

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

Генерация стабов

Поскольку язык имеет возможности FFI¹¹, для связи с функ-циями на низкоуровневых языках порождаются различныестабы, предназначенные для линковки с кодом виртуальноймашины — обертки функций, обертки структур (осуществля-ющие отображение структур Бипа на структуры Си) и табли-цы опкодов виртуальной машины.

1.3.5. Производительность и оптимизацияПоддержанию производительности компиляции на прием-

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

¹¹Foreign Function Interface, интерфейс для вызовафункций, реализованныхна других языках, в нашем случае на C или ассемблере.

14 © 2009 «Практика функционального программирования»

Page 15: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

1.3. Второе приближение — Бип /Beep/

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

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

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

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

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

В целом производительность компилятора следует при-знать удовлетворительной, хотя до идеала далеко — в частно-сти, компилятор OCaml работает существенно быстрее.

1.3.6. Дизайн рантаймаРантайм представляет собой двухстековую виртуальную

машину, оптимизированную для шестнадцатибитной архи-тектуры. Текущей платформой является микроконтроллерMSP430 от Texas Instruments, но существует и версия, запус-кающаяся на PC, используемая в настоящий момент для про-тотипирования и отладки логики скриптов.

Стек A представляет собой стек данных, стек R — стек дляадресов возврата и служебных данных. В отличие от Форта, вБипе прямой доступ к стеку R невозможен, виртуальная ма-шина управляет им сама.

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

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

Параметр Размер Источник ограничения

Code memory ≈3 КБ реализация VMSys. stack + RAM < 100 байт реализация VMСтек A 128 слов определяется пользователемСтек R 64 слова определяется пользователемКуча 4096 слов определяется пользователем

Таблица 1.1. Приблизительные требования виртуальной ма-шины к ресурсам

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

На текущий момент виртуальная машина насчитывает73 команды.

Опкоды имеют разрядность 8 бит, литералы — 16 бит.Память адресуется исключительно словами. Помимо рас-

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

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

Данные, умещающиеся в слово, размещаются на стеке A, аданные, превосходящие в размере слово, размещаются в ди-намической памяти.

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

Не используются ни стек, ни дополнительное поле заголовкадля применения техники reversed pointers. Использование та-кого алгоритма ухудшает асимптотику алгоритма обхода, но,ввиду отсутствия глобальных переменных, периметр сборкимусора определяется только текущей глубиной стека A, и внекоторые моменты память может быть очищена за гаранти-рованное время O(1).

Куча организована в виде связного списка свободных и за-нятых блоков, накладные расходы на управление памятью —одно слово на блок. Максимальный размер блока — 8191 сло-во, что превышает количество RAM на типичном представи-теле целевой архитектуры.

Контроллер Частота Code mem. RAM в т. ч. куча

MSP430F1612 7.3728МГц 55КБ 5КБ 1КБMSP430F5418 7.3728МГц 41КБ¹² 16 КБ до 14КБ

Таблица 1.2. Типичные используемые конфигурации

Производительность виртуальной машины можно характе-ризовать как «достаточно приличную». Оценивать её имеетсмысл только в сравнении с другими, но на целевой платфор-ме сравнивать не с чем, а сравнение с PC будет происходитьв слишком неравных условиях, так как рантайм Бипа ориен-

¹²Расширенная память не используется.

© 2009 «Практика функционального программирования» 15

Page 16: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

1.4. Окончательный вид языка

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

Тем не менее, сравнение работы приблизительно одинако-вого алгоритма синтаксического разбора строки NMEA¹³ наLua, Python и Beep показало, что даже в условиях, когда в VMBeep за цикл работы скрипта GC вызывается более десяти ты-сяч раз¹⁴, Бип оказывается в три — пять раз быстрее Python,и в полтора — два раза быстрее Lua. Отнести такие результа-ты можно на счет статической типизации, при которой отсут-ствуют накладные расходы на проверку типов во время испол-нения.

1.4. Окончательный вид языкаВ результате нескольких итераций разработки компилятора

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

Типы данныхЯзык поддерживает следующие встроенные типы данных:

Int Целочисленный тип шириной в слово. Может быть знако-вым и беззнаковым. Символы и байты также представля-ются числом.

String Строка, интерпретируемая как последовательностьсимволов (чисел). В текущей реализации предполагается,что символ не превышает в разрядности один байт,строка хранится в упакованном виде (каждое словосодержит два символа). Строки являются иммутабель-ными, одинаковые константные строки в программеявляются ссылками на одну и ту же строку¹⁵.

Bool Логический тип данных. Управляющие конструкции илогические операторы оперируют значениями этого типа.

Pair Кортеж элементов разных типов размерностью 2.

List Список однородных элементов.

Vector Массив однородных элементов с произвольным досту-пом.

Fun Функция.

Record «Запись», аналог структуры в языках C, C++.

ЛитералыПоддерживаются численные шестнадцатеричные, десятич-

ные и строковые литералы, а также литералы «символ», транс-лируемые в Int.

¹³Стандарт на обмен данными для GPS устройств.¹⁴При ограничении кучи в 1КБ.¹⁵Константы размещаются в памяти кода, не занимая память кучи.

ПеременныеПеременные объявляются оператором local и могут содер-

жать спецификации типов. Переменные могут объявляться влюбом месте блока и являются только локальными. Глобаль-ных переменных в Бипе не существует. При объявлении каж-дая переменная должна быть инициализирована значением,попытка объявить переменную без её инициализации приво-дит к ошибке компиляции. В Бипе также не существует значе-ний, подобных null, None или undefined в других языках.

ОперацииБип обладает достаточно стереотипным набором операций:

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

Все операции типизированы, например, логические опера-ции принимают и возвращают значения типа Bool.

Оператор сравнения полиморфен и может принимать зна-чения типов Int и String. В последнем случае компиля-тор генерирует вызов встроенной функции strcmp, которуюможно вызвать и напрямую.

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

Встроенные функцииБип содержит некоторое количество встроенных функций,

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

Управляющие конструкцииЯзык включает минимум управляющих конструкций: цикл

с условием while, условный оператор if-elif-else и опе-раторы break и continue, прерывающий цикл и переходя-щий к следующей итерации цикла, соответственно.

Условный оператор и оператор цикла требуют в качествеусловия выражение типа Bool.

БлокиБлоки являются последовательностями операторов, разде-

лённых символом «;» (точка с запятой).

ФункцииФункции объявляются ключевым словом def и вызывают-

ся при помощи оператора ().Функции в Бипе являются первоклассным типом¹⁶, мо-

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

При объявлениифункцииможно специфицировать типы еёаргументов и тип возвращаемого значения, в противном слу-чае эти типы будут выведены.

МакрокомандыНа текущий момент язык поддерживает макроопределение

только для литералов.

¹⁶Насколько это возможно без реализации замыканий.

16 © 2009 «Практика функционального программирования»

Page 17: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

1.5. Примеры скриптов

FFIКомпилятор языка умеет автоматически генерировать

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

Обработка ошибокБип является языком с сильной статической типизацией,

вследствие чего скрипты на нем гарантированно корректны вотношении типов. Исключения на текущиймомент отсутству-ют, ошибки времени исполнения, такие как переполнения сте-ков, обрабатываются на уровне API рантайма.

1.5. Примеры скриптовHello, world!def main() {

putsn(”Hello, world!”);}

Создадим и распечатаем список пар строкРади разнообразия здесь мы декларируем типы явно.

def print(val:(string,string)):void {puts(fst(val));puts(snd(val));

}

def main() {local l = (”B”,”E”)::(”E”,”P”)

::(”R”,”U”)::(”L”,”Z”)::[];local t = l;while !nil(t) {

print(head(t));t = tail(t);

}}

Функции—почти совсемпервоклассные гражданеdef print_smth() {

putsn(”BEEP RULZ!”);}

def print(x: fun(void):void ) {x();

}

def main() {local l:[fun(void):void] = print_smth

:: print_smth:: print_smth :: [];

local t = l;while !nil(t) {

print(head(t));t = tail(t);

}}

Работаем с приемником GPSЧтобы предоставить читателю возможность проникнуться ат-мосферой продукта, приведу часть реального боевого скрип-та:

# FFI - declaring external functions# stubs are generated automatically

@extern gps_power(bool):void;@extern nmea_read() : string;@extern seconds(void):int;

type gps_data {gps_utc:string,gps_fx:int,gps_sat:int,gps_hdop:string,gps_lat:string,gps_lats:int,gps_lon:string,gps_lons:int

}

def str_ntok(s,seps,n) {local len = strlen(s);local len2 = vect_len(seps);local off = 0, size = 0;

if n == 0 then {off = 0;size = vect_get(seps,0);

}elif n >= len2 then {

n = len2;off = vect_get(seps, len2-1) + 1;size = len - off + 1;

}else {

off = vect_get(seps,n-1) + 1;size = vect_get(seps,n) - off;

}

ret strsub(s, off, size);}

def collect_gps_data() {local fx = 0;local utc = ””;local sat = 0;local hdop = ””;local lat = ””;local lats = ””;local lon = ””;local lons = ””;local i = 0;local timeout = 30;local t1 = seconds(), dt =0;while i < 10 && timeout != 0 {

dt = seconds() - t1;t1 = seconds();if timeout >= dt then timeout = timeout - dt;

else timeout = 0;local s = nmea_read();if s != ”” then {

#putsn(s);local sep = strfindall(s,’,’);if startswith(s, ”$GPGGA”) then {

fx = strtoul(str_ntok(s,sep,6),16);sat = strtoul(str_ntok(s,sep,7),16);utc = strsub(str_ntok(s,sep,1),0,6);lat = str_ntok(s,sep,2);lats = str_ntok(s,sep,3);lon = str_ntok(s,sep,4);

© 2009 «Практика функционального программирования» 17

Page 18: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

1.6. Итоги

lons = str_ntok(s,sep,5);hdop = str_ntok(s,sep,8);i = i + 1;

}}if fx > 0 then break;

}local ss = strnth(lats, 0);ret { gps_data:

gps_utc = utc,gps_fx = fx,gps_sat = sat,gps_hdop = hdop,gps_lat = lat,gps_lats = strnth(lats,0),gps_lon = lon,gps_lons = strnth(lons,0)

};}

def main() {putsn(”GPS ON”);gps_power(true);

while true {local nmea = collect_gps_data();if nmea.fx > 0 then {

putsn(”Coords fixed”)puts(”Satellites: ”);putsn(utoa(nmea.gps_sat, 16));puts(”Latitude: ”);putsn(nmea.gps_lat);puts(”Longitude: ”);putsn(nmea.gps_lat);

}}

}

Модем (и макросы)И напоследок, поработаем с модемом и немного с макросами:

@extern gps_power(bool) : void;@extern modem_power(bool) : void;@extern modem_power_check(void) : bool;@extern modem_init(int) : bool;@extern modem_ussd(string, int) : string;@extern modem_sms_send(string, string) : bool;@extern nmea_read() : string;@extern seconds(void) : int;

@literal INITIAL 0;@literal DIGIT 1;@literal NOPINCODE 0xFFFF;

def parse_account(s) {local i = 0, begin =0, end = 0;local len = strlen(s);local state = ‘INITIAL;while i < len {

local c = strnth(s, i);if state == ‘INITIAL && c >= ’0’ && c <= ’9’

then { state = ‘DIGIT; begin = i; }if state == ‘DIGIT && c == ’.’

then { end = i; break; }if state == ‘DIGIT && c < ’0’ || c > ’9’

then state = ‘INITIAL;

i = i + 1;}if begin >= end then ret (false,0);ret (true,strtoul(strsub(s,begin,end-begin),10));

}

def main() {gps_power(true);modem_power(true);putsn(”MODEM ON”);local i = 0;while i < 20 {

puts(”.”);sleep_ms(1000);i = i + 1;

}modem_init(‘NOPINCODE);sleep_ms(2000);local money = snd(parse_account(

modem_ussd(”#102#”,32)));puts(”MONEY: ”);put_int(money);sleep_ms(2000);modem_sms_send(”+71234567890”

”PRIVED, KRIVEDKO!”);}

1.6. ИтогиПрактическое применение языка

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

Основные цели, которые ставились при разработке Бипа,были достигнуты, а по отдельнымпараметрам он даже превзо-шел связанные с ним ожидания.

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

Еще один немаловажный аспект: байткод Бипа плотнее, чеммашинный код, генерируемый компилятором Си. В то время,когда ресурсы code memory, отведённые под прошивку (около32КБ flash), практически исчерпаны, 8 КБ сегмента, отведён-ного под байткод, еще имеют резервы. В связи с этим нехваткафункциональности прошивки вполне может быть компенси-рована за счет скрипта.

Применение Бипа позволило добиться таких характери-стик системы, как надежное удалённое изменение поведенияустройств и отсутствие необходимости вфиксированных про-токолах работы: протокол реализуется скриптом иможет бытьлюбым, удобным для конкретного применения устройства.

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

18 © 2009 «Практика функционального программирования»

Page 19: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

1.6. Итоги

HTTP-клиент, разумеется, также реализован на Бипе; ничтонемешает реализовать SOAPилиXMLRPC, если возникнет та-кая необходимость.

Обновление скрипта может происходить различными спо-собами, наиболее простой из них — HTTP с докачкой.

Удалённое обновление скрипта уже многократно использо-валось при пользовательском тестировании, позволяя устра-нять различные проблемы на лету, практически между двумясобытиями трекинга, менее чем за 30 секунд. Пользователи да-же не замечали, что произошло что-то особенное.

Стоит упомянуть, что принятые в дизайне языка и ран-тайма решения — строгая статическая типизация, отсутствиенеинициализированных переменных, отсутствие рантайм-исключений¹⁷ — полностью окупились: за полгода пользова-тельского тестирования не зафиксировано ни одного паденияпрошивки устройств, вызванного дефектами дизайна рантай-ма или компилятора¹⁸. При этом не потребовалось разработкиэмуляторов и длительного тестирования скриптов на них —скрипты пишутся сразу и тестируются непосредственно на са-мих устройствах. Благодаря типизации есть уверенность, чтоскомпилировавшийся скрипт будет нормально работать и неприведет к потере связи с устройством, что не может быть га-рантировано при использовании, например, Форта или гипо-тетических динамических языков.

OCaml в качестве языка разработкиВыбранный инструментарий безусловно оправдал себя.

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

В рамках данной задачи именноOCaml оказался идеальныминструментом — остальные альтернативы неизбежно приве-ли бы к сильному проигрышу в сроках, что в нашем случае бы-ло эквивалентно смерти проекта, так как он мог выжить толь-ко в случае очень быстрого появления хотя бы proof-of-conceptреализации, которая показала бы, что язык с требуемыми ха-рактеристиками вообще возможно разработать за реалистич-ное время имеющимися силами.

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

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

Рост размера кода, увеличение его структурной сложно-сти, количества предположений, соглашений и взаимосвязей,уменьшение его понятности — очень существенные негатив-ные факторы, которые нельзя игнорировать.

Можно привести такой пример: существуют довольно ин-тересные языки программирования Haxe и Boo. Они во мно-

¹⁷Управляемые исключения не противоречат концепции системы, здесьимеются ввиду исключения вида NullPointerException, исключения при ошиб-ках типов и тому подобные, которыми «радует» своих пользователей, на-пример, Java, и которые были бы фатальны для устройства. Декларируе-мые, управляемые исключения с гарантированной обработкой компиляторомвполне допустимы и не были реализованы исключительно из-за недостаткавремени.

¹⁸Разумеется, паденияиз-за ошибок реализацииприсутствовалии устраня-лись в процессе разработки.

гом похожи, например, строгой статической типизацией и вы-водом типов. Первый язык реализован преимущественно наOCaml, второй — на C#.

Реализация системы типов в Boo, написанном на C#, зани-мает более семнадцати тысяч строк кода (более половины ме-габайта кода на высокоуровневом языке), размещающихся вста двадцати двухфайлах. При этомпредставлявшийнаиболь-ший интерес алгоритм вывода типов надежно декомпозиро-ван на слои и «шаблоны проектирования» и код, который егореализует является вполне идиоматичным для этого классаязыков. Задача идентифицировать основной код алгоритма ипонять его не выглядела решаемой за разумное время и былаоставлена.

Ни одной понятной реализации системы типов и алгоритмавыведения на императивных языках найдено не было (былирассмотрены варианты на C#, С++ и даже Perl).

Реализация системы типов в языке Haxe, занимает 4088строк (127624 байт кода) на OCaml и размещается в трех фай-лах, при этоминтересующийалгоритмидентифицируется сра-зужеипредставляет собой, в основном, свертку списков с при-менением сопоставления с образцом. Прочитать и понять егобыло достаточно легко, несмотряна то, что на тотмомент опытразработки и чтения кода, написанного на императивных язы-ках, сильнопревосходил аналогичныйопыт дляфункциональ-ных.

Даже со всеми возможными оговорками разница в функци-ональности систем типов Boo и Haxe гораздо меньше, чем раз-ница в количестве кода, эту функциональность реализующе-го: 100072 строки (2955595 байт) кода текущей реализацииBooубивали всякую надежду, что подобный проект может вообщебыть реализован самостоятельно.

Для сравнения, текущие значения для Бипа: 2592 строки(109780 байт) кода, а время, затраченное на реализацию вме-сте с рантаймом, не превышает трех человеко-месяцев. Этома-ленький проект, и сделать его маленьким помог именно выборOCaml в качестве языка разработки.

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

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

Использование динамических языков, таких как Python илиRuby¹⁹, наверняка бы привело к разрастанию количества юнит-тестов и отказу от продолжения разработки проекта на эта-пе еще второго рефакторинга. Эта тенденция четко прослежи-валась на нашем опыте использования Python в качестве ос-новного языка разработки в различных проектах, и разработ-ка транслятора Форта, речь о котором шла в начале статьи,её только подтвердила. Исследование возможности примене-ния проекта PyPy (реализация транслятора Python на Python, атакже инфраструктура для построения компиляторов на этомязыке) тоже не убедило в обратном.

¹⁹Ruby не демонстрирует особенных преимуществ перед Python, но имеетряд очень существенных недостатков, так что реально его рассматривать былобессмысленно.

© 2009 «Практика функционального программирования» 19

Page 20: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

1.6. Итоги

Никакое количество тестов не может гарантировать отсут-ствие ошибок. Между тем, сильная статическая типизацияименно гарантирует отсутствие ошибок определённого клас-са.

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

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

Последний фактор оказался весьма важен, так как скоростькомпиляции — это заметный фактор, влияющий на использо-вание языка.

Принимая во внимание усилия, которые пришлось (и ещепридется в дальнейшем) приложить для достижения приемле-мого для работы времени компиляции, можно констатироватьправильность этого выбора.

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

Попытки использовать Haskell также предпринимались, ноон был с сожалением отложен, несмотря на все его возможно-сти, которых недостаёт в OCaml. Довольно быстро стало ясно,что использование Haskell неподготовленным человеком мо-жет привести к затягиванию сроков, а кроме того, было неоче-видно влияние его ленивости на разработку.

В заключение хотелось бы сказать, что не стоит рассмат-ривать OCaml и Haskell как языки, предназначенные исклю-чительно для разработки компиляторов — это отличные ин-струменты, подходящие для обширного круга задач. Эти язы-ки предлагают удачный набор абстракций, который позволя-ет концентрироваться на решении задачи, не размениваясь навторостепенные цели типа обслуживания инфраструктуры²⁰или то, что обычно называют строительством велосипедов.Они обладают развитым инструментарием²¹ и набором биб-лиотек, при этом позволяют генерировать код, по произво-дительности не сильно уступающий, а иногда превосходящийкод более распространённых низкоуровневых языков.

²⁰Пресловутые «шаблоны проектирования».²¹Если не понимать под ним исключительно IDE.

20 © 2009 «Практика функционального программирования»

Page 21: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

Использование Haskell при поддержке критически важной для бизнесаинформационной системы

Дмитрий Астапов[email protected]

Аннотация

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

e article describes how Haskell functional language proved instrumental in solving practical tasks arising during developmentand maintenance of a certain business-critical information system in a large telecom company.

Обсуждение статьи ведётся по адресуhttp://community.livejournal.com/fprog/1985.html.

Page 22: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

2.2. Используемый в системе язык программирования и связанные с ним проблемы

2.1. Обзор системы управления услуга-ми

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

Описываемые события происходили восемь лет назад, в2001 году. В то время я работал в одном из крупнейших в Укра-ине операторов мобильной связи, и мне было поручено отве-чать за технические аспекты внедрения в компании промыш-ленной системы управления услугами. После того, как проектвнедрения был завершен, я должен был единолично отвечатьза «вторую линию» поддержки и развитие системы.

Система управления услугами¹ отвечает за претворениев жизнь высокоуровневых команд на управление услугами,таких как: «подключить нового абонента», «приостановитьобслуживание абонента за неуплату», «активировать услугуMMS», и так далее.

Эти высокоуровневые команды должны быть преобразова-ны в набор низкоуровневых инструкций для телекоммуни-кационного оборудования (например, «активировать услугуGSM Data» или «дать абоненту доступ к GPRS APN 2»), по-сле чего инструкции должны быть выполнены в определенномпорядке нужными экземплярами коммуникационного обору-дования. Система должна учитывать, что одни и те же функ-ции в рамках сети оператора могут выполняться на разнотип-номоборудованиинесколькихпоставщиков—например, в се-ти могут присутствовать коммутаторы нескольких поколенийот двух поставщиков. Соответственно, система должна выби-рать правильные протоколы для подключения к оборудова-нию, правильный набор команд для формирования инструк-ций, обрабатывать всевозможные исключительные ситуациии вообще всячески скрывать от других информационных си-стем детали и подробности процесса управления услугами.

Являясь критически важной для бизнеса, система исполь-зовалась круглосуточно. В часы пик в нее поступало от 6 до10 тысяч входящих запросов в час. Каждый запрос преобразо-вывался в 5—15 низкоуровневых задач, каждая из которых, всвою очередь, состояла из нескольких команд для конкретногоэкземпляра оборудования. Система имела дюжину различныхинтерфейсов к нескольким десяткам телекоммуникационныхплатформ.

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

2.2. Используемый в системе язык про-граммирования и связанные с нимпроблемы

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

¹Речь идет о продукте Comptel MDS/SAS, ныне известном как ComptelInstantLink.

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

Например, в рамках запроса «активировать для указанно-го абонента услуги SMS, MMS, GPRS, CSD, WAP-over-GPRS,WAP-over-CSD» можно выделить часть, описывающую або-нента (его номер телефона, номер SIM-карты и т. п.), и части,описывающие параметры всех перечисленных услуг.

Производители системы решили, что лучше всего процессобработки запросов организовать в виде, представляющем посути интерпретатор императивного языка:

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

Таким образом, обработка запроса сводилась к анализу пе-реданныхв запросе переменных, порождениюнаих основе но-вых, и, по окончании анализа, порождению команд на основа-нии всего множества переменных. Запрос вида «удалить ука-занного абонента» мог быть обработан таким образом:

• В запросе присутствует переменная $IMSI? Значит, речьидет об абоненте сети GSM, выполняем MARKET=”GSM”.

• Если ($MARKET==”GSM”) и в списке услуг абонента естьMMS, то надо удалить его inbox на MMS-платформе. Вы-полняем MMS_ACTION=”DELETE”.

• …и так далее для всех прочих услуг, которые требуют от-дельного удаления учетных записей.

• Если ($MARKET==”GSM”) && ($MMS_ACTION<>””),то вычисляем ID абонента на MMS-платформе по егономеру телефона и SIM-карты.

• …и тому подобное для всех прочих услуг.

• Для каждого запланированного действия находим егоприоритет в справочной таблице.

• Преобразуем все действия в команды для оборудованияи выполняем в порядке возрастания приоритета.

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

К сожалению, команд в языке было всего пять:

• Присвоение константного значения новойили существующей переменной «окружения»:Assign(var, constant).

22 © 2009 «Практика функционального программирования»

Page 23: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

2.2. Используемый в системе язык программирования и связанные с ним проблемы

Рис. 2.1. Система управления услугами

Условие 1?

Условие 2?

Нет

Условие N?

Нет

К

input1=value1input2=value2

. . .inputN=valueN

начало

конец

Действие 2

Да

Нет

Входящийзапрос

. . .

Действие N

Да

Действие 1

Да

Рис. 2.2. Блок-схема работы интерпретатора

• Присвоение переменной значения, полученного кон-катенацией значений других переменных и констант:Concat(destination, $foo, $bar, ”baz”).

• Сопоставление значения переменной с регулярным вы-ражением и присвоение результата сопоставления все-го выражения и входящих в него групп другим пере-менным: Regexp($var1, regexp, full_match,group1_match, group2_match, ...).

• Извлечение значения из внешнего «словаря» (базы дан-ных), используя значение переменной в качестве «ключа»:Lookup(”datasource”, $key, value).

• Отправка на исполнение устройству X команды, состав-ленной из шаблона T, заполненного значениями перемен-ных S1, S2, S3…: Command(”X”, $T, $S1, $S2, $S3,...).

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

проблемно-ориентированный язык (domain-specific language,DSL), код на котором выглядел примерно так:

10 Comment : ’NMT Convert MAIN_DIRNUM → NMT_PRIMARY_RID’Cond : Equals{$MARKET,”NMT”}Oper : Regexp{$MAIN_DIRNUM=(.....)(...)(.∗),$MTX_DUO=/2/,

$TEMP_REMAIN=/3/}AND Concat{$NMT_MTX_NUMBER,$MTX_DUO,$TEMP_REMAIN}AND Regexp{$NMT_MTX_NUMBER=(.)(.∗),

$NMT_EDI_MSISDN_11_1=/2/}AND Lookup{mtxridlookup,$MTX_DUO,$PORT_DUO}AND Concat{$NMT_PRIMARY_RID,$PORT_DUO,$TEMP_REMAIN}

20 Comment : ’RID2 conversions’Cond : Equals{$MARKET,”NMT”}Oper : Regexp{$EDI2_NEW_PORT=(......)(.∗),$NMT_RID2=/2/}

30 Comment : ’NMT Convert MAIN_DIRNUM → NMT_VMS_NUMBER’Cond : Equals{$MARKET,”NMT”}Oper : Regexp{$MAIN_DIRNUM=(.....)(...)(.∗),

$TEMP_DIRNUM=/2/,$TEMP_REMAIN=/3/}AND Lookup{vmslookup,$TEMP_DIRNUM,$VMS_TRIPLET}

© 2009 «Практика функционального программирования» 23

Page 24: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

2.3. Постановка задачи

AND Concat{$NMT_VMS_NUMBER,$VMS_TRIPLET,$TEMP_REMAIN}

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

Приведенный выше текст программы — это выдержка изсистемного отчета, который выводил всю DSL-программу вкрасивом читаемом текстовом виде. В самой же системе ре-дактирование текста DSL-программы (или бизнес-логики, какя буду называть её в этой статье) осуществлялось с помощьюграфического интерфейса.

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

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

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

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

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

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

Ивсе было быничего, если бы в реализации системногоDSLне обнаружились сложновоспроизводимые ошибки. Напри-мер, все переменные по умолчанию имели специальное зна-чение NULL, которое, в принципе, могло быть присвоено дру-гой переменной. Однако, присвоение значения NULL иногдаприводило к тому, что интерпретатор без всякой диагности-ки пропускал остаток программного блока после такого при-сваивания. Такое поведение проявлялось только под большойнагрузкой, и нам стоило больших трудов докопаться до перво-причины. До тех пор, пока производитель не устранит ошиб-ку в своем коде, необходимо было найти способ как-то обхо-дить эту ошибку. Логично было бы не допускать присваиванияNULL вообще, но как это отследить?

2.3. Постановка задачиИз вышесказанного становится ясно, что для успешной под-

держки этой критически важной для бизнеса системы требо-валось радикально изменить все этапы стандартного жизнен-ного цикла поддержки и развития системы.

Если бы информационная система была разработана «подзаказ», естественным решением было бы заказать её доработ-ку. Если бы она была доступна в исходных кодах, можно былобы потенциально думать над тем, чтобы произвести все необ-ходимые изменения самостоятельно. Однако система пред-ставляла собой коробочный программный продукт, в связи счем сравнительно быстрого и/или недорого способа получитьтребуемую функциональность не предвиделось.

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

В результате я решил разработать отдельный набор инстру-ментов, которые позволили бы:

• Облегчить разработку. Получить, по меньшей мере, воз-можность использовать копирование/вставку текста ипоиск с заменой.

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

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

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

2.4. Написание инструментальныхсредств на Haskell

Поскольку система предоставляла возможность экспорти-ровать полный текст бизнес-логики в текстовый файл, вопросс контролем версий был частично решен путем регулярного

24 © 2009 «Практика функционального программирования»

Page 25: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

2.4. Написание инструментальных средств на Haskell

Рис. 2.3. Снимок экрана графического интерфейса системы

размещения этого текста в корпоративной системе контроляверсий.

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

В результате появилась возможность не только экспортиро-вать бизнес-логику из системы в текстовый файл, но и импор-тировать подобный текстовый файл обратно. После этого всяразработка начала вестись в нормальном текстовом редакто-ре³. Кроме того, появилась возможность автоматически пере-носить изменения из тестовой системы в промышленную безвмешательства человека.

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

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

³Использовался emacs, для которого был сделан свой модуль подсветкисинтаксиса.

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

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

Кроме этого, на базе интерпретатора был сделан инстру-мент, позволяющий автоматически формировать репрезента-тивную выборку входящих запросов. Запросы, прошедшие че-рез систему за какой-то достаточно большой период (месяцили квартал), разбивались на классы эквивалентности по сле-дующему критерию: все запросы, которые генерировали похо-жую трассу исполнения и порождали один и тот же набор ко-манд (с точностью до значений переменных типа «номер теле-фона», «номер SIM-карты» и т. п.), считались эквивалентными.Из каждого класса эквивалентности в репрезентативную вы-борку попадал только один запрос. Полученный набор запро-сов использовался для регрессионного тестирования и поис-ка программных блоков, которые по каким-либо причинам ниразу не были использованы при обработке всех этих запросов.

Также на базе интерпретатора был сделан аналог утилиты«sdiff»⁴ для результатов работы бизнес-логики. На вход ей по-

⁴Утилита «sdiff» выводит текст сравниваемых файлов в две колонки, обо-значая вставки, удаления и правки в сравниваемых текстах.

© 2009 «Практика функционального программирования» 25

Page 26: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

2.5. Достигнутые результаты

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

diffBusinessLogic oldLogic newLogic request =let context = mkInitContext request

oldLogicTrace = runAndTrim context oldLogicnewLogicTrace = runAndTrim context newLogicinif newLogicTrace == oldLogicTracethen return ()else do printSectionHeader

printRequest requestprintAligned $

suppressEquals oldLogicTracenewLogicTrace

whererunAndTrim context logic =

trimVolatileVars $ run context logic

Функция run, предоставляемая интерпретатором, преобра-зует запрос и бизнес-логику в трассу исполнения. ФункцияtrimVolatileVars, взятая из генератора репрезентативнойвыборки, удаляет из трассы исполнения все упоминания пере-менных, которые обязательно разнятся от запроса к запросу(порядковый номер запроса и тому подобные служебные пе-ременные). Если обработанные таким образом трассы испол-нения различаются, то они выводятся в два столбца (при по-мощи функции printAligned), при этом в выводе подавля-ются (функцией suppressEquals) упоминания переменныхи выражений, которые в обеих трассах имеют одинаковые зна-чения.

Именно с помощью этой утилиты и проводилось тестиро-вание новых версий бизнес-логики перед передачей в про-мышленную эксплуатацию. В частности, если было известно,что новая версия бизнес-логики отличается от старой толькообработкой запросов, включающих в себя входную перемен-ную FOO, то репрезентативная выборка запросов при помощиgrep разделялась на две части: запросы, содержащие перемен-ную FOO, и запросы, её не включающие. После чего с помо-щью «sdiff»-подобной утилиты проверялось, что старая и но-вая версии обрабатывают все запросы из первой группы по-разному, а все запросы из второй группы - одинаково.

Наконец, на базе библиотеки QuickCheck был сделан ге-нератор случайных (но не произвольных!) входных запросов.В частности, было известно, что номера телефонов абонентовмогут принадлежать только некоторому определенному диа-пазону, номера IMSI тоже определенным образом ограниче-ны, перечень услуг известен и конечен и так далее. Исполь-зуя эти знания, можно было генерировать запросы, коррект-ные по структуре, но не обязательно непротиворечивые и пра-вильные по содержанию. Они использовались для тестирова-ния бизнес-логики на «дуракоустойчивость».

Полный перечень разработанных инструментальныхсредств и отношения между ними можно увидеть на рисун-ке 2.4.

2.5. Достигнутые результатыГлавный результат заключался в том, что удалось уйти от ис-

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

Удалось устранить падения системы под нагрузкой из-заошибок в системном интерпретаторе бизнес-логики. Сам про-изводитель окончательно устранил все трудно обнаруживае-мые ошибки в области исполнения бизнес-логики только спу-стя полтора года после появления на свет нашего «детекторабагов».

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

Общий объем написанного мною кода — примерно 1600строк на Haskell с обширными многострочными комментари-ями. В том числе:

Программный модуль Кол-во строк

Типы данных абстр. синтакс. дерева 80Парсеры бизнес-логики и запросов 190Компилятор в SQL 100Интерпретатор бизнес-логики 280Утилита «sdiff» 100Построитель «репрезентативной выборки» 300Генератор запросов на QuickCheck 300

Таблица 2.1. Объем кода программных модулей, в строках

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

Почему же был выбран именно Haskell и в чем же, в ретро-спективе, оказались его преимущества?

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

Я выбрал Haskell, который уже тогда привлекал меня про-стотой и скоростью разработки. Кроме того, мне импонирова-ла возможность положиться на механизм вывода типов и пи-сать код, который по возможности будет «правильным по по-строению». Ведь я замахивался на то, чтобы своим интерпре-таторомнаходить и устранять ошибки в другоминтерпретато-ре, и мне вряд ли удалось бы это, будь в моем интерпретаторесвои уникальные ошибки.

В числе прочих преимуществ Haskell можно назвать:

• Возможность «встроить» код парсера непосредственно вкод основной программы при помощи библиотеки ком-бинаторов парсеров Parsec.

• Богатый набор контейнерных типов, предоставляемых

26 © 2009 «Практика функционального программирования»

Page 27: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

2.6. Постскриптум

Парсербизнес-логики

Абстрактноесинтаксическое

дерево

Компиляторв SQL

Парсер запросов

Интерпретатор

Детектор ошибок

Построитель репрезентативнойвыборки запросов

Diff трасс исполнения

QuickCheck

Генератор произвольныхзапросов

Рис. 2.4. Полный перечень разработанных инструментальных средств

языком и стандартными библиотеками (списки, массивы,хэш-таблицы, отображения map).

• Широкий арсенал средств для написания кода на вы-соком уровне абстракции и комбинирования решенияиз частей: функции высших порядков, монады Reader,Writer и State.

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

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

Таким образом, считающийся «академическим» язык был сбольшим успехомприменен для решения практических повсе-дневных задач в критичной для бизнеса области.

2.6. ПостскриптумЯ делал доклад о применении описанных в этой статье ин-

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

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

© 2009 «Практика функционального программирования» 27

Page 28: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

Прототипирование с помощью функциональных языковПрименение функциональных языков для моделирования цифровых электронных схем

Сергей Зефиров, Владислав Балин[email protected], [email protected]

Аннотация

В статье рассказывается о применении функциональных языков в моделировании аппаратуры. Приведены кри-терии, по которым были выбраны функциональные языки вместо распространенных в индустрии инструментов, ипредставлены результаты работы.

e article discusses the use of functional programming languages in hardware modelling. e criteria are provided for choosingthe functional programming languages over the more mainstream tools, and the results of the authors’ work are described.

Обсуждение статьи ведётся по адресуhttp://community.livejournal.com/fprog/2260.html.

Page 29: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

3.2. Инструменты прототипирования компонентов

3.1. ВведениеПри разработке ПО часто применяют прототипирование —

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

• Длительный цикл разработки. Если природа задачи тако-ва, что разработка состоит из большого количества эта-пов, у вас просто не хватит времени исправить ошибкув подходе. Скажем, разработка программно-аппаратныхкомплексов именно такова. В особенности, если у вас…

• …высокая стоимость получения результата. Постановкана производство автомобиля, как и изготовление опыт-ных образцов, является дорогой операцией. Не хватит нетолько времени, но и денег. На данных этапах надо дей-ствовать наверняка. Что сложно, если налицо…

• …отсутствие должного опыта у инженерной группы. Вслучае инновационной разработки, когда вы делаете про-дукт на уровне лучших аналогов (или превосходящийих),это всегда так, ибо вы делаете то, что до вас почти никтоне делал.

Разработка микроэлектроники, как отличный пример про-ектов такого типа, имеет длительный (типичная длительностьпроекта — 1,5 года) и многоступенчатый цикл разработки, вкотором задействовано много разных специальностей. В мик-роэлектронике крайне велика стоимость получения результа-та и, следовательно, высока цена ошибки. Набор фотошабло-нов для современных тех-процессов (тоньше чем 90 нм), безкоторого не получить образцы микросхем, стоит миллионыдолларов и изготавливается фабрикой в срок от 2 месяцев, абюджет относительно простых проектов составляет от единицдо десятковмиллионов долларов. В таких условиях группа раз-работки продукта должна действовать наверняка — ошибка втребованиях или в выборе подхода к проблеме неприемлема изакончится для проекта фатально. Стоит отметить, что подоб-ное происходит не только в разработке микроэлектроники —некоторые чисто программные проекты также во многих ас-пектах обладаютподобнымихарактеристиками. В связи с этиммногие компании-разработчикимикроэлектроники выполня-ют моделирование на раннем этапе для проверки своих реше-ний. Определенного стандарта в данный момент не существу-ет, применяемые инструменты варьируются от одной компа-нии к другой. Ниже мы рассмотрим существующие средствапрототипирования микроэлектроники с их сильными и сла-

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

3.2. Инструменты прототипированиякомпонентов

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

Сейчас разработка устройства в терминах библиотечныхэлементов является скорее исключением, и для описания ап-паратуры применяются языки высокого уровня, такие какVerilog и VHDL, наиболее популярен из них первый.

Verilog крайне прост в изучении — каждый блок устрой-ства описывается функцией, аргументами которой являютсяотдельные «провода» и «массивы проводов» (то есть, биты ибитовые массивы). Это язык чрезвычайно низкого уровня померкам современных языков программирования — в нем пол-ностью отсутствуют типы. Язык содержит ограниченное «син-тезируемое» подмножество, которое может быть оттрансли-ровано в цифровую схему при помощи САПР.

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

Разработчиками САПР поддерживается два языка для ре-шения данной проблемы. Первый из них — SystemC — на делеязыкомне является, это некоторыйфреймворк дляС++.По су-ти, данная библиотека позволяет писать наС++в стилеVerilog,в том числе описывая и синтезируемые конструкции. АвторыSystemC надеялись, что развитые языковые средства С++ поз-волят программистам и инженерам описывать более сложныемодели.

Вторым языком является SystemVerilog. Это последняя ре-дакция языка Verilog, расширенная современными конструк-циями вроде классов и типов. Упрощая структурированиекрупной системы для разработчиков аппаратуры, данныйязык в целом сохраняет подход Verilog и не адресует проблемархитектурного прототипирования.

Основные требования к инструментам прототипированиямикроэлектроники перечислены ниже, в порядке убыванияважности:

• Низкие затраты на разработку. Чем раньше будет созданпрототип, тем лучше.

• Низкая стоимость внесения изменений. Прототип не яв-ляется чем-то статичным, в него очень часто вносятся из-менения. Чем короче цикл внесения изменений, тем боль-ше экспериментов удастся провести на этапе проектиро-

© 2009 «Практика функционального программирования» 29

Page 30: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

3.3. Моделирование аппаратуры с помощью функциональных языков

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

• Скорость работы модели (скорость моделирования). Чемвыше скорость, тем большие по объёму тесты можно бу-дет подать на вход модели. Разница в поведении на ма-лом и большом тестах может быть значительной¹ и суще-ственно повлиять на выбор подхода к решению.

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

• Встроенность в цикл разработки: возможность постепен-ного уточнения описания компонента с целью перехода кописанию уровня синтезируемой модели.

SystemVerilog и SystemC в основном нацелены на решениепоследних двух пунктов требований, и практически не адре-суют первые три.

Отдельно стоит упомянуть машины состояний. Электрон-ная аппаратура практически вся построена на них, и слож-ность варьируется от простых машин с парой состояний (естьданные — нет данных) до сложных составных. Большая частьмашин состояний не может быть представлена в виде упроща-емого цикла² и выглядит в простейшем случае примерно так:

loop1:action 1;

loop2:action 2;if condition then goto loop1;action 3;goto loop2;

Хорошее средство прототипирования позволяет описыватьмашины состояний просто и безопасно. Чем проще такое опи-сание, тем быстрее можно получить точный прототип компо-нента, тем выше скорость реализации — первый пункт в пе-речне выше.

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

Если попытаться посмотреть на функциональные языки сточки зрения инструмента для прототипирования аппарату-ры, то выводы будут достаточно интересны:

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

¹Например, кодирование видео. Разница между требуемой пропускнойспособностью подсистемы памяти для видео-потоков телевидения высокой истандартной чёткости составляет > 13 раз, тогда как разница в общем объёмеданных не более 5 раз (1920 ∗ 1080/(720 ∗ 576) = 5).

²Неупрощаемый цикл определяется как цикл с несколькими точками вхо-да.

³Современными их аналогами, конечно же.

шин состояний. Это позволяет ускорить реализацию иоборот идей.

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

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

• Функциональные языки никак не встроены в процесс по-лучения конечного результата⁴. Мы не можем взять опи-сание компонента на функциональном языке и получитьописание на логических вентилях путём уточнения опи-сания. Все тесты придётся переносить на языки описанияаппаратуры отдельным этапом, весьма вероятно, вруч-ную.

Принимая во внимание цель прототипирования, «синтези-руемость» от прототипа не требуется, требуется только потак-товая аккуратность — совпадение временных диаграмм с точ-ностью до такта. Это позволяет существенно сократить уро-вень детальности модели, подняв при этом скорость разработ-ки и моделирования.

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

3.3. Моделирование аппаратуры с помо-щью функциональных языков

3.3.1. Общий подходОбщий подход прост: на основе текущего состояния и теку-

щих входных данных надо рассчитать текущие выходные дан-ные и состояние для следующего такта (k — номер компонен-та; состояние, входные и выходные сигналы представляют со-бой кортежи):

(Oki, Iki+1) = Fk(Iki,Ski)

Получается рекурсивная функция.Между компонентами практически всегда существует коль-

цевая зависимость. Её наличие приводит к необходимостиупорядочения вычислений выходов на основе входов. Нижеприведён RS-триггер на ИЛИ–НЕ элементах:

RS-триггер — это простое устройство с памятью на элемен-тах логики. При подаче 1 на проводA (вход S, Set) и 0 на проводB (вход R, Reset) мы должныполучить 1 на проводеC (выходQ,прямой выход), который останется при сбросе A в 0. И наобо-рот, при подаче пары 01 наABмы должныполучить на прямомвыходе 0, который останется на входе после сброса провода Bв 0.

⁴Lava [?], Hydra [?] и ForSyDe [?] позволяют надеяться на то, что ситуацияизменится в ближайшем будущем.

30 © 2009 «Практика функционального программирования»

Page 31: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

3.3. Моделирование аппаратуры с помощью функциональных языков

ÈËÈ-ÍÅ

ÈËÈ-ÍÅ

A

B

C

D

Выход одного элемента зависит от входа другого и суще-ствует вариант входных данных, когда эта схема находится всамовозбуждённом состоянии (если с самого начала подать наоба входа 1).

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

Одним из вариантов абстрагирования от порядка вычисле-ний является использование ленивых вычислений. Развитиесобытий во времениможно представить списком событий. Ес-ли скрестить эти два приёма, получатся ленивые списки собы-тий [?].

Этот подход не является новым и широко известен как де-композиция системы на «потоках» (streams). Подход хорошоописан в курсе SICP [?, ?, гл. 3.5] и является старейшим изизвестных подходов к моделированию состояния в «чистых»функциональных языках.

Ленивые списки могут применяться для обеспечения ввода-вывода⁵. Однако их применение в реальных программах за-труднено⁶, поскольку события из внешнего мира могут прихо-дить в произвольном порядке. В случае моделирования аппа-ратуры порядок фиксирован, и ленивые списки являются са-мым простым вариантом при использовании языков со ссыл-ками (Lisp, семейство ML, Haskell) или с ленивым порядкомвычислений (Clean, Haskell).

3.3.2. Вариант на языке HaskellНачнём сразу с примеров. Построим накапливающий сум-

матор—небольшой элемент с состоянием, которое равно сум-ме всех принятых входных значений. На выход будет посту-пать результат суммирования. Итак:

runningSum :: [Int] → [Int]runningSum inputs = sum

wheresum = 0 : (zipWith (+) sum inputs)

Вот его схема:

+ 0:

⁵В библиотеке языка Haskell этот атавизм можно наблюдать до сих пор [?].⁶Интересное обсуждение связанных с этим проблем содержится в [?].

Функция zipWith применит первый аргумент — двумест-ную функцию — к одинаковым по порядку элементам второгои третьего аргументов. (+) — это синоним безымянной функ-ции λxy. x+y. Оператор (:)— это оператор конструированиясписков. Слева находится голова списка, справа хвост.

Результат работы приведён ниже:

Prelude> runningSum [0,0,1,0,0,−1,10,0,0,−1][0,0,0,1,1,1,0,10,10,10,9]Prelude> runningSum [1,0,1,0,0,−1,10,0,0,−1][0,1,1,2,2,2,1,11,11,11,10]

Сумма поступает на выход с задержкой в один такт. Самыйпервый элемент всегда 0 — мы сформировали задержку путёмдобавления 0 в голову списка сумм. Второй элемент — функ-ция от 0 (sum[0]) и inputs[0]. Третий элемент — функцияот sum[1] (0+inputs[0]) и inputs[1], и так далее.

Второй пример будет чуть более сложным — RS-триггер наэлементах ИЛИ–НЕ. У него два входа и два выхода: вход R(RESET, сброс), вход S (SET, установка) и выходы Q (прямой)и Q’ (инверсный).

nor :: [Bool] → [Bool] → [Bool]nor xs ys = False : zipWith nor’ xs ys

wherenor’ a b = not (a ∣ ∣ b)

rsTrigger :: [Bool] → [Bool] → ([Bool], [Bool])rsTrigger r s = (q, q’)

whereq = nor s q’q’ = nor r q

Схема создания nor похожа на runningSum: вводим за-держку с помощью (False : вычисление), само вычисле-ние сводится к применению чистой функции к входам поэле-ментно.

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

Вот результат работы rsTrigger:

∗Main> rsTrigger (replicate 5 False) (replicate 5 False)([False,True,False,True,False,True],[False,True,False,True,False,True])∗Main> rsTrigger (True : replicate 4 False) (replicate 5 False)([False,True,True,True,True,True],[False,False,False,False,False,False])∗Main> rsTrigger (replicate5 False) (True : replicate 4 False)([False,False,False,False,False,False],[False,True,True,True,True,True])

Самый первый запуск приводит к самовозбуждению схемы.Второй (r активен) приводит к установке q в 0, сбросу. Третийпример показывает работу входа s.

В RS-триггере единицей времени моделирования являетсязадержка на элементе ИЛИ–НЕ. В накапливающем сумматореединица времени не указана, это такт работы какой-то схемы.В принципе, при моделировании сложной аппаратуры пользу-ются именно вторым вариантом, в качестве единицы модель-ного времени берётся один такт всей схемы.

© 2009 «Практика функционального программирования» 31

Page 32: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

3.3. Моделирование аппаратуры с помощью функциональных языков

Накапливающий сумматор интересен потому, что в нём те-кущее состояние и входы определяют значение текущих выхо-дов и следующего состояния. Такая схема весьма распростра-нена, она называется автоматом Мили (Mealy machine). Легкооформив её в отдельный примитив, можно писать только чи-стые функции преобразования данных.

:unzipunzip

inputs

state0

outputs

f

Вот приблизительный код этой функции ядра с примеромприменения — реализацией накапливающего сумматора:

mealy :: (state → input → (state,output))→ state→ [input]→ [output]

mealy f startState inputs = outputswhere

(nextStates,outputs) =unzip $ zipWith f states inputs

states = startState : nextStates

runningSumMealy :: [Int] → [Int]runningSumMealy = mealy (λsum i → (sum+i,sum)) 0

Результат работы runningSumMealy:∗Main> runningSumMealy [0,0,1,0,0,−1,10,0,0,−1][0,0,0,1,1,1,0,10,10,10]∗Main> runningSumMealy [0,0,1,0,0,−1,10,0,0,−1,0,0][0,0,0,1,1,1,0,10,10,10,9,9]

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

3.3.3. Вариант на языке ErlangЯзык программирования Erlang является строгим, и языко-

вые конструкции, соответствующие «потокам», в нем отсут-ствуют. Описываемый подход, тем не менее, возможно реали-зовать на базе очередей сообщений и процессов.

В данном случае каждый блок будет также представленбесконечно-рекурсивнойфункцией, работающей в своём про-цессе. Однако, вместо того, чтобыработать со списками, функ-ция должна принимать и отправлять сообщения. Логику по-сылки и отправки сообщений возможно выделить в отдель-ный модуль (как это сделано для модуля gen_server стандарт-ной библиотеки).

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

⁷Количество выходных элементов равно количеству входных из-за приме-нения zipWith.

⁸First class value — сущность, для которой доступны любые операции язы-ка.

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

Мы решили использовать Erlang для моделирования ап-паратуры из-за его существенно более простого синтаксиса.Опрошенные инженеры сказали, что модели на Erlang воспри-нимаются ими проще — аргументы функций в скобках болеепривычны, последовательность операций более прозрачна (кпримеру, where вHaskell «параллелен» в том смысле, что мож-но переставлять определения без изменения семантики).

3.3.4. Алгебраические типы и полиморфизмАлгебраические типы при моделировании аппаратуры ис-

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

В качестве примера можно привести описание команд мик-ропроцессора.

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

data Cmd = Nop∣ Load DestReg Reg∣ Add DestReg Reg Reg

После этапа чтения операндов структура команд не изме-нится, поменяется только содержимое тех полей, что имелитип Reg.

data ReadCmd = Nop∣ Load DestReg Word32∣ Add DestReg Word32 Word32

Разумно будет параметризовать команды:

data Cmd reg = Nop∣ Load DestReg reg∣ Add DestReg reg reg

Ниже приведены два примера: один из plasma [?] (свободнораспространяемая реализация ядраMIPS [?] наVHDL), другойиз тестовой реализации похожего ядра MIPS на Haskell.

Вот код декодера команд plasma:

entity control isport(opcode : in std_logic_vector(31 downto 0);

intr_signal : in std_logic;rs_index : out std_logic_vector(5 downto 0);rt_index : out std_logic_vector(5 downto 0);rd_index : out std_logic_vector(5 downto 0);imm_out : out std_logic_vector(15 downto 0);alu_func : out alu_function_type;shift_func : out shift_function_type;mult_func : out mult_function_type;branch_func : out branch_function_type;a_source_out : out a_source_type;b_source_out : out b_source_type;c_source_out : out c_source_type;pc_source_out: out pc_source_type;mem_source_out:out mem_source_type;exception_out: out std_logic);

end; −−entity control

architecture logic of control is

⁹Это справедливо и для динамически типизированных языков, таких какErlang, тем более, что полиморфизм типов данных в Erlang практически ничемне ограничен.

32 © 2009 «Практика функционального программирования»

Page 33: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

3.3. Моделирование аппаратуры с помощью функциональных языков

begin

control_proc: process(opcode, intr_signal)variable op, func : std_logic_vector(5 downto 0);variable rs, rt, rd : std_logic_vector(5 downto 0);variable rtx : std_logic_vector(4 downto 0);variable imm : std_logic_vector(15 downto 0);variable alu_function : alu_function_type;

beginalu_function := ALU_NOTHING;shift_function := SHIFT_NOTHING;mult_function := MULT_NOTHING;a_source := A_FROM_REG_SOURCE;b_source := B_FROM_REG_TARGET;c_source := C_FROM_NULL;pc_source := FROM_INC4;branch_function := BRANCH_EQ;mem_source := MEM_FETCH;op := opcode(31 downto 26);rs := ’0’ & opcode(25 downto 21);rt := ’0’ & opcode(20 downto 16);rtx := opcode(20 downto 16);rd := ’0’ & opcode(15 downto 11);func := opcode(5 downto 0);imm := opcode(15 downto 0);is_syscall := ’0’;

case op iswhen ”000000” ⇒ −−SPECIAL

case func iswhen ”000000” ⇒ −−SLL r[rd]=r[rt]<<re;

a_source := A_FROM_IMM10_6;c_source := C_FROM_SHIFT;shift_function := SHIFT_LEFT_UNSIGNED;

when ”000010” ⇒ −−SRL r[rd]=u[rt]»re;a_source := A_FROM_IMM10_6;c_source := C_FROM_shift;shift_function := SHIFT_RIGHT_UNSIGNED;

when ”011011” ⇒ −−DIVU s→ lo=r[rs]/r[rt];s→ hi=r[rs]%r[rt];

mult_function := MULT_DIVIDE;

when ”100000” ⇒ −−ADD r[rd]=r[rs]+r[rt];c_source := C_FROM_ALU;alu_function := ALU_ADD;

when ”000001” ⇒ −−REGIMMrt := ”000000”;rd := ”011111”;

...

Обратите внимание, что команды и их данные разнесены.Поэтому проверить соответствие команд даннымможно толь-ко пересмотром кода или тестированием.

Вот код декодера команд на Haskell:

type RI = IntSz FIVE −− register index.

data Dest = Dest RIderiving (Eq,Show)

data Imm5 = Imm5 (IntSz FIVE)deriving (Eq,Show)

data Signed = Signed ∣ Unsignedderiving (Eq,Show)

data MIPSCmd reg =Nop

∣ Trap∣ Add reg reg Dest∣ And reg reg Dest...

−−−− Decoder.

mipsDecode :: Word32 → MIPSCmd (IntSz FIVE)mipsDecode word = cmdwherehigh6 :: IntSz SIXlow26 :: IntSz SIZE26(high6,low26) = castWires wordbase_rs :: IntSz FIVErt :: IntSz FIVEoffset :: IntSz SIZE16(base_rs,rt,offset) = castWires low26cmd = case (high6,base_rs,rt) of

(0x00,_,_) → mipsDecodeSpecial low26(0x02,_,_) → J (Imm26 low26)(0x20,_,_) → LB Signed base_rs (RI rt) (Imm16 offset)(0x01,_,_) → mipsDecodeRegImm (base_rs,rt,offset)(0x07,_,_) → BGT base_rs rt (Imm16 offset)(0x06,_,_) → BLE base_rs rt (Imm16 offset)(0x05,_,_) → BNE base_rs rt (Imm16 offset)...

mipsDecodeRegImm ::(IntSz FIVE, IntSz FIVE, IntSz SIZE16)→ MIPSCmd (IntSz FIVE)

mipsDecodeRegImm (base_rs,rt,offset) = cmdwhere

cmd = case (base_rs,rt) of(0x00,0x11) → BAL (Imm16 offset)...

mipsDecodeSpecial :: IntSz SIZE26 → MIPSCmd (IntSz FIVE)mipsDecodeSpecial low26 = cmd

wherers, rt,rd, bits5 :: IntSz FIVEspecialOp :: IntSz SIX(rs,rt,rd,bits5,specialOp) = castWires low26cmd = case (rs,rt,rd,bits5,specialOp) of

(0,0,0,0,0) → Nop(rs,rt,rd,0x00,0x21) → AddU rs rt (RI rd)...

Все заметные отличия сводятся к возможности более эф-фективного синтеза описаний компонентов. Именно поэто-му данные декодера plasma идут по своим отдельным ши-нам. Зато при реализации выполнения команды на Haskell мыне перепутаем местами индекс регистра приёмника и одно-го из операндов. Нам не надо заводить типы ALU_NOTHING,MULT_NOTHING и им подобные, у нас есть тип Maybe. В об-щем и целом преобразования получаются проще и надёжнее.

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

Практически все конечные автоматы реализуются через

© 2009 «Практика функционального программирования» 33

Page 34: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

3.5. Заключение

примитив mealy, показанный выше. Функция преобразова-ния состояния — первый параметр mealy — чистая и мо-жет быть проверена на все краевые случаи отдельно от систе-мы. При тестировании не надо строить код, который приведётвнутреннее состояние в надлежащее, достаточно подать его навход нашей функции преобразования.

3.3.5. Потактовая точностьО потактовой точности следует рассказать чуть более по-

дробно. Рассмотрим пример работы конвейера типичногоRISC процессора.

clk

fetch/decode read execute memory write

fetch/decode read+exec memory write

Вверху показано изменение сигналов тактовой частоты, за-тем идёт конвейер команд для настоящего процессора [?], авнизу находится конвейер такой же длины в тактах, все опера-ции которого начинаются на одном и том же изменении так-товой частоты.

Видимые изменения будут производится на шагах memoryи write, где будут выполняться запросы к памяти и записи врегистровый файл. Завершение этапа write у второго вариан-та точно совпадает с завершением его же в первом варианте,завершение этапа memory сдвинуто на половину такта, но приэтом темп выполнения остался тот же.

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

3.4. Результаты применения подхода вжизни

Мы использовали язык Haskell для создания модели совре-менного микропроцессора с системой команд MIPS32 и вне-очередным выполнением команд (out-of-order execution), па-раллельно были созданы модель контроллера памяти и модельдинамической памяти. Модели контроллера памяти и самойпамяти служили гарантией, что модель нашего микропроцес-сорного ядра будет работать в условиях, максимально прибли-женных к реальным — нам было известно, что многие идеи,хорошие на бумаге, показывали плохие результаты при соеди-нении с обычной памятью со всеми её задержками.

Над проектом работала команда из двух программистов идвух инженеров; по паре «инженер с программистом» на мо-дель ядра и на модель инфраструктуры памяти. Инженеры вы-ступали в роли консультантов и контролёров, их время ис-пользовалось только частично.

Уже через четыре месяца после запуска работ проект смогпоказать работу системы на разных задачах с учётом всех ин-тересовавших нас параметров. Известные нам примеры менеесложных проектов (без модели памяти, только ядро с модельюкэша) потребовали нескольких человеко-лет для достижениятакого же уровня модели.

Модель системы получилась высоко параметризованной: 14параметров ядра процессора (размер кэшейи связанных буфе-ров, размер окна предпросмотра, параметры предпросмотра ит. д.) и 9 параметров контроллера памяти (параметры алгорит-ма и размеры буферов) и самой памяти (пропускная способ-ность).

Общий объём кода модели—3400 полезных строк кода.Мо-дель ядра микропроцессора — 2100 строк кода.

Модель опровергла некоторые наши первоначальные пред-положения, основным из которых являлся тезис о возможно-сти (с использованием доступных нашей команде ресурсов)построения микропроцессорного ядра, способного декодиро-вать видео высокого разрешения. После провала прототипамикропроцессорамырешилииспользовать специализирован-ные аппаратные модули для декодирования видео и сосре-доточиться на проблемах, критичных для нашей системы-на-кристалле: видеоконтроллер для телевидения высокой чётко-сти, декодирование и демультиплексирование потоков циф-рового телевидения, DRM¹⁰, интеграционные задачи разногоуровня.

Возвращаясь к вопросу о скорости, стоит отметить прове-дённое нами неформальное сравнение моделей двух, пример-но одинаковых по функциональности, устройств. Одна из мо-делей была написана на SystemC, другая — на Haskell. Модельна языке Haskell показала заметное увеличение скорости мо-делирования по сравнению с моделью на SystemC, от 2 раз (свключёнными отчётами о работе устройств) до 100 (без отчё-тов). Причины столь гигантского разрыва кроются во «встро-енной» в ленивые языки «оптимизации» — отчёты просто невычислялись, а расходы на протягивание отложенных вычис-лений по иерархии компонентов минимальны. Второй причи-ной является то, что к ленивым спискам были применены ме-тоды оптимизации, давно и хорошо известные функциональ-ному сообществу [?].

3.5. ЗаключениеНаш опыт говорит о том, что функциональные языки

вполне применимы для моделирования цифровых электрон-ных компонентов.

Помимо простоты написания описаний моделей, функцио-нальные языки дают ещё и высокую скорость моделирования.

Строгая система типов с выводом типов и алгебраическимитипами данных позволяет описывать типичные микроэлек-тронные решения просто и безопасно.

3.6. Краткийобзор библиотекмоделиро-ванияаппаратуры

LavaСтраничка: http://raintown.org/lava/Самый известный язык описания аппаратуры, встроенный

в Haskell. Схема описывается комбинаторами.Алгебраические типы данных не поддерживаются.

HydraСтраничка: http://www.dcs.gla.ac.uk/~jtod/

Hydra/

¹⁰Digital Rights Management, защита видео- и аудиоинформации.

34 © 2009 «Практика функционального программирования»

Page 35: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

3.6. Краткий обзор библиотек моделированияаппаратуры

Язык описания аппаратуры.Позволяет описывать комбина-торную логику, получать нетлисты¹¹ (результат синтеза).

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

Алгебраические типы данных не поддерживаются.

Hierarchical Sorting Dataflow MachineСтраничка: http://thesz.mskhug.ru/svn/

hiersort/ (SVN)Пример модели аппаратуры с использованием бесконечных

списков. Ядро модели находится в подкаталоге core, содержитпорядка 25 строк кода и может быть использовано для напи-сания других моделей аппаратуры.

Получение синтезируемого описания модели не предусмот-рено.

Алгебраические типы данных поддерживаются.

Другие языкиДля OCaml было создано два языка описания аппарату-

ры: HDCaml и Confluence. Оба языка больше не поддержи-ваются, оставшиеся исходные коды можно найти на http://funhdl.org/. Ни в одном из них не было сделано попыт-ки реализовать поддержку ни алгебраических типов данных,ни полиморфизма.

¹¹Netlist — очень подробный уровень описания аппаратуры, представляетиз себя просто список компонентов с соединяющими их проводами.

© 2009 «Практика функционального программирования» 35

Page 36: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

Использование Scheme в разработке семейства продуктов «Дозор-Джет»

Алексей Отт[email protected]

Аннотация

Данная статья представляет собой краткий обзор использования функционального программирования в разра-ботке семейства продуктов «Дозор-Джет» — программного обеспечения для контентной фильтрации трафика, при-меняемого для предотвращения утечек производственной информации, и соблюдения законодательства в части со-хранения информации [2], [1].

is article provides a brief overview of the use of the functional programming in the development of the «Dozor-Jet» family ofproducts. e «Dozor-Jet» soware employs traffic content filtering to provide enterprise information leak prevention and ensurelegal compliance.

Обсуждение статьи ведётся по адресуhttp://community.livejournal.com/fprog/2368.html.

*Я хочу поблагодарить своих коллег по «Инфосистемам Джет» за помощь в написании этой статьи.

Page 37: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

4.2. Архитектура систем

4.1. Что такое «Дозор-Джет»?Семейство продуктов «Дозор-Джет»¹ состоит из нескольких

продуктов, реализующих функциональность по фильтрациипочтового и веб-трафика. История данного семейства продук-тов началась в 1999 году с разработки системы мониторингаи архивирования почтовых сообщений (СМАП) для одногокрупного заказчика. Первая версия продукта² была поставле-на заказчику в начале 2000 года [4]. Постепенно систему нача-ли внедрять и у других заказчиков, и она стала обретать черты«коробочного» продукта. К 2006 году система была внедренауже у более, чем 200 клиентов, среди которых были крупныегосударственные и коммерческие организации, объем продаждостиг нескольких миллионов долларов в год³.

В 2005 году был выпущен еще один продукт семейства«Дозор-Джет»—система контроля веб-трафика (СКВТ) [3], наоснове которой были затем разработаны и другие продуктыэтого семейства.

Отличительной чертой линейки продуктов по сравнению сконкурирующими продуктами являлась возможность постро-ения сложных условий обработки трафика, поддержка всех ис-пользуемых в России и СНГ кодировок⁴, автоматическое опре-деление типов файлов и кодировок документов, большое ко-личество поддерживаемых форматов документов и архивов.Все это позволяло создавать очень гибкие политики безопас-ности и предотвращать утечки важной для организации ин-формации.

С точки зрения программиста эта линейка продуктов инте-ресна тем, что при её разработке активно использовался языкпрограммирования Scheme (а именно, PLT Scheme⁵). СМАП«Дозор-Джет» практически полностью написан на этом язы-ке (за исключением небольших платформенно-зависимых ча-стей, написанных на языке C), а в СКВТ Scheme используетсядля реализации серверной части веб-интерфейса.

4.2. Архитектура системВ данном разделе приводится краткое описание архитекту-

ры продуктов семейства «Дозор-Джет»⁶.

4.2.1. Архитектура СМАП «Дозор-Джет»СМАП «Дозор-Джет» может функционировать в двух ре-

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

СМАП «Дозор-Джет» состоит из нескольких взаимодей-ствующих между собой подсистем (см. рис. 4.1). К основнымподсистемам относятся:

¹http://www.jetso.ru/²Первая версия продукта была разработана силами трех разработчиков, и

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

³Данные по продажам взяты из пресс-релиза компании, опубликованногов журнале Jet Info, №10 за 2006-й год, стр. 22.

⁴Это было одной из проблем при использовании продуктов иностранныхкомпаний.

⁵http://www.plt-scheme.org/⁶Описания архитектуры взяты из официальной документации соответ-

ствующих продуктов, которую вы можете найти на сайте компании: http://www.jetsoft.ru/. Документация также содержит подробное описаниевозможностей продуктов данного семейства.

• Подсистема приема почтовых сообщений, обеспечива-ющая прием почты от внешних клиентов и серверов.На этом этапе производится первоначальная фильтрацияпочтовых сообщений для предотвращения рассылки спа-ма и несанкционированной отправки почты через поч-товый сервер. Эта подсистема была реализована на базеSMTP-прокси из другого продукта компании — Z–2.

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

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

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

• Подсистема архивации, реализующая интерфейс к базеданных (Oracle или PostgreSQL) и обеспечивающая рабо-ту с почтовыми сообщениями, хранимыми в базе данных.

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

Практически все подсистемы СМАП, за исключением под-системы приема почтовых сообщений и клиентской частиподсистемы управления, написаны на языке Scheme.

Модульная архитектура СМАП позволяет разнести разныеподсистемы по нескольким серверам (когда это было необ-ходимо), обеспечивая балансировку нагрузки между сервера-ми и надежность работы комплекса. Эксплуатация продуктау больших клиентов показала, что система может обрабаты-вать десятки гигабайт почтовых сообщений в день даже прииспользовании одного сервера фильтрации (при выделенномсервере базы данных).

4.2.2. Архитектура СКВТ «Дозор-Джет»В отличие от СМАП, СКВТ «Дозор-Джет» работает только

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

• Подсистема фильтрации трафика, выполняющая провер-ку передаваемых данных на соответствие политике без-опасности, а также архивирование передаваемой инфор-мации (веб-почта и т. п.) в СМАП «Дозор-Джет»;

© 2009 «Практика функционального программирования» 37

Page 38: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

4.4. Использование DSL

Рис. 4.1. Архитектура СМАП «Дозор-Джет»

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

• Подсистема кэширования данных на базе кэш-сервераSquid.

В данном продукте на языке Scheme написана только сер-верная часть подсистемы управления, при этом используют-ся те же самые библиотеки, что и в подсистеме управле-ния СМАП «Дозор-Джет». В связи с этим реализация веб-интерфейса СМАП «Дозор-Джет» практически полностью со-ответствует реализации интерфейса СКВТ «Дозор-Джет».

4.3. Почему Scheme?Язык Scheme⁷ был выбран по ряду причин, подробнее опи-

санных ниже:

• Простота и выразительность языка. Scheme — доста-точно простой язык, с легковесной средой выполнения.Существовавший на то время стандарт языка, R5RS, за-нимал около 50 страниц, в отличие от значительно более«подробного» Common Lisp. Простота языка позволялабыстро вводить новых разработчиков в курс дела, дажеесли до прихода в компанию они не имели опыта разра-ботки на языке Scheme.

В процессе разработки использовались макросы Scheme,с помощью которых сильно сокращался объем кода, ко-торый необходимо было поддерживать, и программа пи-салась в терминах целевой предметной области. Стоит от-метить, что размер кода системыСМАП (вместе с различ-ными модулями расширений и дополнительными утили-тами) составляет порядка 35 тысяч строк на Scheme инесколько тысяч строк кода на языках C и С++. Для срав-нения, размер кода одного из конкурирующих продуктов

⁷Для первых версий СМАП «Дозор-Джет» использовалась PLT Schemev103, но позднее, в 2004 году, был выполнен переход на версию PLT Schemev30x, в которой реализовано много полезных вещей, таких как FFI, системамодулей и т. п.

(с меньшей функциональностью), написанного на язы-ке C++, составляет более 200 тысяч строк.

Сокращение размера кода позволило уменьшить общееколичество ошибок в продукте, а достаточно небольшойпроцент кода на C и C++ позволил избежать типичныхдля этих языков ошибок, таких как утечки памяти иошибки доступа к памяти.

• Возможность интерактивной разработки. ИспользуяREPL, разработчик может писать и тестировать код безпересборки всего продукта, что существенно ускоряетразработку частей продукта. За счет более короткого цик-ла разработки время вывода продуктов на рынок было су-щественно сокращено.

• Возможность выполнения сгенерированного кода во вре-мя выполнения программы — политика безопасности, со-зданная администраторомбезопасности, преобразовыва-ется в исходный код Scheme, проводится оптимизацияэтого кода и выполняется как часть программы, обновля-ясь на лету, без перезапуска процессов.

• Переносимость. Кроссплатформенность Scheme поз-волила использовать СМАП «Дозор-Джет» на разныхUnix-совместимых операционных системах — различ-ные дистрибутивы Linux, Sun Solaris на процессорахSparc и x86/AMD, HP-UX на процессорах PA-RISC.Платформенно-зависимая часть продукта составлялнесколько сотен строк на языке C и в основном ис-пользовалась для реализации привязки к различнымбиблиотекам, в том числе и для баз данных.

Еще одной причиной выбора этого языка, было наличие на-работок в части создания веб-приложений на языке Scheme —наличие готовых компонентов, что также позволило умень-шить время вывода продукта на рынок.

4.4. Использование DSLВ продуктах семейства также используются и Domain-

Specific Languages — для библиотеки определения типов, со-зданной для замены библиотеки libmagic и утилиты file,

38 © 2009 «Практика функционального программирования»

Page 39: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

4.5. Реализация СМАП

был спроектирован отдельный язык с Lisp-образным синтак-сисом. Данный язык позволяет разработчику или админи-стратору безопасности описывать процедуру точного опреде-ления типа файла, используя логические операторы, и функ-ции доступа к содержимому определяемого файла ([2], с. 35).Пример процедуры определения типов, написанной на этомязыке приведен ниже:

; RAR a r c h i v e s( and (= @0 ” Rar ! \ x1a ” )

( b y t e @44 )(<= ( b y t e @35 ) 4 ) )

=> ( fo rmat ”RAR a r c h i v e da ta , v~x , ” ( b y t e @44 ) )=> ( c a s e ( b y t e @35 )

( ( 0 ) ” os : MS−DOS” )( ( 1 ) ” os : OS / 2 ” )( ( 2 ) ” os : Win32 ” )( ( 3 ) ” os : Unix ” ) )

(= @0 ”MZ” )> ( and ( l e l o n g @# x3c )

(= @( l e l o n g @# x3c ) ”PE \ x0 \ x0 ” ) )=> ”MS Windows PE”

> ( and ( l e l o n g @# x3c )(= @( l e l o n g @# x3c ) ”NE” ) )

=> ”MS Windows 3 . x NE”> # t

=> ”MS−DOS e x e c u t a b l e ”=> ( and (= @24 ”@” ) ” \ b , OS/2 or MS Windows ” )=> ( and ( s e a r c h ” Rar ! ” @# xc000 10000)

” \ b , Rar s e l f − e x t r a c t i n g a r c h i v e ” )=> ( and (= @11696 ”PK \ 0 0 3 \ 0 0 4 ” )

” \ b , PKZIP SFX a r c h i v e v1 . 1 ” )

В приведенном примере показаны процедуры определениятипов для архивов в формате RAR, а также для исполняе-мых файлов MS DOS и MS Windows. Как видно из приме-ра, язык позволяет описывать достаточно сложные проверки,недоступные в существующих библиотеках, включая объявле-ние и использование локальных (для правил) переменных.

Политики безопасности СМАП также можно рассматри-вать какпримерпрограммынаDSL.Обладая соответствующи-ми знаниями, можно модифицировать политику, не прибегаяк использованию веб-интерфейса. Сформированная админи-стратором политика безопасности, компилируется в програм-му на Scheme, загружается в среду выполнения, и автоматиче-ски начинает применяться ко всем новым почтовым сообще-ниям. Пример скомпилированной политики безопасности выможете увидеть ниже:

(define |filter−file:Confidential data| (make−filter−file ”filter-file.100”))(define |message:Confidential data|

’(((”from” ”admin@localhost”)(”to” ”security_admin@localhost”)(”subject” ”Confidential data found!”))”Mail with confidential data was found””Confidential data”))

(define (|cond:All mail| message)(log−condition ”cond:All mail” (lambda (message) #t) message))

(define (|cond:Executable files| message)(log−condition ”cond:Executable files”(lambda (message)

(iter−mime−part1(rfc822:message−info−body (filtering−info−message message))(lambda (x)

(string−contains−ci? (mime−part−content−type x) ”executable”))))message))

(define (|cond:Confidential data| message)(log−condition ”cond:Confidential data”(lambda (message)

(iter−mime−part1(rfc822:message−info−body (filtering−info−message message))(lambda (x)

(text−match−regexp−list?(mime−part−text−file x)

(filter−file−escaped−regexp−list|filter−file:Confidential data|)))))

message))(define (|set:Template rule set| message)

(call/ec(lambda (return)

(let ((result (|cond:All mail| message)))(when result (action:relay−message message) (return #t)))

#f)))(define (|set:Main Rule| message)

(call/ec(lambda (return)

(let ((result (|cond:Confidential data| message)))(when result

(action:archive−message message)(action:send−notification message #t |message:Confidential data|)(return #t)))

(let ((result (|cond:Executable files| message)))(when result (action:void) (return #t)))

(let ((result (|cond:All mail| message)))(when result (action:relay−message message) (return #t)))

#f)))|set:Main Rule|

В данной политике определяется несколько условий («Allmail», «Confidential data» и «Executable files») и действий, изкоторых формируется политика безопасности под названием«Main Rule». Данная политика выполняет следующие действияс проходящей почтой:

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

• удаляет письмо в том случае, если в письме передается ис-полняемый файл;

• доставляет адресатам всю остальную почту.

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

4.5. Реализация СМАП4.5.1. Подсистема фильтрации

Подсистема фильтрации является самой важной подсисте-мой СМАП «Дозор-Джет» — она обрабатывает всю почту, по-падающую в систему, и, в зависимости от политики безопас-ности, определенной администратором безопасности, прини-мает решение о дальнейшей её судьбе.

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

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

⁸В СМАП практически для всех параметров используются собственныепроцедуры определения корректных значений формата файлов, кодировкитекста и т. д. Это позволяло избежать проблем с неправильным указанием па-раметров письма в MUA, а также правильно обрабатывать письма с целена-правленно измененным содержимым: переименованными файлами и т. п.

© 2009 «Практика функционального программирования» 39

Page 40: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

Литература Литература

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

В текущей версии продукта все условия по умолчанию явля-ются «ленивыми» — вычисление соответствующих парамет-ров (разбор заголовков письма, извлечение вложений из пись-ма, извлечение текста из документов) откладывается до перво-го вызова соответствующего условия. Это позволяет снизитьнагрузку на систему, особенно в тех случаях, когда политикабезопасности достаточно простая и не требует разбора всехвложений.

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

4.5.2. Веб-интерфейсВеб-интерфейс администратора безопасности состоит из

серверной и клиентской частей. Клиентская часть написана наJavaScript и использует AJAX для получения данных от сервераи представления их в браузере.

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

Пример интерфейса администратора безопасности вы мо-жете видеть на рис. 4.2.

4.6. Основные результатыКак показывает десятилетний опыт разработки и эксплуата-

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

Литература[1] Алексей Отт. О контентной фильтрации. Продолжение

темы // Jet Info. — 2006. — № 10. — С. 2–21.

[2] Олег Слепов. Контентная фильтрация // Jet Info. — 2005. —№ 10.

⁹В последних версиях СКВТ вместо связки Apache + mod_mzscheme ис-пользуется веб-сервер, поставляемый вместе с PLT Scheme.

[3] Олег Слепов, Алексей Отт. Контроль использованияИнтернет-ресурсов // Jet Info. — 2005. — № 2.

[4] Александр Таранов, Владимир Цишевский. Система мо-ниторинга и архивирования почтовых сообщений // JetInfo. — 2001. — № 9. — С. 10–28.

40 © 2009 «Практика функционального программирования»

Page 41: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

Литература Литература

Рис. 4.2. Пример интерфейса СМАП «Дозор-Джет»

© 2009 «Практика функционального программирования» 41

Page 42: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

Как украсть миллиардили Давайте сделаем это по-быстрому

Александр Самойлович[email protected]

Аннотация

В статье описывается создание программы типа crawler, которая обходит интернет-сайт, находит и сохраняет надиске данные нужных типов.Программа будет написана на языке Erlang. Разработка программыбудет вестись «сверхувниз». Цель статьи — показать, как свойства Erlang позволяют значительно ускорить разработку и выполнение про-граммы по сравнению с использованием других языков, не требуя при этом от программиста практически никакихдополнительных усилий.

e article describes the creation of a spider soware which crawls the web site in search of files of certain types, and saves themto disk. e program is written in Erlang using the «top-down» approach. is article aims to show how Erlang allows for significantacceleration of development process and minimization of the run time of the program compared to other programming languageswith no extra effort on the part of the programmer.

Обсуждение статьи ведётся по адресуhttp://community.livejournal.com/fprog/2735.html.

Page 43: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

5.1. Введение

5.1. ВведениеЭта программа появилась в результате решения приклад-

ной задачи. Несколько лет назад мне понадобилось выкачатьс некоторого сайта хранящиеся на нем картинки. Картинокбыло много, примерно несколько тысяч, может быть, десят-ки тысяч. Прихотливой рукой автора они были щедро раз-бросаны по всему сайту. Придумать простой алгоритм опи-сания того, где они лежат, мне не удалось. Поэтому в кон-це концов была написана программа, которая обходила весьсайт, находила картинки и сохраняла их. Задача оказалась до-статочно удобной для использования в целях самообразова-ния. С тех пор, начиная изучать новый язык программирова-ния, я пишу на нем очередную реализацию этой программы.Предпоследняя из них была составлена на Scheme. Програм-ма работала и выдавала разумные результаты, но дождатьсяокончания её работы мне не удалось. Насколько я понимаю,единственным ограничением производительности была одно-поточность. Программа была переписана на Erlang. Этот про-цесс и воспроизводится в статье.

ДокументацияДокументации, статей и книг про Erlang сейчас существу-

ет намного меньше, чем, например, про Haskell. На англий-ском языке выпущено две книги: Programming Erlang (ав-тор Joe Armstrong) и Erlang Programming (авторы FrancescoCesarini и Simon ompson). Описание языка, примеры ииные материалы можно найти на официальном сайте Erlang:www.erlang.org. Например, по адресу http://www.erlang.org/download/getting_started-5.4.pdf можно най-ти введение в Erlang. Собрание немногочисленных «рецеп-тов» (cookbook) лежит здесь: http://schemecookbook.org/Erlang/WebHome.

Алгоритм работыАлгоритм работы нашей программы прост:1) получаем текст страницы;2) находим в ней все интересующие нас ссылки;3) в зависимости от типа ссылки либо сохраняем ссылку (не

содержимое), либо переходим к пункту 1, либо игнориру-ем её;

4) когда все интересующие нас ссылки найдены, сохраняемсодержимое этих ссылок на диске.

Сохранение картинки разделено на два шага (1—3 и 4) спе-циально. Сначала мы сохраняем только ссылку на картинку, апотом получаем ее содержимое. Так было сделано потому, чтоне все картинки были мне интересны, и хотелось иметь воз-можность редактировать список ссылок для сохранения вруч-ную.

АрхитектураОдна из главных особенностей Erlang — легковесные про-

цессы. В связи с этим проектирование программ на Erlang —это разбиение задач на независимые процессы и определениеспособов взаимодействия между ними. Процессы в Erlang со-здаются легко и стоят дешево, поэтому наличие тысяч или де-сятков тысяч процессов в одной задаче — вполне обычное де-ло. Процессы в Erlang можно рассматривать как аналоги клас-сов в C++ или Java. Для каждой задачи (независимой актив-ности) создаётся отдельный процесс. Процессымогут обмени-ваться между собой сообщениями. Посылка сообщений асин-

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

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

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

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

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

Замечание о долгоживущих процессахКак уже говорилось, типичная программа на Erlang состо-

ит из большого количества процессов. В разных задачах этипроцессы имеют схожее поведение. Они создаются, контроли-руют поведение друг друга, обмениваются сообщениями и т. д.Такие общие черты поведения называютсяшаблонами поведе-ния. В реализацию Erlang входит библиотека OTP, в которой,помимо прочего, реализованышаблоны поведения процессов,чтобы разработчику не было необходимости каждый раз за-ново это поведение программировать. Но так как мы хотим нетолько уметь пользоваться библиотечными абстракциями, нои знать, как они устроены, мы организуем долгоживущий сер-висный процесс вручную, используя рекурсию, не прибегая кпомощи модуля gen_server из библиотеки OTP.

Организация кода, модулиФайлы с кодом программ на Erlang называются модулями,

расширение файлов с исходным текстом должно быть .erl. Из-нутри модуля его функции могут быть вызваны просто поимени, а снаружи модуля — по длинному имени, состоящемуиз названия модуля и названия функции. Наш модуль будетназываться download.erl, а функция, которая обходит сайти сохраняет адреса файлов на диске— start(). Снаружи, на-пример, из командной строки Erlang, эта функция будет вызы-ваться как download:start().

© 2009 «Практика функционального программирования» 43

Page 44: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

5.2. Разработка

В начале каждого модуля обязательно идёт заголовок, со-держащий имя этого модуля, которое должно совпадать с име-нем файла без расширения. После заголовка мы описываем,какие функции можно вызывать извне этого модуля. По умол-чанию, все функции модуля доступны только изнутри, сна-ружи модуля они не видны. Для того, чтобы функцию мож-но было вызывать извне, она должна быть описана командой−export(...). К ней мы вернемся в конце статьи. А сейчас,для упрощения отладки, сделаем все функции модуля публич-ными:

−module(download).−compile(export_all).

Замечания об отладкеВжизни программа создавалась «сверху вниз», практически

в той же последовательности, как это представлено в статье.Работоспособность каждой вновь написанной функции про-верялась путем ее вызова в командной строке Erlang с соответ-ствующими параметрами. Для того, чтобы скомпилироватьфункцию более высокого уровня без реализации всех функ-ций более низкого уровня, нам необходимо создать функции-заглушки. Например, пусть нужно написать такую заглушкудля функции f(P1, P2). Если возвращаемое значение нас не ин-тересует, то заглушка может выглядеть как:

f(_P1, _P2) −> ok.

Символы подчеркивания перед аргументами — это сообще-ние компилятору о том, что в дальнейшем аргументы не ис-пользуются, и их значения не важны. Если этого не сделать,компилятор выдаст предупреждение о неиспользованных пе-ременных.

В Erlang есть настоящие отладчики, например, debugger, нодля нашего приложения вполне достаточно диагностическихсообщений в консоли.

КомментарииКомментарии в Erlang однострочные. Комментарием счита-

ется все, начиная с символа % и до конца строки. Коммента-рии разных уровней принято выделять разным количеством%. Например, комментарии, относящиеся ко всему модулю —%%%, комментарии, описывающиефункцию—%%, а коммен-тарии к строке кода — %.

КонстантыВо время работы нам потребуется несколько параметров,

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

get_start_base() −> ”http://airwar.ru/”.get_start_url() −> get_start_base() ++ ”image/”.out_file_name() −> ”pictures.txt”.

5.2. РазработкаГлавная функция обхода

Функция start() будет вызываться из командной строки.В начале работы она произведет инициализацию библиотеки

inets, которая будет читать для нас данные из интернета. За-тем мы создадим процесс Writer, записывающий на диск ин-тересующие нас ссылки. Каждый процесс, который хочет что-то записать, должен будет послать процессу Writer сообщениеоб этом. Функция обработки веб-страниц process_page()сделает всю работу по разбору текстов страниц и определениютипов найденных ссылок. И, наконец, мы информируем запи-сывающий процесс, что все данные обработаны, и он можетзавершать свою работу.

start() −>inets:start(),Writer = start_write(),process_page(Writer, get_start_url()),stop_write(Writer).

Процессы создаются стандартной функцией spawn(). Призапуске процесса ей передаётся функция процесса. Процессзавершится вместе с завершением этой функции. Сам жеspawn() возвращает родителю идентификатор запущенногопроцесса-потомка, «pid». Полученный идентификатор можноиспользовать в качестве адреса при посылке процессу сообще-ний: Pid !AnyMessage.

Мыхотим, чтобынашпроцессжил достаточно долго для то-го, чтобы обработать неизвестное заранее число сообщений,поэтому мы будем использовать рекурсию, которая позволитнам принять и обработать потенциально неограниченное ко-личество сообщений одно за другим. Один вызов функциислужит для обработки одного сообщения. Накапливать дан-ные мы будем, передавая их аргументом рекурсивно вызывае-мой функции. Так как вначале работы список сохраненных ад-ресов картинок пуст, аргументом для первого вызова будет пу-стой список [].

У функции spawn() есть несколько вариантов вызова. Вэтой статье мы будем пользоваться только одним их них:

start_write() −> spawn(fun write_proc/0).

Здесь мы передали в spawn() безаргументную функциюwrite_proc(), которую определим ниже. Также начальнаяфункция процесса, передаваемая в spawn(), может созда-ваться и при помощи конструкции вида:

fun(A) −> A + 42 end

Так порождается анонимная функция, аналогλα → α + 42 в Haskell. Мы ещё воспользуемся этимспособом.

Управлять процессом Writer мы будем при помощи двух со-общений — write и stop. Для их посылки используем двефункции:

stop_write(W) −>W ! stop.

write(W, String) −>W ! {write, String}.

Функция stop_write() посылает сообщение, состоящееиз одного атома stop. Функция write() для посылки со-общения использует пару {write, String}, состоящую изатома write и произвольных данных String. Цель введениятаких однострочныхфункций для посылки сообщения—изо-ляция логики программы от деталей реализации.

44 © 2009 «Практика функционального программирования»

Page 45: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

5.2. Разработка

Функция обработки цикла сообщений процессаWriter

Функция обработки цикла сообщений процесса Writerдолжна знать о существовании сообщений stop и {write,String}, описанных в предыдущем разделе. Для получениясообщений в Erlang в общем случае используется конструк-ция:

receivePattern1 when Guard1 −> expr_1_1, ..., expr_1_N;Pattern2 when Guard2 −> expr_2_1, ..., expr_2_N;...PatternM when GuardM −> expr_M_1, ..., expr_M_N

afterTimeout −> expr_1, ..., expr_N

end

Ветвь будет выполнена, только если будет найдено соответ-ствие PatternN, и при этом GuardN будет истинным. Guard —это выражение, которое принимает значениеtrue или false.Часть when Guard не является обязательной. В этой статьеона нам не понадобится, поэтому выражение receive будетвыглядеть так:

receivestop −> expr_1_1, ..., expr_1_N;{write, String} −> expr_2_1, ..., expr_2_N;

afterTimeout −> expr_1, ..., expr_N

end

Дойдя до блока receive, процесс остановится и будетждать, пока в очереди сообщений что-то не появится. Еслиочередь не пуста, программа попытается найти соответствиемежду сообщением и образцами, описанными в разных вет-вях receive. Сопоставление будет удачным только в том слу-чае, если удалось найти соответствие для всех величин, вхо-дящих в данные. Сейчас у нас таких образцов два — stopи {write, String}. Если соответствие найдено, программавыполнит правую часть выражения, если нет—программа бу-дет ожидать в receive бесконечно. Бесконечно? Но это же нето, что нам надо. Что будет, если по каким-то причинам со-общение вообще не дойдет? Тогда функция никогда не закон-чится, и процесс не завершится. Для обработки этого случая ииспользуется ветвь after Timeout. Если в течение Timeoutмиллисекунд ни одного соответствия между пришедшим со-общением и ветвью receive не будет найдено, то выполнитсяветвь after:

write_proc() −> write_loop([], 0).

write_loop(Data, DataLen) −>receive

stop −>io:format(”Saving ~b entries~n”, [DataLen]),{ok, F} = file:open(out_file_name(), write),[io:format(F, ”~s~n”, [S]) || S <− Data],file:close(F),io:format(”Done~n”);

{write, String} −>%io:format(”Adding: ~s~n”, [String]),case DataLen rem 1000 of

0 −> io:format(”Downloaded: ~p~n”,[DataLen]);

_ −> okend,

write_loop([String|Data], 1 + DataLen)after 10000 −>

io:format(”Stop on timeout~n”),stop_write(self()),write_loop(Data, DataLen)

end.

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

Если придёт сообщение {write, String}, тоwrite_loop/2 для каждого тысячного сообщения напе-чатает диагностику на консоль и вновь вызовет себя, уже сновыми аргументами. Рекурсия, а именно, концевая рекур-сия — это обычный способ организации циклов в Erlang. Таккак переменные в Erlang не изменяются, для сохранения дан-ных между вызовами функций используются их аргументы.В нашем случае мы добавляем величину String, полученнуюиз сообщения, к списку данных Data, который был у нас намомент вызова функции write_loop/2. После этого рекур-сивного вызова процесс Writer окажется там же, где и доприхода сообщения — в ожидании сообщения в инструкцииreceive.

В нашем случае ветвь after служит упрощённым обработ-чиком ошибок. Его логика такова: если в течение некоторо-го времени мы не получили ни одного сообщения о том, чтонужно сохранить какие-то данные, это значит, что все, что мо-жет быть найдено, уже записано. Если какие-либо процессыне смогли завершиться нормально к этому моменту, то скореевсего это обозначает, что они уже никогда и завершатся в свя-зи с неизвестными нам ошибками. Величина 10 секунд выбра-на произвольно. Она должна быть достаточно велика, чтобыпревышать возможные задержки, возникающие при нормаль-ной работе сайта.

Чтобы избежать дублирования кода, в ветви after процесспосылает сообщение stop себе же, а затем вызывает функ-цию write_loop/2, чтобы его обработать. Просто выйти изфункции мы не хотим, так как реальная запись данных в файлпроисходит именно при обработке сообщения stop. Посылкаstop из Timeout позволит нам сохранить на диске те данные,которые уже накопились к этому моменту.

Одна из первых версий программы не содержала спискаобработанных данных Data. Она немедленно записывала ихна диск при получении сообщения {write, String}. Обра-ботчик сообщения stop только закрывал файл. К сожалению,получилось не очень хорошо. Файловые операции в Erlang,предоставляемые библиотечным модулем io, довольно мед-ленны, поэтому процесс записи на диск работал недостаточнобыстро. Собрать как можно больше данных в памяти, а затемзаписать их в один прием — это один из примеров оптими-зации скорости работы, рекомендованный ДжоАрмстронгом,одним из авторов языка.

Обработка одной страницыВеб-страницу мы будем обрабатывать построчно,

чтобы упростить нашу учебную задачу. Сначала про-читаем исходный текст страницы, вызвав функциюget_url_contents(), которую мы вскоре опишем. Ес-ли чтение закончилось с ошибкой, то есть возвращенноезначение отличается от {ok, Data}, немедленно закончимфункцию. Если чтение было успешным, разобьем полученный

© 2009 «Практика функционального программирования» 45

Page 46: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

5.2. Разработка

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

process_page(W, Url) −>MyPid = self(),case get_url_contents(Url) of

{ok, Data} −>Strings = string:tokens(Data, ”\n”),Pids = [spawn(fun() −>

process_string(W, MyPid, Url,Str)

end) || Str <− Strings],collect(length(Pids));

_ −> okend.

В этом коде наибольший интерес представляет функцияcollect(). Её назначение — ждать, пока все процессы обра-ботки строк не завершатся:

collect(0) −> ok;collect(N) −>

%io:format(”To collect: ~p~n”, [N]),receive

done −> collect(N − 1)end.

Реализация этой функции состоит из двух ветвей. Привызове функции Erlang попытается найти соответствиемежду формальными и фактическими параметрами. Еслиcollect() вызвана с нулевым аргументом, она вернет атомok, и на этом всё закончится. Если же она вызвана с положи-тельным аргументом, она будет ждать в receive до тех пор,пока не получит сообщение done, после чего она вызовет себяже с уменьшенным аргументом и снова будет ждать сообще-ния. Это значит, что, будучи вызвана, функция collect(N)не завершится, пока не получит N сообщенийdone. Так какмы вызвали её с аргументом, равным количеству запущенныхпроцессов-обработчиков строк, она будет ждать до тех пор,пока количество полученных сообщений done не сравняетсяс количеством строк в странице.

Получение содержимого URL адресаДля чтения данных, расположенных по известному

URL, мы будем пользоваться библиотечной функциейhttp:request(). В случае успеха чтения (коды возврата200 и 201) мы вернем прочитанное содержимое. Если ошибкапроизошла по вине сервера (коды возврата 5XX), то мыподождем и вновь повторим попытку. В случае любой другойошибки вернем атом failed, что будет означать, что чтениене удалось. Если чтение не удалось по вине библиотеки http(ветвь {error, Why}), мы тоже повторим попытку чтенияпосле паузы.

get_url_contents(Url) −> get_url_contents(Url, 5).get_url_contents(Url, 0) −> failed;get_url_contents(Url, MaxFailures) −>

case http:request(Url) of{ok, {{_, RetCode, _}, _, Result}} −> if

RetCode == 200;RetCode == 201 −>{ok, Result};

RetCode >= 500 −>% server error, retry%io:format(”HTTP code ~p~n”, [RetCode]),

timer:sleep(1000),get_url_contents(Url, MaxFailures−1);

true −>% all other errorsfailed

end;{error, _Why} −>

%io:format(”failed request: ~s : ~w~n”, [Url, Why]),timer:sleep(1000),get_url_contents(Url, MaxFailures−1)

end.

Функция обработки одной строкиПри обработке строки мы попытаемся найти в этой строке

URL. Если он будет найден, мы его обработаем. При выходеиз функции сообщим родительскому процессу, что функцияобработки строки завершилась. Это приведет к уменьшениюсчетчика ожидания collect() на 1.

process_string(W, Parent, Dir, Str) −>case extract_link(Str) of

{ok, Url} −> process_link(W, Dir, Url);failed −> ok

end,done(Parent).

done(Parent) −>Parent ! done.

Функция done() посылает родительскому процессу сооб-щение, состоящее из одного атома done.

Извлечение URL из строкиИзвлечениеURLиз строкиHTMLпроизвольного вида—за-

дача непростая. Мы будем решать её очень приблизительно,при помощи регулярного выражения. Используемый методимеет множество ограничений: он не найдет URL, разбитыйна несколько строк, или второй URL в строке. Но для нашихучебных целей он вполне подойдет. В случае успеха мы вернёмпару, состоящую из атома и найденной величины {match,Value}, а в случае неуспеха — только атом failed:

extract_link(S) −>case re:run(S, ”href *= *([ >̂]*)>”, [{capture,

all_but_first, list}]) of{match, [Link]} −> {ok, string:strip(Link, both,

$”)};_ −> failed

end.

Обработка URLДля определения типа входного URL вновь воспользуемся

регулярными выражениями. Типов URL, которые мы умеемраспознавать, три:image — ссылка на картинку,page — ссылка на другую страницу иstrange — URL, не являющийся ни одним из двух предыду-

щих типов.Найдя картинку, мы пошлем процессу Writer сообщение отом, что этот адрес нужно сохранить. Если адрес соответству-ет типу page, для него необходимо вызвать ту же функциюprocess_link(), с которой начались наши вычисления вфункции start(). Для типа strange мы просто напечатаемдиагностику.

46 © 2009 «Практика функционального программирования»

Page 47: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

5.2. Разработка

process_link(W, Dir, Url) −>case get_link_type(Url) of

image −> process_image(W, Dir ++ Url);page −> process_page(W, Dir ++ Url);_ −> process_other(W, Dir ++ Url)

end.

%% Site−specific heuristics.get_link_type(Url) −>

{ok, ReImg} = re:compile(”\\.(gif|jpg|jpeg|png)”,[extended, caseless]),

{ok, RePage} = re:compile(”̂ [^/]+/$”),case re:run(Url, ReImg) of

{match, _} −> image;_ −> case re:run(Url, RePage) of

{match, _} −> page;_ −> strange

endend.

process_image(W, Url) −>write(W, Url).

process_other(_W, _Url) −>%io:format(”Unexpected URL: ~p~n”, [Url])ok.

Результат работы функции start()Мызакончили часть кода, которая обходит сайт и сохраняет

в файле адреса картинок для последующей обработки. Ника-кая дополнительная сборка программы не требуется. В моемслучае уже через несколько секунд работы в файле pictures.txtоказалось записано более 55 тысяч адресов.

Главная функция сохраненияОтредактировав файл с адресами картинок pictures.txt, при-

ступим с получению самих картинок из сети и сохранению ихна диске.

save(Path) −>inets:start(),{ok, Data} = file:read_file(Path),L = string:tokens(binary_to_list(Data), ”\n”),save_loop(0, L).

Вызов функции download:save() происходит независи-мо от вызова функции download:start(). Между двумяэтими вызовами сессия Erlang не обязана сохраняться. Послетого, как start() отработает, мы вполне можем закрыть сес-сию, отредактировать файл со ссылками на картинки, а со-хранять их через пару дней. Поэтому мы вновь проинициа-лизируем модуль inets, читающий для нас данные из ин-тернета. Сначала прочитаем файл с адресами картинок. Приэтом функция file:read_file() вернёт данные в бинар-ном формате. Превратим бинарные данные в список (а стро-ки в Erlang — это списки) и разобьем одну длинную строку начасти, считая перевод строки разделителем. В конце вызовемфункцию, осуществляющую цикл сохранения.

Ограничение количества процессовОдна из первых версий программы запускала независимый

процесс сохранения для каждого адреса. К сожалению, это незаработало. Я не смог получить ни одной картинки. Я не знаю

истинной причины. Возможно, модуль http не захотел рабо-тать со слишкомбольшимколичествомодновременных соеди-нений. Может быть, сайт оказался не готов к такой нагруз-ке. Мы воспользуемся решением, которое успешно обходитвсе возможные трудности одновременно. Ограничим коли-чество одновременно читающих/пишущих процессов, напри-мер, двумя сотнями. Число 200 выбрано экспериментальнымпутем. Разницы в скорости сохранения при 20 и 200 процес-сах я не заметил, но совершенно точно, что 200 процессов ра-ботают быстрее, чем 2. С другой стороны, число процессов недолжно быть очень большим, чтобы не натолкнуться на огра-ничение операционной системы на количество одновременнооткрытых файлов.

Логика ограничения количества процессов реализована вфункции save_loop(). Она включает в себя четыре ветви.

save_loop(0, []) −>io:format(”saving done~n”, []);

save_loop(Running, []) −>receive

done −>io:format(”to save: ~p~n”, [Running]),save_loop(Running − 1, [])

end;save_loop(Running, [U|Us]) when Running < 200 −>

S = self(),spawn(fun() −> save_url(S, U) end),save_loop(Running + 1, Us);

save_loop(Running, Us) −>receive

done −>io:format(”to save: ~p~n”, [Running +

length(Us)]),save_loop(Running − 1, Us)

end.

Первая ветвь выполнится в том случае, если запущенныхпроцессов уже не осталось, s и список входных URL для со-хранения пуст. Эта ветвь прервет цикл сохранения.

Вторая ветвь выполнится в том случае, если список входныхURLпуст, а количество запущенных процессов—любое. Циклсохранения будет ждать в receive до тех пор, пока какой-нибудь процесс не сообщит о своём завершении. Тогда циклснова вызовет себя же, уменьшив счётчик исполняемых про-цессов на 1.

Третья ветвь вызовется при непустом входном списке и ко-личестве исполняющихся процессов меньшем двухсот. Циклзапустит новый процесс сохранения и вновь вызовет себя же,увеличив счетчик исполняющихся процессов на 1.

Четвертая ветвь выполнится в том случае, если не найденони одного из предыдущих соответствий. Она отвечает случаю,когда количество запущенных процессов превышает макси-мально разрешённое. Эта ветвь будет ждать в receive до техпор, пока один из процессов не закончится, после чего сновавызовет save_loop(), уменьшив счётчик процессов на 1.

Сохранение одного URLСохраняя содержимое URL, мы должны по URL сформиро-

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

© 2009 «Практика функционального программирования» 47

Page 48: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

5.3. Заключение

save_url(Parent, Url) −>Path = url_to_path(Url),ensure_dir(”.”, filename:split(Path)),case get_url_contents(Url) of

{ok, Data} −>%io:format(”saving ~p~n”, [Url]),file:write_file(Path, Data);

_ −> okend,done(Parent).

Функция url_to_path() жульнически проста. Она отку-сывает от абсолютного URL протокол и имя сервера:

url_to_path(Url) −>string:substr(Url, length(get_start_base())+1).

Функция sure_path() в качестве входных аргументов ис-пользует имя текущей директории и список, состоящий извсех промежуточных директорий и имени файла. Этот списокполучается в результате вызова filename:split(Path):

ensure_dir(_Dir, [_FileName]) −> ok;ensure_dir(Dir, [NextDir|RestDirs]) −>

DirName = filename:join(Dir, NextDir),file:make_dir(DirName),ensure_dir(DirName, RestDirs).

Если входной список директорий состоит только из именифайла, функция ничего не делает, в противном случае она вы-числяет имя очередной директории, создает её при помощиfile:make_dir(DirName) и вновь вызывает себя с толькочто вычисленной текущей директорией и укороченным спис-ком, лишенным головы.

ExportНаша программа закончена. Осталось только привести её

в соответствие с правилами хорошего тона программиро-вания на Erlang. Сейчас все функции, описанные в моду-ле download.erl, видны снаружи. Для запуска программы мыпользуемся только двумя из них— start и save. Сделаем ви-димыми только их. Для этого заменим:−compile(export_all).

на:−export([start/0, save/1]).

Величины /0 и /1 называются арностью и обозначают коли-чество аргументов функции. Функции с одинаковыми имена-ми, но разной арностью являются разными функциями.

5.3. ЗаключениеКак уже упоминалось, программа которая находит на сайте

картинки, выкачивает их и сохраняет на диске, была сначаланаписана на Scheme, а потом на Erlang. При практически оди-наковом размере кода и одинаковых временных затратах наразработку, программа на Erlang работает в сотни раз быстрее.Это обусловлено природой задачи, которая легко поддаётсяраспараллеливанию. Поддержка параллельности на Erlang нетребует от программиста практически никаких дополнитель-ных усилий. Трудности синхронизации процессов на Erlangне идут ни в какое сравнение с теми, с которыми приходит-ся сталкиваться, программируя многопоточное приложение вC или C++. Модель процессов, принятая в Erlang, позволяетпрограммисту самостоятельно определять удобные ему меха-низмы синхронизации.

48 © 2009 «Практика функционального программирования»

Page 49: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

Алгебраические типы данных и их использование в программировании

Роман Душкин[email protected]

Аннотация

Статья рассматривает важную идиому программирования — алгебраический тип данных (АТД). Приводитсятеоретическая база, которая лежит в основе практического применения АТД в различных языках программирова-ния. Прикладные аспекты рассматриваются на языке функционального программирования Haskell, а также краткона некоторых других языках программирования.

Algebraic Data Types (ADT) is an important programming idiom. A theoretical background is given that forms a foundationfor practical application of ADT in various programming languages. Practical aspects of ADTs are discussed using Haskell and arealso briefly outlined for certain other programming languages.

Обсуждение статьи ведётся по адресуhttp://community.livejournal.com/fprog/2921.html.

Page 50: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

6.1. Мотивация

ВведениеВ 1903 году английский математик Бертран Рассел предло-

жил антиномиюврамках языка классической («наивной») тео-рии множеств Георга Кантора, которая показала несовершен-ство введённого им определения множества: «множество естьмногое, мыслимое как единое»¹ [7, 12]:

Пусть K — множество всех множеств, которыене содержат сами себя в качестве своего подмно-жества. Ответ на вопрос «содержит ли K самосебя в качестве подмножества?» не может бытьдан в принципе. Если ответом является «да», то,по определению, такое множество не должно бытьэлементом K . Если же «нет», то, опять же по опре-делению, оно должно быть элементом самого себя.

В общем, куда ни кинь — всюду клин. Ситуация парадок-сальна.

Данная антиномия (более известная под названием «пара-докс Рассела») поколебала основы математики и формальнойлогики, что вынудило ведущих математиков того времени на-чать поиск методов её разрешения. Было предложено несколь-ко направлений, начиная от банального отказа от теоретико-множественного подхода в математике и ограничения в ис-пользовании кванторов (интуиционизм, основоположникомкоторого был голландский математик Лёйтзен Брауэр), до по-пыток аксиоматической формализации теории множеств (ак-сиоматика Цермело — Френкеля, аксиоматика Неймана —Бернайса — Гёделя и некоторые другие). На сегодняшнийдень аксиоматические теории множеств, дополненные аксио-мой выбора или другими аналогичными аксиомами, как рази служат одним из возможных оснований современной мате-матики.

Позже австрийский философ Курт Гёдель показал, чтодля достаточно сложных формальных систем всегда найдут-ся формулы, которые невозможно вывести (доказать) в рам-ках данной формальной системы — первая теорема Гёделяо неполноте [14]. Данная теорема позволила ограничить поис-ки формальных систем, дав математикам и философам пони-мание того, что в сложных системах всегда будут появлятьсяантиномии, подобные той, что предложил Б. Рассел.

В конечном итоге парадокс Рассела и запущенные им на-правления исследований в рамках формальных систем приве-ли к появлению теории типов, которая, наряду с упомянутымиаксиоматическими теориями множеств и интуиционизмом,является одним из способов разрешения противоречий наив-ной теории множеств. Сегодня под теорией типов понимаетсянекоторая формальная система, дополняющая наивную тео-рию множеств [13]. Теория типов описывает области опреде-лений и области значений функций — такие множества эле-ментов, которые могут быть значениями входных параметров

¹Впрочем, Г. Кантор дал достаточно чёткое математическое определениемножества [2]:

Unter einer «Menge» verstehen wir jede Zusammenfassung M von bestimmtenwohlunterschiedenen Objekten m unserer Anschauung oder unseres Denkens (welchedie «Elemente» von M genannt werden) zu einem Ganzen.

«Под «множеством»мыпонимаемпроизвольную коллекциюM в целом, со-стоящую из отдельных объектов m (которые называются «элементами» M»),которые существуют в нашем представлении или мыслях». Данное определе-ние показывает, что Г. Кантор заложил основы перехода математики от ту-манных размышлений к точным символическим формулировкам.

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

В частности, решение приведённой в начале статьи антино-мии было предложено самим Б. Расселом как раз в рамках тео-рии типов. Решение основано на том, что множество (класс)и его элементы относятся к различным логическим типам, типмножества выше типа его элементов. Однако многие матема-тики того временинеприняли это решение, считая, что онона-кладывает слишком жёсткие ограничения на математическиеутверждения.

В рамках общей теории типов разработано определённоеколичество теорий, абстракций и идиом, описывающих раз-личные способы представления множеств значений и мно-жеств определений функций. Одна из них — алгебраическийтип данных (другими важнейшими теориями в рамках дис-кретной математики являются комбинáторная логика, и тео-рия рекурсивных функций) [10, 6, 11]. Именно эта идиомаи является предметом рассмотрения настоящей статьи, по-скольку она имеет серьёзное прикладное значение в информа-тике в целом и в функциональном программировании в част-ности. К примеру, в языке Haskell любой непримитивный типданных является алгебраическим.

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

К примеру, если говорить о простом типизированном , тов нём даже нет констант типов вроде Int, Bool и т. д. Типы— это выражения специального вида, составленные из знач-ков (→), (∗) и круглых скобок «(» и «)» по простым правилам:

1) ∗— тип;

2) если α и β — типы, то (α→ β)— тип.

Другими словами, типы—это строки вида∗→ (∗→ ∗)→ ∗.Именно строки, а не множества значений. Иногда, конечно,вводят и константы типов, но принципиальной разницы этоне вносит.

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

6.1. МотивацияПеред рассмотрением теоретических основ АТД имеет

смысл сравнить реализацию этой идиомы на языке програм-

²Вместе с тем, типы в информатике появились из-за необходимости со-поставлять идентификатору внутреннее представление идентифицируемогообъекта. Типы в информатике и типы в математике — это немного разные по-нятия. Понятие типов в информатике основано на математическом понятии,но не совсем тождественно ему (имеются расширения, необходимые для прак-тических применений). Робин Милнер, автор и главарь разработчиков функ-ционального языкаML, был однимиз первых, кто попытался применить мате-матические типы для выбора внутреннего представления программных сущ-ностей [4], что породило определённую путаницу.

50 © 2009 «Практика функционального программирования»

Page 51: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

6.1. Мотивация

мирования, в котором в явном виде это понятие отсутствует(например, язык семействаC) с описанием тогожеАТДна язы-ке, где это понятие является естественным (наиболее «продви-нутым» в отношении АТД является язык Haskell).

Например, пусть имеется задача реализовать тип данных,представляющий двоичное дерево³, при этом такой тип дол-жен предоставлять разработчику возможности построитьопределённые дополнительныемеханизмы контроля внутрен-ней структуры данных и доступа ним, к примеру — осуществ-лять проверки корректности значений, присваиваемых от-дельным внутренним полям. Далее эти дополнительные тре-бования к типам будут рассмотрены во всех подробностях.

Пусть имеется обычное определение структуры, при помо-щи которой выражается двоичное дерево (в таком дереве дан-ные, по сути, хранятся только в узловых вершинах; «листовой»считается вершина, у которой оба поддерева пусты):

struct Tree {int value;struct Tree *l;struct Tree *r;

};

Этот вариант определения не совсем подходит для опи-санных целей, потому что для доступа к элементам этойструктуры, а также для разнообразных проверок целостностии непротиворечивости, потребуется писать дополнительныефункции⁴. Как бы сделать так, чтобы транслятор автоматиче-ски делал за разработчика «чёрную работу» по созданию вспо-могательных программных конструкций?

Структуру Tree можно переопределить примерно следу-ющим образом (впрочем, это — не совсем корректное пере-определение, поскольку в этом случае данные будут хранить-ся только в листовых вершинах, а не в любых вершинах де-рева; тем не менее, такое переопределение вполне достаточнодля целей статьи):

struct Tree {union {

int value;struct {

struct Tree *left;struct Tree *right;

} branches;} data;

enum {LEAF,NODE

} selector;};

В этом определении имеются два взаимозависимых элемен-та: объединение data и перечисление selector. Первый эле-мент содержит данные об узле дерева, а второй идентифици-рует тип первого элемента. Если в качестве значения в объеди-нении data содержится поле value, то значением элементаselector должно быть LEAF. Соответственно, если в первом

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

⁴Например, для данного конкретного определения необходимо написатьслужебную функцию проверки того, что указатели l и r ненулевые, а еслии нулевые, то эта ситуация корректна (нулевые указатели на дочерние под-деревья могут быть только у листовых вершин).

элементе находится структура branches, скрывающая в се-бе два указателя на такие же двоичные деревья (левое и пра-вое поддерево), то во втором элементе должно быть значе-ние NODE. Опять налицо необходимость иметь внешние по от-ношению к этому определению инструменты, которые следятза семантической непротиворечивостью значений типа⁵.

Разработчику придётся в явномвиде писать примерно такиефункции для доступа на запись к полям определённой вышеструктуры:

void setValue (Tree *t, int v) {t−>data.value = v;t−>selector = LEAF;

}

void setBranches (Tree *t, Tree *l, Tree *r) {t−>data.branches.left = l;t−>data.branches.right = r;t−>selector = NODE;

}

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

Итак видно, что определение типа Tree в языке типа C по-лучилось достаточно громоздким, но при этом была решенаважная задача — здесь явно определена возможность выбо-ра между двумя альтернативами: (value, LEAF) и (branches,NODE). Такой тип позволяет, по сути, хранить совершенно раз-нородные данные в зависимости от своего назначения в каж-дом конкретном случае — для листовых элементов двоичногодерева хранятся числовые значения, для узловых — указате-ли на левое и правое поддеревья соответственно. Для понима-ния того, какой именно тип используется в каждом таком кон-кретном случае, имеются «метки» из перечисления selector.Но цена этого — дополнительные «метки» и множество явноописываемых проверок в функциях доступа.

А вот определение того же типа данных на языке програм-мирования Haskell:

data Tree = Leaf Int∣ Node Tree Tree

Это определение описывает тип, который может быть пред-ставлен двумя видами значений (все такие виды разделенысимволом вертикальной черты ( ∣)). Первый вид значений, по-меченный «меткой» Leaf, представляет собой целое число,хранимое в листовой вершине двоичного дерева. Второй видзначений — узловая вершина дерева, хранящая ссылки на ле-вое и правое поддеревья (соответственно, используется меткаNode).

А вот тотже тип, но уже годный для хранения значенийпро-извольного типа, а не только целых чисел:

data Tree α = Leaf α∣ Node (Tree α) (Tree α)

⁵Конечно, эту задачу можно реализовать через класс, но ничего иного, какпомещение структуры данных ифункций для её обработки под одним именемпрограммной сущности, это не даст — фактически всё будет то же самое.

© 2009 «Практика функционального программирования» 51

Page 52: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

6.2. Теоретические основы

Здесь переменная типов α является «заменителем» для лю-бого другого типа (можно даже придумать ситуацию, когдав листовых вершинах двоичного дерева хранятся двоичныедеревья и т. д. до бесконечности⁶). В языках типа C (C++, C#,Java и др.) для этих же целей можно воспользоваться шаблона-ми, и заинтересованный читатель может самостоятельно ре-ализовать такой шаблон, а также функцию для контроля це-лостности значений к нему (после чего можно будет сравнитьопределения, хотя бы по количеству использованных скобок).

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

Можно подвести итоги о преимуществам АТД, выделив яв-ные положительные моменты в использовании этой идиомыфункционального программирования:

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

2) АТД — это программная сущность для определения га-рантированно безопасных размеченных объединений.

3) Наконец,АТДпредоставляют возможность описания вза-имно рекурсивных типов данных, то есть являются ком-пактной статически безопасной реализацией связныхструктур.

Для понимания отличительных особенностей и преиму-ществ АТД в программировании далее будут представле-ны теоретические аспекты этого понятия (с применениемнескольких математических формул уровня первого курсатехнического вуза), после чего в двух последних разделах бу-дут приведены способы реализации теоретического понятияв прикладных языках программирования. Основное повест-вование ведётся на языке Haskell, даётся краткое описание тойчасти синтаксиса языка, которая связана с АТД. Для другихязыков программирования, где явно реализованы АТД, про-сто приводятся примеры определений.

6.2. Теоретические основыВ теории есть два способа описания АТД. Первый использу-

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

⁶В информатике такие деревья называются «деревьями высшего порядка»(англ. high-order trees).

читабельно описывать на математическом языке типы данныхв рамках теории типов. Интересно видеть, как данная нотациябыла преобразована при реализации языков программирова-ния.

6.2.1. Определение АТДАлгебраический тип данных неформально можно опреде-

лить как множество значений, представляющих собой неко-торые контейнеры, внутри которых могут находиться значе-ния каких-либо иных типов (в том числе и значения того жесамого типа — в этом случае имеет место рекурсивный АТД).Множество таких контейнеров и составляет сам тип данных,множество его значений.

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

С теоретической точки зрения алгебраическим типом дан-ных является размеченное объединение множеств (иначе на-зываемое «дизъюнктным объединением»)⁷, под которым по-нимается видоизменённая классическая операция объедине-ния — такая операция приписывает каждому элементу ново-го множества метку (или индекс), по которой можно понять,из какого конкретно множества элемент попал в объедине-ние. Соответственно, каждыйиз элементов размеченного объ-единения в свою очередь является декартовым произведениемнекоторых иных множеств.

Пусть есть набор множеств Ai, i ∈ I , из которых создаётсяих размеченное объединение. В этом случае под размеченнымобъединением понимается объединение пар:

∐i∈I

Ai =⋃i∈I{(x, i) ∣ x ∈ Ai}.

Здесь (x, i) — упорядоченная пара, в которой элементу xприписан индекс множества, из которого элемент попал в раз-меченное объединение. В своюочередь каждое измножествAi

канонически вложено в размеченное объединение, то естьпересечения канонических вложений A∗i всегда пусты, дажев случаях, когда пересечения исходных множеств содержаткакие-либо элементы. Другими словами, каноническое вложе-ние имеет вид:

A∗i = {(x, i) ∣ x ∈ Ai},

а потому

∀i, j ∈ I, i ≠ j ∶ A∗i ∩A∗j = ∅.

Итак, АТД — это размеченное объединение, то есть элемен-ты такого типа с математической точки зрения представляютсобой пары (x, i), где i — индекс множества (метка типа), от-куда взят элементx. Но чем являются сами элементыx? Теорияговорит о том, что эти элементы являются декартовыми про-изведениями множеств, которые содержатся внутри контей-неров Ai. То есть, каждое множество Ai, из которых собира-ется размеченное объединение, является декартовым произ-ведением некоторого (возможно, нулевого) числа множеств.Именно эти множества и считаются «вложенными» в контей-нер АТД, вложение обеспечивает операция декартова произ-ведения.

⁷Для заинтересованных читателей имеет смысл дать англоязычное наиме-нование термина — tagged union или disjoint union.

52 © 2009 «Практика функционального программирования»

Page 53: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

6.2. Теоретические основы

Другими словами, каждое множество Ai представляет со-бой декартово произведение:

Ai = Ai1 ×Ai2 × . . . ×Ain,

где множества Aik, k = 1, n являются произвольными (в томчисле нет ограничений на рекурсивную вложенность). По-скольку элементами декартова произведения множеств явля-ются кортежи вида

x = (x1, x2, . . . xn),в целом АТД можно записать как

∐i∈I

Ai =⋃i∈I{((x1, x2, . . . xni), i)}. (6.1)

В общем случае декартово произведение вообще можетбыть представлено пустым кортежем. Тогда считается, что со-ответствующий контейнер Ai не содержит никаких значенийвнутри себя, а в размеченное объединение канонически вкла-дывается единственный элемент этого множества — ((), i).Данная ситуация возможна тогда, когда вАТДвключаетсямет-ка i ради самой себя, то есть в АТД содержится нуль-арное ка-ноническое множество A∗i , имеющее единственный элемент.Обычно это требуется для определения перечислений (эта си-туация и её реализация будут продемонстрированы далее встатье).

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

ÀÒÄ A

Part A*1

. . .

1 x1 x2 xn...

1 x1 x2 xn...

1 x1 x2 xn...

Part A*2

. . .

2 y1 y2 ym...

2 y1 y2 ym...

2 y1 y2 ym...

Part A*N

. . .

N z1 z2 zl...

N z1 z2 zl...

N z1 z2 zl...

. . .

Ìåòêà рàçìå÷åííîãî îáúåäèíåíèÿ

Äåêàрòîâî ïрîèçâåäåíèå

Êàíîíè÷åñêîå âëîæåíèå ìíîæåñòâà AN

Рис. 6.1. Схема произвольного АТД

В качестве примера АТД в математической нотации можнорассмотреть тип Tree, введённый ранее:

data Tree α = Leaf α∣ Node (Tree α) (Tree α)

Данный тип есть размеченное объединение двух множествLeaf и Node, так что A1 ≡ Leaf , A2 ≡ Node. Множество A1

есть декартово произведение одного произвольного множе-ства a. Значения этого множества «упаковываются» в контей-нер A1. Соответственно, множество A2 есть декартово произ-ведение двух одинаковых множеств Tree(a), то есть налицорекурсивное определение АТД.

6.2.2. Синтаксически-ориентированное кон-струирование

Как было показано выше, при создании АТД использу-ются две операции: декартово произведение и размеченное

объединение. Эти операции вместе с понятиями теории ти-пов были взяты за основу так называемого синтаксически-ориентированного подхода к конструированию типов, пред-ложенного Чарльзом Энтони Хоаром [1]. Дополнительно о ти-пах в языках программирования можно прочесть в [5].

Данный подход предлагает более удобную нотациюдля представления типов, нежели формальные математиче-ские записи в теоретико-множественной нотации. В даннойнотации типы именуются словами английского языка, на-чинающимися с заглавной буквы, причём конкретные типыимеют вполне конкретные названия: List, Tree и т. д., а пе-ременные типов (то есть такие обозначения, вместо которыхможно подставлять произвольный тип) — просто буквыс начала алфавита, возможно с индексами: A, B, C1, Cn и т. п.

Также в качестве ключевых слов в нотации Ч. Хоара исполь-зуются слова constructors, selectors, parts и predicates (а так-же эти же слова в форме единственного числа). Данные клю-чевые слова используются для ввода наименований отдельных«элементов» АТД. Под «элементами» АТД понимаются различ-ные сущности в составе типа — отдельные размеченные мно-жества и типы, упакованные в контейнеры.

• Конструкторы (constructors) — это наименования функ-ций, создающих декартовы произведения из состава АТД.

• Селекторы (selectors) — это специальные утилитарныефункции, обеспечивающие получение отдельных значе-ний из декартовых произведений.

• Части (parts) — наименования отдельных каноническихмножеств размеченного объединения.

• Предикаты (predicates) — функции, позволяющие иден-тифицировать принадлежность заданного значения кон-кретному множеству из состава размеченного объедине-ния.

Далее в настоящем разделе каждый из этих элементов опи-сывается более подробно.

Наконец, два символа, (+) и (×), используются для записиопределений типов. Знак (+) обозначает размеченное объеди-нение, а знак (×)используется для обозначения декартова про-изведения.

В качестве примера определения АТД в данной нотацииможно привести классическое определение АТД «списокэлементов типа A»:

List(A) = NIL + (A ×List(A))

nil, prefix = constructorsList(A)head, tail = selectorsList(A)NIL,nonNIL = partsList(A)null, nonNull = predicatesList(A)

Здесь A — произвольный тип данных, так называемая пе-ременная типов, вместо которой в конкретных случаях можноподставлять любой необходимый тип. Например, если необ-ходимо иметь список целых чисел, то достаточно подставитьInt вместо всех вхождений символа A.

На представленном примере можно пояснить основныепонятия нотации синтаксически-ориентированного констру-ирования. Первая строка определения описывает сам тип

© 2009 «Практика функционального программирования» 53

Page 54: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

6.2. Теоретические основы

List(A). Список — это размеченное объединение пусто-го списка NIL и декартова произведения элемента типа Aсо списком таких же элементов. Значением типа List(A) мо-жет быть либо пустой список, либо непустой, который естьпара (декартово произведение двух множеств), первым эле-ментом которой является значение обёртываемого типа, а вто-рым—список (в том числе и пустой). Это значит, что «в конце»каждого конечного списка должен находиться пустой списоккак базис рекурсии.

Следующие четыре строки определяют «элементы» АТД«список». Первая из них определяет два конструктора, каж-дый из которых соответствует одной из частей размеченногообъединения. Конструктор nil создаёт пустой список NIL,конструктор prefix создаёт непустой список соответственно.Этот конструктор принимает на вход значение заданного типаи список, а возвращает пару, первым элементом которой яв-ляется указанное значение, вторым — список. Следовательно,типы конструкторов можно определить так⁸:

#nil = List(A)#prefix = A→ (List(A)→ List(A))

Таким образом, тип конструктора типа Ai, являющегося де-картовым произведением типов Ai1,Ai2, . . . ×Ain, определя-ется формулой:

#constructorAi = Ai1 → (Ai2 → . . . (Ain → A) . . .),

то есть конструктор одного декартова произведения принима-ет на вход значения «оборачиваемых» типов (тех, которые по-мещаются в контейнер), а возвращает значение целевого, свое-го АТД⁹. Все частиАТД, каждая из которых есть декартово про-изведение, имеют по одному конструктору. Сам АТД являет-ся в таком случае размеченным объединением частей, а частиобозначаются ключевым словом parts.

Для всех конструкторов, которые создают «контейне-ры», имеются так называемые селекторы. Селектор — этофункция, которая возвращает одно определённое значениеиз контейнера. В случае типа List(A) селекторы есть толькоу части nonNIL (непустой список). Первый селектор headвозвращает голову списка, то есть значение типа A, а второйtail — хвост списка, то есть второй элемент пары в декар-товом произведении. Типы селекторов можно понять по ихназначению:

#head = List(A)→ A#tail = List(A)→ List(A)

Другими словами, каждый селектор имеет тип видаA→ Aik, и такой селектор принимает на вход значениетипа АТД, а возвращает заданное значение из контейнера.Селекторы имеют место только для декартовых произведе-ний, а для каждой части типа имеется столько селекторов,сколько типов упаковывается в соответствующий контей-нер. Для каждой части типа верно следующее равенство,

⁸Запись #x означает «тип значения x» (в теории типов функции такжеимеют типы, поэтому в качестве значения x может выступать и функция).

⁹Здесь необходимо отметить, что в теории и функциональном про-граммировании имеется понятие «обобщённого алгебраического типа дан-ных» (ОАТД), конструкторы которого могут в общем случае возвращать зна-чения не своего типа.

называемое «аксиомой тектоничности»¹⁰:

∀x ∈ Ai ∶ constructorAi(si1x)(si2x) . . . (sinix) = x,

где si1, si2, . . . sini — селекторы соответствующих компонен-тов декартова произведения.

Уже упомянутые части типа — это множества, включённыевАТДпосредствомразмеченного объединения. ДляАТДопре-деляются предикаты, при помощи которых можно выявить,к какому конкретно множеству в рамках размеченного объ-единения относится значение. Наличие таких предикатов —одно из свойств размеченности объединения. Соответствен-но, сколько в АТД частей, столько и предикатов. Части задают-ся ключевым словом parts, предикаты — predicates. Для пре-дикатов верна следующая аксиома:

(x ∈ Ai)⇒ (Pix = 1)&(∀j ≠ i ∶ Pjx = 0).

Наличие такой аксиомы необходимо для того, чтобыдля произвольного значенияАТДможно было выявить ту кон-кретную часть, к которой это значение принадлежит. Далееможно будет применять селекторы конкретной части (при-менение селекторов к значению из другой части приведётк ошибке согласования типов — необходимо помнить о типеселекторов). Соответственно, при реализации в языках про-граммирования такие предикаты позволяют использовать ме-ханизм сопоставления с образцом (подробно рассказываетсяниже в разделе про язык Haskell).

Нотация Ч. Э. Хоара также позволяет представлять АТДв виде диаграмм, на которых представлено древовидное опи-сание структуры АТД. Такие деревья состоят из двух или трёхуровней. На первом уровне изображается вершина АТД с егонаименованием. На втором уровне перечисляются части ти-па. Если часть представляет собой декартово произведение, тодля данной части на третьем уровне перечисляются типы ком-понентов части. Рёбра дерева, ведущие с первого на второйуровень, помечаются наименованиями предикатов. Соответ-ственно, рёбра, ведущие со второго на третий уровень, поме-чаются наименованиями селекторов.

Произвольный АТД выглядит так, как показано на рис. 6.2.В качестве примера представленияАТДв виде дереваможно

привести диаграмму для типа List(A), показанную на рис. 6.3.На приведённой диаграмме пунктирной линией показано ре-курсивное включение типаList(A) в качестве одного из своихкомпонентов.

6.2.3. Примеры описания АТДВ заключение теоретического введения можно привести

ряд примеров определений различных АТД. Так, следующееопределение используется для типа с поэтическим названием«розовый куст»¹¹:

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

¹¹Необходимо отметить, что тип Rose(A) взят из Standard Template Library.Этот тип являются достаточно широко используемым контейнерным типом.Впрочем, в STL имеются определения огромного количества контейнерныхтипов, заинтересованному читателю рекомендуется использовать STL для от-тачивания своих навыков в определении АТД. Это поможет дополнительноосознать преимущества и выгоды АТД.

54 © 2009 «Практика функционального программирования»

Page 55: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

6.3. АТД в языке программирования Haskell

ÀÒÄ A

Predica

te A* 1

Part A*1. . .

Pred

icat

e A*

2

Predicate A*N

. . . . . .

y1. . .

Selector

Y 1

Sele

ctor

Y2 Selector Y

m

y2 ym

Part A*2 Part A*N

Рис. 6.2. Древовидное представление АТД

List (A)

null

NIL

notNull

A

head tail

List (A)

nonNIL

Рис. 6.3. Древовидное представление типа List(A)

Rose(A) = NIL +A ×List(Rose(A))

nil, rose = constructorsRose(A)node, branch = selectorsRose(A)NIL,Rose = partsRose(A)null, isRose = predicatesRose(A)

Вот определение типа с названием «верёвка» (данный типпредставляет собой список элементов с чередующимисятипами A и B):

Rope(A,B) = NIL +B × (Rope(B,A))

nil, rope = constructorsRope(A,B)element, twisted = selectorsRope(A,B)NIL,Twist = partsRope(A,B)null, isTwisted = predicatesRope(A,B)

А вот и определение двоичного дерева, в вершинах которо-

го находятся элементы типа A¹²:

Tree(A) = Empty +A × Tree(A) × Tree(A)

empty, node = constructorsTree(A)element, left, right = selectorsTree(A)Empty,Node = partsTree(A)isEmpty, isNode = predicatesTree(A)

Ну а определение двоичного дерева, приведенного в разде-ле 6.1, выглядит так:

Tree(A) = A + Tree(A) × Tree(A)

leaf, node = constructorsTree(A)element, left, right = selectorsTree(A)Leaf,Node = partsTree(A)isLeaf, isNode = predicatesTree(A)

Как видно из примеров, ничего особенно сложного в нота-ции синтаксически-ориентированного конструирования АТДнет. Стоит отметить, что абстрактная математическая нота-ция сегодня используется крайне редко, поскольку можно вос-пользоваться одним из языков программирования, в которомподдерживается понятие АТД.

6.3. АТД в языке программированияHaskell

Одним из языков программирования, где наиболее полнои близко к теории синтаксически-ориентированного констру-ирования представлены алгебраические типы данных, являет-ся функциональный язык Haskell. В связи с этим для изуче-ния применения АТД в прикладном программировании име-ет смысл обратить пристальное внимание на синтаксис этогоязыка. Далее в этом разделе будет кратко рассмотрен общийвид определения АТД с некоторыми примерами, пояснён ме-ханизм сопоставления с образцами, а также приведена класси-фикация АТД.

6.3.1. Общий вид определения АТД в языкеHaskell

Как уже упоминалось, в языке Haskell любой непримитив-ный тип данных является алгебраическим¹³. АТД вводятсяв программу при помощи ключевого слова data, использова-ние которого определяется следующим образом [3]:data [context =>] simpletype = constrs [deriving],

где:

• context — контекст применения переменных типов(необязательная часть определения);

¹²Это определение отличается от того определения двоичного дерева, ко-торое рассмотрено в начале статьи — здесь в каждом узле дерева находитсяопределённое значение, а листовые вершины отличаются от узловых тем, чтооба поддерева пусты.

¹³Впрочем, с одной стороны, примитивные типы также могут быть пред-ставлены в виде конечных или бесконечных перечислений, что делает воз-можным их определение посредством АТД. С другой стороны, в языке Haskellимеется понятие «функциональный тип», то есть тип функции как программ-ной сущности (не тип значения, возвращаемого функцией, а именно типфункции). Примеры функциональных типов приведены в теоретическом раз-деле (см., например, стр. 54); в языке программированияHaskell используютсяименно такие типы функций.

© 2009 «Практика функционального программирования» 55

Page 56: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

6.3. АТД в языке программирования Haskell

• simpletype — наименование типа с перечислением всехпеременных типов, использующихся во всех конструкто-рах АТД (если переменные типов не используются, то ни-чего не указывается);

• constrs — перечень конструкторов АТД, разделённыхсимволом ( ∣) (данный символ соответствует операцииразмеченного объединения);

• deriving — перечень классов, для которых необходимоавтоматически построить экземпляры определяемого ти-па (необязательная часть определения).

Контекст и экземпляры классов являются понятиямииз системы типизации с параметрическим полиморфизмоми ограниченной квантификацией типов, которая используетсяв языке Haskell. Эти понятия выходят за рамки статьи, поэто-му в дальнейших примерах эти части, и без того являющиесянеобязательными, будут пропущены. Тем не менее, в дальней-шем (в последующих статьях на темы теории типов и системтипизации функциональных языков программирования) этотвопрос будет подробно рассмотрен, поэтому заинтересован-ному читателю рекомендуется уже сейчас «держать в уме»все эти нюансы. Кроме того, в компоненте constrs могутбыть специальные отметки строгости конструкторов, самиконструкторы могут быть инфиксными (об этом чуть позже),а также содержать именованные поля (об этом тоже написанониже).

Наименование АТД и все его конструкторы по обязатель-ным соглашениям о наименовании, принятым в языке Haskell,должны начинаться с заглавной буквы. Наименование типаи наименования его конструкторов находятся в разных про-странствах имён, поэтому в качестве наименования одногоиз конструкторов можно вполне использовать слово, являю-щееся наименованием всего типа (эта ситуация часто исполь-зуется в случаях, когда у определяемого АТД один конструк-тор). После наименования типа, как уже было сказано, долж-ны быть перечислены все переменные типов, использующиесяв конструкторах, причём переменные типов опять же по со-глашениям должны начинаться со строчной буквы (обычнов практике используются начальные буквы латинского алфа-вита — a, b и т. д., в некоторых специальных нотациях языкаиспользуются строчные греческие буквы α, β и т. д.).

В некоторых случаях конструктор АТД может иметь спе-циальное наименование, составленное из небуквенных сим-волов. Для определения такого конструктора его наимено-вание должно начинаться с символа двоеточия (:). Способиспользования подобных конструкторов ограничен — онидолжны быть бинарными и использоваться в инфиксной фор-ме (то есть располагаться между своими аргументами). Впро-чем, любой бинарный конструктор, как и произвольная би-нарная функция, может быть записан в инфиксной форме по-средством заключения его наименования в обратные апостро-фы (‘ ). Возвращаясь к конструкторам с небуквенными на-именованиями, можно отметить, что сам символ (:) являетсяодним из конструкторов типа «список». Его формальное опре-деление таково:

data [α] = []∣ α:[α]

Это определение не является правильным с точки зрениясинтаксиса языка Haskell, но оно вшито в таком виде в ядро

языка, чтобы вид списков в языке был более или менее удоб-ным для восприятия (таким образом, переопределить кон-структор (:) нельзя). Если определять список самостоятельно,то определение этого типа будет выглядеть примерно так:

data List α = Nil∣ List α (List α)

Как можно заметить, каждый конструктор типа начинает-ся с заглавной буквы (Nil и List), причём второй конструк-тор совпадает по наименованию с наименованием всего ти-па (как уже сказано, наименования типов и их конструкторовлежат в разных пространствах имён и употребляются в раз-личных контекстах, поэтому неоднозначностей в интерпрета-ции наименований не возникает, чем удобно воспользовать-ся). Первый конструктор пустой, зато второй определяет де-картово произведение двух типов: некоторого типа α (здесьсимвол α — переменная типов) и типа List α, причём в дан-ном случае в качестве аргумента конструктора используетсянаименование АТД. Это важно чётко понимать для уяснениясути определения. Определение могло бы быть переписано та-кимобразом (второйконструкторназывается в традицииязы-ка Lisp):

data List α = Nil∣ Cons α (List α)

А вот как, к примеру, определяется специальный типдля представления обычных дробей (данный тип определёнв стандартном модуле Prelude)¹⁴:

data Ratio α = α :% α

Здесь, как видно, применён бинарный инфиксный кон-структор. Этот конструктор принимает на вход два значения,а возвращает их связанную упорядоченную пару. Данная па-ра является представлением дроби. Соответственно, для зна-чений этого типа определены такие селекторы (наименованияприведенных функций переводятся с английского языка как«числитель» и «знаменатель» соответственно):

numerator :: Ratio α → αnumerator (x :% y) = x

denominator :: Ratio α → αdenominator (x :% y) = y

Из этих определений видно, что функции-селекторы как быраскладывают АТД на элементы, возвращая тот из них, кото-рый требуется по сути функции. Другими словами, селекторыв языке Haskell аналогичны функциям-аксессорам или опера-торам доступа к полям данных в других языках программиро-вания. Например, запись «denominator n» в языке Haskellаналогична записи «n.denominator()» в языке Java.

6.3.2. Сопоставление с образцомПредставленные выше функции numerator

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

¹⁴Определение, конечно, несколько иное, но, как было заявлено ранее, осо-бенности системы типизации языка Haskell в настоящей статье рассматри-ваться не будут.

56 © 2009 «Практика функционального программирования»

Page 57: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

6.3. АТД в языке программирования Haskell

«Образцом» называется выражение, в котором некоторыеили все поля декартова произведения (если декартово произ-ведение не пусто, то есть конструктор не представляет собойпростую метку размеченного объединения) заменены свобод-ными переменными¹⁵ для подстановки конкретных значенийтаким образом, что при сопоставлении происходит означива-ние параметров образца — однозначное приписывание пере-менным образца конкретных значений. Для АТД образцом яв-ляется указание одного из конструкторов с перечислением пе-ременных или конкретных значений в качестве его полей.

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

Этот процесс можно пояснить на примере. Пусть есть опре-деление функции:

head :: [α] → αhead [] = error ”Empty list has no first element.”head (x:xs) = x

Здесь первая строка является сигнатурой, описывающейтип функции, вторая и третья — два клоза функции head со-ответственно. Сигнатура функции может не указываться в ис-ходных кодах, так как строго типизированный язык Haskellимеет механизмы для однозначного вычисления типа любо-го объекта в наиболее общей форме (соответственно, наличиеили отсутствие сигнатур функций не влияет на их работоспо-собность). Впрочем, многие разработчики говорят о том, чтоналичие сигнатур функций рядом с определениями позволяетболее чётко понимать смысл и суть функции.

Первый клоз определяет значение функции в случае, еслина вход функции подан пустой список. Как видно, функцияпредназначена для возвращения первого элемента («головы»)списка, а потому для пустого списка её применение ошибоч-но.Используется системнаяфункцияerror. Второй клоз при-меним для случаев, когда список не является пустым. Непу-стой список, как уже говорилось в теоретической части — этопара, первым элементом которой является некоторое значе-ние, а вторым — список оставшихся элементов, «хвост» спис-ка. В языке Haskell эти элементы декартова произведения со-единяются при помощи конструктора (:), что и представленов образце. Две свободныепеременные—xиxs—этопарамет-ры образца, которые получают конкретные значения в случаекорректного применения функции. Например:

> head [1, 2, 3]

сопоставит с переменной x конкретное значение 1, а с пере-менной xs— значение [2, 3]. Соответственно, результатомвыполнения функции будет значение переменной x, то есть 1.

Приведённый пример можно понимать и по-другому. Разсписок есть пара, созданная при помощи конструктора (:), тосписок[1, 2, 3]может быть представлен как(1:[2, 3]),

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

¹⁶Клозом (от англ. clause) называется одна запись в определении, определя-ющая значение функции для конкретного набора входных параметров.

а ещё вернее как (1:(2:(3:[]))). В данном случае вызовhead [1, 2, 3] приведёт к следующей последовательностивычислений:

> let (x:xs) = (1:[2, 3])in x

или, что то же самое:

> let x = 1xs = [2, 3]

in x

В конечном итоге, поскольку свободная переменная xsне участвует в вычислении результата, оптимизирующийтранслятор языка, основанного на ленивых вычислениях, про-ведёт такое преобразование¹⁷:

> let x = 1in x

Итогом вычислений будет значение переменной x, то есть 1.Теперь можно рассмотреть более сложный пример. Пусть

определён АТД для представления бинарного дерева (та-кой же, как в теоретической части):

data Tree α = Empty∣ Node α (Tree α) (Tree α)

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

depth :: Tree α → Intdepth Empty = 0depth (Node _ l r) = 1 + max (depth l) (depth r)

Первый клоз функции определяет её значение для перво-го конструктора АТД Tree, то есть для пустого дерева. Вто-рой клоз определяет значение функции уже для непустого де-рева. У непустого дерева есть хранимый в узле элемент и дваподдерева — левое и правое. Функции depth хранимый в уз-ле элемент неинтересен, а потому в образце применена «маскаподстановки» для неиспользуемых элементов АТД — (_). Этотсимвол при сопоставлении с образцами означает, что на егоместо может быть подставлено всё, что угодно. Кроме того, этоединственный символ, который можно использовать несколь-ко раз в одном образце. Два других компонента конструктораNode, l и r, сопоставляются с левым и правым поддеревьямисоответственно.

Процесс сопоставления с образцом устроен таким образом,что не требует от значенийАТДбыть быть величинами, над ко-торыми определена операция сравнения¹⁸— возможность вы-бора конкретной части АТД обеспечивается наличием преди-катов (необходимо вспомнить аксиому для предикатов АТД).Вместе с тем это накладывает дополнительные ограничения—нельзя написать определениефункции, подобное следующему:

isElement x [] = FalseisElement x (x:xs) = TrueisElement x (y:xs) = isElement x xs

Здесь во втором клозе в образцах переменная x использует-ся два раза, что недопустимо в образцах.

¹⁷О ленивой стратегии вычислений можно прочесть в статье [9] и дополни-тельных источниках, в том числе перечисленных в указанной статье.

¹⁸В терминах Haskell — типы не обязаны быть экземплярами класса Ord.

© 2009 «Практика функционального программирования» 57

Page 58: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

6.4. АТД в других языках программирования

Таким образом, при сопоставлении с образцом происходитсравнение конструктора поданного на вход функции значе-ния с конструктором в образце. Это значит, что технологиясопоставления с образцом является очень мощной и гибкойпри определении функций — она не требует явного определе-ния функций сравнения величин.

6.3.3. Классификация АТДОстаётся кратко упомянуть о дополнительной классифика-

ции АТД в языке Haskell. При помощи АТД определяются лю-бые типы данных, включая те, для которых в других языкахпрограммирования имеются отдельные ключевые слова. На-пример, простое перечисление на языке Haskell выражаетсякак АТД, все конструкторы которого пусты:

data Weekday = Monday∣ Tuesday∣ Wednesday∣ Thursday∣ Friday∣ Saturday∣ Sunday

Декартовым типом называется такой АТД, который име-ет только один конструктор декартова произведения (иногдадекартов тип называют также типом-произведением). Приве-дённый ранее пример типа Ratio представляет собой декар-тов тип. Обычно декартовы типы используются для определе-ния записей с именованными полями. Для этих целей в языкеHaskell имеется специальная синтаксическая конструкция:

data Timestamp = Timestamp{

year :: Int,month :: Month,day :: Int,hour :: Int,minute :: Int,second :: Int,weekday :: Weekday

}

В приведённом случае транслятором языка будут автомати-чески сгенерированы функции доступа к перечисленным по-лям декартова типа, имеющие такие же наименования, каки поля. Так, если есть переменная ts типа Timestamp, то вы-зов выражения year ts позволит получить из этой перемен-ной значение первого поля в декартовом произведении. Ис-пользование выражения ts{year = 1984} позволяет уста-новить значение первого компонента декартова произведе-ния. Символ равенства (=) здесь означает не присваивание,а копирование объекта с установкой в определённых поляхновых значений (впрочем, оптимизирующие трансляторы мо-гут действительно делать замену в соответствующих ячейкахпамяти в случаях, если известно, что старый объект большене будет использован).

Ещё одним специфическим АТД является тип-сумма. Та-кой тип состоит из набора конструкторов, каждый из которых«обёртывает» только одно значение. Ближайшим аналогом та-кого типа в языках типа C является объединение (ключевоеслово union). Например:

data SourceCode = ISBN String -- Код книги.

| ISSN String -- Код периодического издания.

Наконец, осталось упомянуть, что приведённое делениеАТД на типы в языке Haskell достаточно условно. Никто не за-прещает сделать АТД, в котором тринадцать конструкторовбудут пустыми, а четырнадцатый представлять собой струк-туру с именованными полями. Таким образом видно, что самапо себе концепция АТД позволяет достаточно гибко представ-лять типы данных в языках программирования.

Более подробно с АТД и методиками программированияна языкеHaskell с их применениемможно ознакомиться в кни-ге [8].

6.4. АТД в других языках программиро-вания

Помимо рассмотренного в предыдущем разделе языкаHaskell концепцияАТД явно реализована в следующих языкахпрограммирования (перечень дан по алфавиту)¹⁹:

• F#;

• Hope;

• Nemerle;

• OCaml и большинство языков семейства ML;

• Scala;

• Visual Prolog.

В этом разделе кратко рассмотрены особенности использо-вания АТД в перечисленных языках программирования.

В языке программирования F# АТД реализованы ограни-ченно исключительно в виде безопасных с точки зрения ти-пизации размеченных объединений (union), то есть все АТДв языке F# являются типами-суммами. Например:

type SomeType =| Constructor1 of int| Constructor2 of string

Язык Hope стал первым языком программирования, в ко-тором концепция АТД и механизм сопоставления с образца-ми были реализованы в полной мере. Этот язык вдохновлялразработчиков последующих языков программирования —Miranda и Haskell. Синтаксис языка Hope достаточно необы-чен, но само понятие АТД отражено в нём в полном объё-ме. Для размеченного объединения используется символ (++),для декартова произведения — символ (#). Типы в декартовыхпроизведениях заключаются в скобки. Например, для бинар-ного дерева АТД определяется так:

data tree == empty ++ node (num # tree # tree);

Язык Nemerle является C-подобным языком программиро-вания для платформы .NET, основное достоинство которо-го заключается в поддержке как объектно-ориентированной,так и функциональной парадигм программирования (впро-чем, язык Haskell также позволяет это делать, особенноего объектно-ориентированные потомки Mondrian, O’Haskellи Haskell++). АТД в языке Nemerle называются вариан-тами и полностью соответствуют теории синтаксически-ориентированного конструирования Ч. Э. Хоара. Синтаксисже несколько необычен для C-подобного языка:

¹⁹В список не включён язык Miranda как прародитель языка Haskell. В этихязыках синтаксис для определения АТД практически совпадает.

58 © 2009 «Практика функционального программирования»

Page 59: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

Литература Литература

variant Colour{∣ Red∣ Orange∣ Yellow∣ Green∣ Cyan∣ Blue∣ Violet∣ RGB {r : int; g : int; b : int;}

}

Как видно, размеченному объединению соответствует сим-вол ( ∣), а декартову произведению — символ (;). Наименова-ния полей в декартовых произведениях обязательны.

Язык OCaml является одним из серии языков ML, которыйиспользует функциональную, объектно-ориентированнуюи процедурную парадигмы программирования. Само се-мейство языков ML имеет достаточно серьёзный вес в мирефункционального программирования, а потому без реали-зации АТД в этих языках не обошлось²⁰. В этом языке, каки в языке Haskell, применяется параметрический полимор-физм (использование переменных типов).

Вот как, к примеру, определяется АТД для представленияколоды карт:

type suit = Spades | Diamonds | Clubs | Hearts;;

type card =Joker

| Ace of suit| King of suit| Queen of suit| Jack of suit| Number of suit * int

;;

Теоретическая концепция АТД реализована в OCaml в пол-ном объёме. Размеченное объединение как обычно представ-ляется символом ( ∣), а декартово произведение — симво-лом (∗). В АТД могут быть как пустые декартовы произведе-ния, так иполноценные, а такжеихпроизвольная комбинация.

Язык Scala является Java-подобным мультипарадигмен-ным языком программирования (как обычно заявляютсяобъектно-ориентированная, функциональная, процедурнаяпарадигмы). АТД в этом языке реализованы достаточно свое-образно при помощи концепции класса. Тем не менее эта реа-лизация полностью соответствует теории. Для представленияАТД и использования технологии сопоставления с образцамииспользуется специальный вид классов:

abstract class Expressioncase class Sum (l: Tree, r: Tree) extends Expressioncase class Var (n: String) extends Expressioncase class Const (v: Int) extends Expression

Декларации за ключевыми словами case class являютсятаким специальным видом классов, каждый из которых пред-ставляет конструктор декартова произведения того АТД, так-же представимого в виде класса, который он расширяет. Син-таксис достаточно необычен, но о позволяет использовать всюсилу концепции АТД.

Если case class Expression объявить как sealed, токомпилятор сможет проверять полноту разбора случаев при

²⁰Собственно, некоторые концепции уже упомянутого языка F# также былиоснованы на языке OCaml, что видно из синтаксиса

сопоставлении с образцом типа Expression, зато в про-тивном случае разработчик сможет расширять множествовыражений, добавив, к примеру, тип выражений Product.Таким образом, Scala поддерживает модульные декларацииcase class, но может и давать определенные гарантии кор-ректности.

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

В этом языке для определения типов используется поня-тие «домен», то есть область определения предиката. Мож-но определять сложные домены так, чтобы предикаты мог-ли принимать значения любого типа, а не не только trueи false («истина» и «ложь»). При определении домена в видеАТД символ (;) используется для размеченного объединения,а (,) —для декартова произведения, причём элементы послед-него должны быть заключены в круглые скобки после наиме-нования конструктора. Вот пара примеров:

domainscategory = art; nom; cop; rel.tree = case(category, string); world(tree, tree);

silence.

В данном примере домен category является перечислени-ем, составленным из четырёх констант. Домен tree представ-ляет собой обычный АТД, состоящий из трёх конструкторов,первые два из которых представляют собой декартовы произ-ведения двух соответствующих компонентов.

Таков краткий обзор реализаций концепции АТД в различ-ных языках программирования. Он демонстрирует, что АТДмогут быть успешноиспользованывразличныхподходах к по-строению программных средств.

ЗаключениеАлгебраические типы данных являются достаточно мощ-

ным и универсальным средством для описания структур дан-ных при программировании. Понимание теоретических основэтой концепции позволит прикладным программистам глуб-же осознавать процессы, происходящие в разрабатываемыхими программных средствах. Более того, на практике, концеп-ция АТД во многих случаях даёт разработчикам программныхсредств возможность избавиться от синтаксического мусорав определениях типов и увидеть суть структур данных на ран-них этапах разработки программ.

Литература[1] Dahl O.-J., Dijkstra E. W., Hoare C. A. R. Structured Program-

ming. — Academic Press, 1972.

[2] G. C. Beitrage zur Begrundung der transfiniten Mengen-lehre. // Math. Ann. — 1895. — Vol. 46.

[3] Jones S. P. et al.Haskell 98 Language andLibraries.eRevisedReport. — Academic Press, 2002.

[4] Milner R. A calculus of communicating systems. — Springer(LNCS 92), 1980.

© 2009 «Практика функционального программирования» 59

Page 60: Практика функционального программированияfprog.ru/2009/issue2/practice-fp-2-compact.pdfциклы со счетчиком организуются

Литература Литература

[5] Pierce B. C. Types and Programming Languages. — MITPress, 2002. — (имеется перевод книги на русский языкURL: http://newstar.rinet.ru/~goga/tapl/ (да-та обращения: 28 сентября 2009 г.)). http://www.cis.upenn.edu/~bcpierce/tapl.

[6] Вольфенгаген В. Э. Методы и средства вычислений с объ-ектами. Аппликативные вычислительные системы.— М.:АО «Центр ЮрИнфоР», 2004. — 789 с.

[7] Гарднер М. А ну-ка, догадайся! — М.: Мир, 1984. — 212 с.

[8] Душкин Р. В. Справочник по языку Haskell. — М.:ДМК Пресс, 2008. — 544 с.

[9] Зефиров С. А. Лень бояться. // Журнал «Практика функ-ционального программирования». — 2009. — № 1. http://fprog.ru/2009/issue01/.

[10] Клини С. К. Введение в метаматематику. — М.: ИЛ, 1957.

[11] Петер Р. Рекурсивные функции. — М.: ИЛ, 1954. — 264 с.

[12] Розанова М.С. Современная философия и литература.Творчество Бертрана Рассела / Под ред. Б. Г. Соколова. —СПб.: Издательский дом Санкт-Петербургского государ-ственного университета, 2004.

[13] Уайтхед А. Н. Основания математики. / Под ред.Г. П. Ярового, Ю. Н. Радаева. — Самара: Изд-во «Самар-ский университет», 1954.

[14] Успенский В. А. Теорема Гёделя о неполноте. — М.: Наука,1982. — 110 с.

60 © 2009 «Практика функционального программирования»