Этот доклад я бы хотел посвятить сразу двум технологиям одновременно: языку Lisp и языко-ориентированному программированию (language-oriented programming)

Вид материалаДоклад

Содержание


Слайд 18-17
Подобный материал:
CЛАЙД 30

Этот доклад я бы хотел посвятить сразу двум технологиям одновременно: языку Lisp и языко-ориентированному программированию (language-oriented programming). Отдаю себе отчет в том, что здесь сидят любители scala, а также в том, что scala – лучший язык программирования современности :-) Но все же хотелось бы поговорить о некоторых вещах, пришедших к нам из прошлого, о технологиях пятидесятилетней давности, о субкультуре первых лиспхакеров MIT-а. Чтобы заинтересовать вас, чтобы вы дослушали доклад до конца, с самого начала скажу, что лисп очень сильно отличаются от всех других языков, так же, как Ассемблер отличается от всех остальных языков. Лисп навязывает свою собственную культуру, свой стиль программирования. Например, в культуре ООП-программистов, любителей C++, java, C# и т. д. одним из признаков профессионализма является отличное знание паттернов проектирования, а также умение быстро и правильно их применять. В среде лисперов над архитектурой, построенной на паттернах, в лучшем случае просто посмеются. Использовать уже реализованные паттерны – это одно, а вот имплементить их каждый раз заново – признак дремучей безграмотности лиспера.

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

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


СЛАЙД 29

Я думаю, все здесь присутствующие, наслышаны о предметно-ориентированных языках (Domain Specific Languages), и приблизительно представляют себе, что это такое. На самом деле, мы часто сталкиваемся с различными DSL-ами, хотя эта тема стала модным трендом совсем недавно. Давайте подробнее рассмотрим, что это такое.

Предметно-ориентированный язык – это язык ограниченной выразительности, фокусирующийся на некоторой предметной области. DSL очень сильно отличается от таких языков общего назначения, как, скажем, java, ruby или scala тем что не пытается объять необъятное и подходить для любой задачи. DSL ориентируется на какую-то одну тематику, на некоторый класс задач. По своим задачам DSL очень близок к хорошо спроектированному API, если этот API заточен на одну предметную область.

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


СЛАЙД 28

Пример языка управления движущейся камерой. Как видите, все очень просто. Главное достоинство этого языка, что освоить его сможет кто угодно, даже вот такой пользователь click


СЛАЙД 27

Здесь показан простой мини-язык для описания оргтехники некоторой фирмы.

Можно ли выразить то же самое на java? Можно, конечно. Click

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


СЛАЙД 26

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

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

Во паттерне MVC, семантическая модель занимает место, естественно, модели. Разработка семантической модели – не обязательная процедура, но желательная, т. к. позволяет лучше отделить представление DSL от его сущности. Далеко не в каждом DSL легко выделить компактную, сфокусированную на задаче семантическую модель. Например, в последнем примере доклада, понятие семантической модели вообще будет несколько размыто. Ее место занимает просто набор инструкций базового языка, с помощью которого и решается задача.

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

Теперь разберемся с компонентом View из модели MVC. Как может выглядеть представление языка для этой семантической модели?

Click

Это может быть некоторый язык со своим собственным синтаксисом. Это может быть, например, паттерн Builder, позволяющий сделать одну длинную гусеницу из вызовов API семантической модели. Это может быть Wizard или любой другой GUI. Это может быть таблица в базе данных. Одним словом, любая конструкция, способная взаимодействовать с этой семантической моделью.


СЛАЙД 25

По типу представления языка, DSL делятся на внутренние и внешние. Внешние DSL реализуются как самые настоящие языки программирования с использованием инструментов построения компиляторов/интерпретаторов. Они обладают произвольным синтаксисом и высоким уровнем абстракции. Это самые настоящие DSL, т. к. кроме задачи из своей предметной области, они не способны больше ни на что и очень жестко ограничены своим синтаксисом. Их легко изучить, ими легко пользоваться, даже непрограммистам – главное, чтобы специалист разбирался в данной предметной области. К минусам этих языков можно отнести сложность их разработки. Если API, например, проектируется под каждую задачу, то обертку в виде выразительного синтаксиса для этого API делает далеко не каждый. Для себя программист часто пользуется этим же API, не сильно заморачиваясь о том, сколько лишнего кода он пишет. Поэтому область применения внешних DSL – это те задачи, где нужно предоставить возможность пользователю-непрограммисту заскриптовать приложение. Пользователь, хорошо знающий предметную область, гораздо легче и быстрее освоит DSL и будет с большей эффективностью его использовать, чем традиционные скриптовые языки, вроде lua, python и т. д.

Внутренние DSL (embedded DSL, eDSL) отличаются от внешних тем, что их синтаксис строится на базе синтаксиса того же языка, на котором реализована семантическая модель. Базовый язык называется хостовым, и его синтаксис ограничивает синтаксис предметно-ориентированного языка.


СЛАЙД 24

На этом слайде показаны два еDSL, реализованные на java и ruby. В первом случае использовался паттерн Builder, позволяющий несколько упростить представление кода и сделать его более выразительным. Во втором случае никаких особенных паттернов не понадобилось.

Как видите, при построении eDSL используется целый набор ухищрений, чтобы как-то обойти недостатки и ограниченность синтаксиса хостового языка. Некоторые языки, вроде java, C#, C++ очень плохо подходят для построения eDSL. Некоторые, вроде того же ruby, подходят лучше.


СЛАЙД 23

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

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

Дальше я вообще не буду касаться внешних DSL, будем рассматривать только встроенные. Именно встроенные DSL – та золотая середина между самым высоким слоем абстракции и легкостью ее реализации.


СЛАЙД 22

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


СЛАЙД 21

Посмотрите, как, например, можно сделать описание двух одинаковых дисков в компьютере при помощи оператора цикла. В обоих случаях код отвратителен. На java и реализовать такое сложно, да и конструкция довольно хрупкая. Попробуйте, например, сделать несколько вложенных циклов, добавить туда объекты разных типов – и простой и удобный еDSL превратится в ночной кошмар. Во втором случае тоже плохо: синтаксис eDSL оказался разбавленным синтаксисом ruby, что сильно портит идею ограниченности DSL, а также снижает его защиту от ошибок. Кроме того, здесь нарушен барьер абстракции, который DSL воздвигает для ограничения сложности программы. В идеале мы должны пользоваться только конструкциями самого DSL, не заботясь о том, как он реализован. Если мы используем конструкции из более низкого слоя абстракции, мы автоматически теряем преимущества сфокусированности на задаче. Впрочем, подробнее читайте об этом в SICP.

Как видите, все языки программирования так или иначе ограничивают нас своим синтаксисом. Это здорово напрягает. А хотелось бы, чтобы наш DSL, обладающий произвольным синтаксисом, вообще был частью хостового языка.


СЛАЙД 20

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

Только задумайтесь на минуту, зачем придумывались все эти паттерны, зачем вас натаскивали на паттернах в универе и на курсах? Почему бы просто не написать библиотеку, где будут реализованы сразу все паттерны, а потом просто пользоваться ей? Задумайтесь на минуту, сколько кода мы написали копипастом для реализации паттернов только потому, что не можем применить повторное использование кода. Сколько методологий уже выдумано: процедурное программированние, структурное, функциональное, объектно-ориентированное, компонентно-ориентированное, аспектно-ориентированное. А проблема нормального code-reuse до сих пор нигде не решена. Да, сейчас есть мощные языки, позволяющие кое-что из паттернов автоматизировать. Но, например, паттерн Адаптер всегда будет писаться копипастом в строго-типизированных ООП-языках. Паттерн DTO и сегодня – жуткий геморрой для J2EE-программистов. Зимой мы писали проект, в котором реализация этого паттерна занимала процентов двадцать кода. Страшно себе представить, сколько это копи-пасты! Даже в языках с duck-typing все равно будут актуальны такие паттерны, как билдер, фасад, фабрика и т. д. Одним словом, ДОКОЛЕ? Сколько еще нам терпеть убогость наших языков и копипастить то, что нельзя встроить в язык или библиотеку?


СЛАЙД 19

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

Для того, чтобы получить полный контроль над своим языком программирования, необходимо получить доступ к его дереву разбора. Ведь именно здесь и задается вся семантика языка. Например, именно дерево разбора придает значение приоритетности операции. Кто сказал, что именно операция умножить более приоритетна, чем плюс? Кто сказал, что плюс и умножить – это вообще операции, соответствющие арифметическим плюс и умножить? Может речь идет вовсе не о них, а о конкатенации и повторении n-раз текстовых строк? Как добавить в язык новый оператор, которого там до сих пор нету? Как в вести в язык паттерны, наконец? Если бы мы могли как-то манипулировать деревом разбора, то, пожалуй, удалось бы снять многие ограничения хостового языка.


^ СЛАЙД 18-17

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

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


СЛАЙД 16

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

Этот синтаксис очень сильно отличается от java. Но в дереве разбора этого синтаксиса click нет ничего особенного, оно выглядит так же, как и для любого другого java-выражения.

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


СЛАЙД 15

Думаю, уже давно понятно, о каком языке я говорю. Язык, в котором нет синтаксиса, а есть одно только дерево разбора – это Lisp. Такой язык называется гомоиконным, т. к. код и данные в нем представлены одинаково – в виде списокв. Выглядит Lisp для непосвященных настолько странно, что про него шутят, будто Lisp – это язык программирования инопланетного происхождения.

Всего существуют три наиболее известных диалекта лиспа: click

Это Scheme, Common Lisp и Clojure. Все примеры далее я буду приводить на Clojure, потому что считаю его самым современным и наиболее практичным из всех лиспов. Для тех, кто не в курсе, Clojure работает под JVM, CLR и JavaScript. Он очень сильно акцентируется на функциональном программировании и конкурентности.


СЛАЙД 14

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

Предположим, мы хотели бы иметь оператор unless, который, по своей сути, выполняет то же, что not if. Мы создаем для этого шаблон с тремя параметрами: два аргумента для выполнения сравнений и набор операторов, которые должны будут выполняться, если выполнилось условие.


СЛАЙД 13

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

Оператор «волна» перед символом означает, что сюда нужно подставить значение переменной из шаблона.


СЛАЙД 12

Макрос в лиспе – это функция, которая выполняется во время компиляции кода. В результате выполнения этих функций должен получиться код, который будет подставлен на место вызова макроса. Обратите внимания, во время компиляции программы на лиспе выполняются именно макросы, функции, также написанные на лиспе. В этом, например, самое серьезное отличие лиспа от C++. Язык шаблонов C++ хоть и выполняется во время компиляции программы, но не совпадает с самим С++. Здесь же мы видим, что для написания макросов мы можем использовать те же самые конструкции, что и для основной программы.


СЛАЙД 11

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

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


СЛАЙД 10

Вот еще пример, в который мы не будем слишком углубляться. В языке Clojure нет оператора цикла, все реализуется через рекурсивные вызовы. Предположим, для каких-то неведомых целей нам понадобился оператор for, изменяющий указанную переменную в указанном порядке. Сделать его довольно просто, учитывая, что можно легко проанализировать переданное в макрос выражение. Так, например, в Clojure нет операторов ++ и –. Это именно макрос for придает смысл записи ++. Еще обратите внимание на то, что переменную i я нигде до этого не объявлял. Я просто передаю имя желаемой перменной в макрос, а он уже ее сам объявляет во время компиляции программы. При этом, использовать переменную i можно еще до того, как макрос будет скомпилирован (println i).


СЛАЙД 09

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


СЛАЙД 08

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

Здесь, например, мы создаем конструкцию, выполняющую вычисление выражения и проверку его на nil. Если выражение не равно nil, то вводится переменная result и ей присваивается результат вычисления этого выражения. Как и в предыдущем макросе, использовать эту переменную можно сразу, не заботясь о том, как и когда макрос ее объявит (println result).


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


СЛАЙД 07

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


СЛАЙД 06

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

Пример этот я взял из статьи Мартина Фаулера 2005-го года, которая называется «Language Workbenches: The Killer-App for Domain Specific Languages?» Привожу этот пример в несколько измененном виде.

На слайде показаны входные данные, которые мы читаем из какого-то потока. Здесь данные могут быть 4-х разных типов: SVCLFOWLER, SVCLHOHPE, SVCLTWO и USGE103. Каждый из этих типов конфигурируется своим набором полей, а каждое поле соответствует последовательности данных между определенными индексами.


СЛАЙД 05

Например, поле CustomerName соответствует подстроке между 4-м и 18-м символом входной строки. Задача состоит из двух частей:
  1. Во-первых, определить все типы данных со всеми полями в виде объектов java;
  2. Во-вторых, научить эти типы читать свои данные в соответствии с заданными индексами.

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

Экстраполируем. Что если у нас целых десять типов данных, и у каждого по десять полей? Уже сложнее, согласитесь? А если пятьдесят?

Вы знаете, ужас ведь даже не столько в огромных объемах индусокогда, который придется написать для этой задачи. Ужас в том, что через пару дней придет заказчик с массой гениальных идей и новой спецификацией, и вам придется все править. Через пару дней история повториться. И она будет повторяться до самого релиза, и даже в последний день разработки вас попросят подправить чуть-чуть индексы и добавить во все типы всего одно поле, да еще одно поле поменять местами с еще одним полем... Ну вы меня поняли. Если писать все вручную – взвоете. Поэтому, как только обнаруживаете у пациента желание увидеть мегабайт индусокода – сразу же пишите DSL.


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


СЛАЙД 04

Здесь показан пример eDSL, который объявляет типы данных SVCLFOWLER и USGE103. А на выходе должны получиться классы java, которые уже все умеют.


СЛАЙД 03

Вот исходник этого eDSL. Спокойно, не напрягайтесь, я знаю, как это выглядит click

Пожалуй не буду разбирать его в подробностях. Просто скажу, что в нем используются все те вещи, о которых я рассказывал раньше. Макрос def-reader действительно создает java-класс с методами, указанными как названия полей. Каждый метод умеет читать из данных с первого по второй индекс. Такой красивый синтаксис обеспечивается за счет анализа входных данных как списков.

Например, определим тип данных.


СЛАЙД 02

Этот макрос объявляет тип ThirdClass. После компиляции исходника мы можем использовать его из java следующим образом: click


СЛАЙД 01

Подитожим, все, о чем я вам рассказывал.
  1. DSL – мини-языки, использующиеся для описания некоторой предметной области. DSL бывают внешние и встроенные. Внешние DSL используются, как правило, для скриптования приложения. Встроенные DSL – для упрощения работы программиста над исходным кодом. DSL обеспечивают очень высокий уровень абстракции, гораздо выше, чем функции или классы. Если у вас есть готовый DSL, то решение задачи, формализованное на этом DSL, всегда будет проще, короче, понятнее и изящнее, чем на языке общего назначения.
  2. Лисп – язык без синтаксиса. Программы на лиспе представляются в виде списков и являются готовым деревом синтаксического разбора. За счет такого представления программы, а также благодаря наличию макросов, т. е. функций, выполняющихся на этапе компиляции, очень легко добавлять в лисп новые конструкции. Поэтому Lisp – идеальное средство разработки встраиваемых DSL.
  3. Разработка мини-языков на лиспе – является очень простым и обычным делом. Поэтому eDSL-ы на лиспе делают намного чаще, чем на других языках. Использовать эти eDSL тоже намного проще и удобнее.