][-holywar: lisp vs. java. Common Lisp: простота и мощь промышленного стандартаLinkFly, http://linkfly.ru В рамках этой статьи мы попробуем сравнить приемы программирования на языке Common Lisp и языке Java. На конкретных примерах мы продемонстрируем концептуальные ограничения языка Java и приведем аргументы, говорящие о необходимости применять более совершенный, легко расширяемый и проверенный временем язык программирования. Почему все-таки Лисп?Первый вопрос, который бы появился у меня, будь я на месте читателя: «Чем эта статья отличается от очередного пиара j2ee/.net/python/jsf/asp/ и тому подобных?». Отлично, попробую на него ответить:
В современном мире даже начинающий программист догадывается, что предоставляемые корпорациями технологии в первую очередь нужны им самим для укрепления позиций на рынке, и только в последнюю очередь для того, чтобы ты быстро и качественно разрабатывал программный продукт. Для них важно другое: насколько конкретно ты влипнешь в эти самые технологии. Итак, пока одни чертыхаются в мире Microsoft и надеются, что лучший из миров — мир IBM, а в мире IBM думают, что у соседа «трава зеленее» и посматривают в сторону Microsoft, мы займемся настоящим программированием. Конечно, вряд ли нам удастся в рамках одной короткой статьи привести все аргументы и рассказать обо всех сокровищах Лисп-культуры. Тем не менее, основополагающие концепции показать вполне реально. Если ты решил замутить свой язык программирования и захотел позаимствовать это решение — ты получишь еще один диалект Лиспа. Если же ты пожелаешь, чтобы в твоем языке была поддержка всех существующих на данный момент технологий программирования, то получишь что-то подобное диалекту Common Lisp. Может быть, какие-то возможности твоего диалекта и не будут входить в этот стандарт, но вряд ли для тех, кто программирует на Лиспе это будет серьезный аргумент — макрос, расширяющий язык дополнительной возможностью, пишется за 1-2 часа (а то и за пару минут). Что и не снилось разработчикам на JavaЗдесь я опишу возможности языка Lisp, которые отсутствуют в языке Java или имеют крайне урезанные варианты, не вписывающиеся в модель описания вычислений языка и имеющие проблемы интеграции друг с другом. Приступим! - Единообразный и древовидный синтаксис, позволяющий максимально просто генерировать программный код. Главная и нерешаемая проблема языков с так называемым «синтаксическим сахаром» заключается в этом самом сахаре. В принципе, в любом языке можно создать функцию (метод), получающую на вход строку с программой на этом языке, и, разбив ее на лексемы, действовать подобно интерпретатору. Или, сохранив строку в файл, вызвать компилятор, и откомпилированный байт-код (к примеру, файл *.class) загрузить в систему (в нашем случае в jvm). Да вот только делается так крайне редко. Как думаешь, почему? А все очень просто — никакой здравомыслящий разработчик не станет усложнять свой проект (может быть, и без того сложный) парсером исходного кода, тем более, это очень круто может ударить по производительности. Как в случае парсинга входной строки, так и сохранения на диск, и при вызове внешнего компилятора (да еще, блин, загрузке в систему получившегося байт-кода). Никакие костыли вроде «Аннотаций» не решают проблему — возиться с генерацией синтаксических конструкций языка довольно долго и чревато ошибками. Об отладке говорить вообще не приходится. Тем не менее, жизнь заставляет генерировать массу кода перед его компиляцией, например, для работы с таблицами базы данных. Таблицы, типы полей и нюансы sql-запросов, зависящих от базы данных — все это требует генерации специфичного программного кода. Так что, мнения скептиков и консерваторов из серии «да зачем это надо» перетекли (в создаваемый, видимо, ими же) так называемый «деплой-код» (deploy code). Конечно же, все реализовано не очень хорошо по той простой причине, что язык имеет множество синтаксических конструкций (хотя программы на Яве генерировать, наверное, в разы проще, чем программы на C++ или Perl). В Лиспе же все происходит с точностью до наоборот: программы на Лиспе генерировать не просто легко — это является частью практики программирования на Лиспе. Можешь убедиться в этом сам, посмотрев какие-нибудь Лисп-библиотеки на http://cliki.net. Например, библиотеку CLOCC. Да и знаменитый лиспер Пол Грэхэм так пишет о своей Viaweb: «Исходный текст редактора Viaweb на 20-25% состоял из макросов». В общем, в процессе программирования на Лиспе писать программы, которые пишут другие программы - совершенно тривиальный процесс. Жаль, что всякие разные «изобретатели деплой-кода» не в курсе. И насколько мы продвинулись в развитии программирования, если, как пишет Пол Грэхэм: «Макросы, очень похожие на современное представление о них, были предложены Тимоти Хартом (Timothy Hart) в 1964 году, через два года после того как Lisp 1.5 был выпущен». Может быть, монстров в нашей жизни было бы гораздо меньше, если бы современные горе-изобретатели знали об этом? Итак, синтаксическтий сахар мешает появлению в языке возможностей простой и понятной кодогенерации. Отсюда следует идея: нельзя ли нам этот недостаток как-нибудь обойти? В конце концов, можно же создать собственный препроцессор! Например, если бы мы писали на языке Java в синтаксисе Лиспа, то код:
Не считая статической типизации (которая во многих случаях проигрывает типизации динамической) и отхода от префиксной нотации в описании цикла, в списке - (i < x), этот код не по семантике, но по синтаксису вполне соответствует символьному выражению в Лиспе (s-expression). Ну что, раздуем философский спор о том, что удобнее и читабельнее? Так или иначе, второй вариант представляет собой связный список — вот его-то как раз и чрезвычайно просто генерировать и обрабатывать, особенно с помощью языка, в котором десятки лет совершенствовались и оттачивались механизмы работы со списками! Это было бы, пожалуй, моим гениальным открытием, я не первый, кто к этому пришел — одно из изобретений корпорации Franz (http://franz.com/) - язык JIL (http://www.franz.com/support/documentation/6.2/doc/jil.htm). Вот что пишут сами разработчики: «Java в Lisp (jil) - это язык для записи программ, выполняющихся на виртуальной машине Java (jvm)». Ну а теперь поговорим о других концепциях, косвенно или явно следующих из данной. Эффективная генерация кода во время выполнения и его оптимизацияЛюбой более-менее приличный язык, поддерживающий функциональную парадигму (Lisp, Python, Haskell), имеет конструкцию «Lambda», позволяющую создать функцию во время выполнения (в run-time). В Лиспе это происходит так: Теперь переменная add5 содержит объект-функцию. Мы ее можем передавать в качестве параметра и вызывать, например, с помощью funcall:
Особенно интересна возможность ее передачи в качестве параметра. Функции, получающие такой аргумент, называются «функционалами». Отличным примером является «отображающий функционал» MAPCAR. Но об этом чуть позже. В Лиспе у нас есть возможность скомпилировать функцию, сгенерированную в run-time. Допустим, во время выполнения мы составили список, представляющий подпрограмму, которую нам нужно добавить в систему (между прочим, без какой-либо записи на жесткий диск и вызова внешнего компилятора). Пусть наша программа для простоты будет такой:
Теперь мы можем добавить ее в текущую лисп-систему:
Воспользуемся стандартной функцией TIME для получении времени выполнения:
И если мы произведем компиляцию добавленной в run-time функции:
…то, еще раз выполнив тестовый прогон (time (loop …), получим значительный выигрыш в скорости выполнения (на Lispworks у меня получилось ~2.5, что, конечно же, не предел). ФункционалыЭто функции, которые получают в качестве аргументов (или одного из аргументов) другую функцию. В статье я расскажу только об одном — MAPCAR. Его еще называют «Отображающим функционалом». Несмотря на страшное название, работает он очень просто: принимает в качестве аргумента какую-либо функцию и список элементов, с которыми ее надо вызвать, и затем строит новый список из результатов:
Или:
Как видишь, функция, принимаемая в качестве первого аргумента, может быть как встроенная, так и определенная тобой или созданная, исходя из каких-то run-time обстоятельств. Попробуем сделать на языке Java последний пример и начнем мы с определения функции во время выполнения. Правда, в Java нет функций, а есть лишь методы классов... к счастью, имеется очередной «костыль» - анонимный класс:
Он, кстати, лишь создает иллюзию определения в run-time — на самом деле, происходит запись программы на диск, и ее компиляция в файл .class. Ты можешь сам в этом убедиться — сохрани приведенную ниже программу в файл My.java и скомпилируй (javac My.java):
Теперь зайди в каталог с классом и полюбуйся на два файла: My$1.class и My$2.class. Так что, если не хочешь тормозов, подумай о том, чтобы разместить соответствующий каталог в оперативной памяти. Да, и самое ужасное в подобных костылях это то, что создается набор некоторых правил, совершенно непредсказуемых и ниоткуда не следующих. В примере выше, если ты избавишься от вложенного класса Object2 и попробуешь использовать Object, то файл просто не скомпилится. А все потому, что вдруг появилось новое правило: анонимный класс должен переопределять существующий метод суперкласса, а не определять новый. Далее об отображающем функционале. В стандарте Java нет ничего подобного mapcar, поэтому реализуем ее сами:
Дело за малым - применить это чудо инженерной мысли:
И всю эту фигню я должен писать вместо (mapcar (lambda (x) (+ num 5)) '(4 5 6))!? Между прочим, тут обнаружилось новое правило. Если захочешь использовать в определяемом анонимном классе локальную переменную, то тебя ждет жестокий облом — этого делать нельзя, если только не объявить ее как final. А теперь посчитаем количество сущностей, которые мы наплодили для реализации этой идеи. Лисп:
Java:
Не правда ли, слишком громоздко? Программирование на таком низком (по сравнению с Лиспом) уровне чревато ошибками, трудностью сопровождения кода и - далее - созданием разного инструментария, исправляющего недостатки языка и … в общем, разработчикам всевозможных eclips'ов работы хватит надолго. Удобное средство инициализации сложных структур данныхОбычно (но не всегда) сложная структура данных вполне укладывается в шаблон «дерево». То есть, фактически, в «связный список», он же - ацикличный граф. Очень часто нужно без лишних хлопот быстро его создать и/или инициализировать. Причем на практике нам еще может понадобиться древовидная структура РАЗНОСОРТНЫХ данных. Сравним удобство реализации. Вот такую древовидную структуру нужно использовать в программе (смотри картинку «Древовидная структура»). Реализация на Lisp:
На Java это можно было бы сделать так:
Количество порождаемых сущностей сравни сам :). Только здесь простое добавление нового класса Symbol нас уже не спасет. Тип Symbol – это часть архитектуры Lisp’a и центральный элемент символьных вычислений. Реализация в Java в любом случае будет иметь ограничения. А если каких-то элементов данных у меня в настоящий момент нет? Тогда я могу создать список, блокируя (с помощью функции quote или сокращения «'») элементы, которые мне надо включить в список как есть, то есть, не вычисляя их:
Пусть этот код тебя не смущает, так как, по большей части, тут используются только три функции:
Но у Лиспа (как обычно) найдется еще кое-что. Последователи любой религии согласятся с тем, что повторения в коде - почти всегда плохо. Если у нас имеет место внушительный объем цитируемых данных, но есть несколько вычисляемых элементов, то мы можем воспользоваться обратным цитированием, пометив с помощью «,» те элементы которые все же надо вычислить. Знаки «`» и «,» являются лишь сокращением и также связаны с макросами чтения:
Вызов: `((1+ 3) ,(+ 4 5)) превращается во что-нибудь вроде: (SYSTEM::BACKQ-LIST (QUOTE (1+ 3)) (+ 4 5)). Обратное цитирование особенно интенсивно применяется в макросах, потому что позволяет сделать процесс генерации кода более читабельным. Судите сами — без макросов:
То есть, просто в двух местах нужно произвести вычисление и подставить результаты в общий шаблон кода (а шаблон кода, между прочим, можно передать в качестве параметра). Возможно, когда это увидят сторонники традиционного подхода, они начнут говорить что-то вроде: «да зачем, да это все в жизни неприменимо ...» и так далее. Вот что в жизни точно ПРИМЕНИМО, так это фантазия! Мы могли бы эту возможность использовать для генерации xml на стороне сервера, и выглядело бы подобное примерно так:
Остались сомнения? Попробуй написать аналогичную программу на Java, и они исчезнут. ЗаключениеК сожалению, очень многое осталось за бортом. Например, такие крайне интересные возможности, как:
И многое другое. Тем не менее, из написанного ты видишь, что Лисп - это не только язык для седовласых старцев, а вполне прагматичный инструмент для решения насущных проблем программирования. На показанных примерах видно, что никакой особой сложности в языке нет, и его идиомы прекрасно интегрируется друг с другом. Начало положено, теперь дело за тобой: исследуй, экспериментируй и то, что казалось сложным, обязательно станет очень простым и полезным. DVDИсходный код на Лисп и Java бери на нашем диске. WWW
|