На Лисп - Глава 1 |
- Statistics
- Participants
- Translate into Russian
- Translation result
- Translated in draft, editing and proof-reading required.
Расширяемый язык
Не так давно, если бы вы спросили, для чего нужен Лисп, то многие бы ответили «для искусственного интеллекта». На самом деле, связь между Лисп и ИИ – просто историческая случайность. Лисп был изобретен Джоном Маккарти (John McCarthy), который также придумал термин «искусственный интеллект». Его студенты и коллеги писали свои программы на Лисп, поэтому о Лисп стали говорить как о языке для задач ИИ. Во времена бума ИИ в 1980-х эта идея обсуждалась и повторялась настолько часто, что стала практически непременным атрибутом.
К счастью, стали ходить разговоры о том, что ИИ – это не единственная область применения Лисп. Недавние успехи в области аппаратного и программного обеспечения сделали Лисп коммерчески жизнеспособным: сейчас он используется в Gnu Emacs – лучшем текстовом редакторе для Unix; Autocad – индустриальном стандарте среди САПР для настольных компьютеров и Interleaf – распространенной профессиональной издательской программе. Использование Лисп в этих программах совершенно никак не связано с ИИ.
Если Lisp не язык ИИ, тогда что это? Вместо того, чтобы судить о Лисп по использующим его компаниям, давайте посмотрим на сам язык. Что вы можете сделать в Лисп и не сможете в других языках? Одна из наиболее отличительных черт Лисп – гибкость – позволяет ему лучше подстраиваться под задачу FIXME. Сам по себе Лисп является программой на Лисп, а программы на Лисп могут быть выражены в виде списков, которые в свою очередь в Лисп являются структурами данных. Вместе эти два принципа означают то, что любой пользователь может добавить операторы в Лисп так, что они не будут отличаться от встроенных в язык.
1.1 Эволюция дизайна.
Поскольку Лисп дает свободу в определении операторов, вы можете преобразовать его в тот язык, который вам нужен. Если вы пишете текстовый редактор, вы можете переделать Лисп в язык для написания текстовых редакторов. Если вы пишете САПР, то можете превратить Лисп в язык для написания САПР. И даже если вы пока не уверены насчет того, какую программу вы пишете, то беспроигрышный вариант – написать ее на Лисп. В какой бы вид программы она ни превратилась, Лисп в процессе ее написания будет эволюционировать в язык, предназначенный для написания такого вида программ.
Вы все еще не уверены, какого рода программу пишете? Для кого-то этот вопрос прозвучит странно. Такой разительный контраст с известной моделью разработки, где вы (1) тщательно планируете то, что собираетесь делать, а затем (2) делаете это. Если Лисп вдохновляет вас на написание программы до того, как вы определите способ ее работы, то согласно этой модели, такой подход лишь порождает хаотичное мышление.
Ну, это совсем не так. Метод «спланируй и реализуй», возможно, был хорош для строительства плотин или совершения военных вторжений, но опыт не показал, что это хороший способ написания программ. Почему? Возможно, потому что он слишком точный. Может быть, между программами больше различий, чем между дамбами или вторжениями. Или, наверное, старые методы не работают, поскольку старая идея избыточности не имеет аналогов в области разработки программного обеспечения: если дамба содержит бетона на 30% выше нормы, то это граница допустимого предела, но если программа работает на 30% больше, то это недопустимо FIXME.
Сложно сказать наверняка, почему старые методы не работают, но любой может видеть, что именно это и происходит. Когда программы сдавались вовремя? Опытные программисты знают, что вне зависимости от того, насколько тщательно вы планировали программу, в процессе ее написания планы в какой-то степени нарушатся. Иногда планы становятся безнадежно неверными. Но лишь немногие жертвы метода «спланируй и реализуй» ставят под сомнение его обоснованность. Вместо этого они начинают винить человеческие недостатки: если бы планы были более дальновидными, то всех этих неприятностей можно было бы избежать. Поскольку даже лучшие программисты, переходя к реализации, сталкиваются с проблемами, возможно, было бы слишком надеяться на то, что люди когда-нибудь станут такими прозорливыми. Вероятно, метод «спланируй и реализуй» может быть заменен другим подходом, более подходящим нашим критериям.
Если у нас есть правильные инструменты, то мы можем подойти к программированию по-другому. Для чего планировать перед реализацией? Большая опасность в чрезмерном планировании состоит в возможности быть загнанным в угол. Мы делаем, вот и все. Гибкость Лисп породила полностью новый стиль программирования. В Лисп многое можно спланировать в процессе написания программы.
Зачем ждать оценок прошлого? Как заметил Монтень, ничто так не проясняет наши идеи, как попытка записать их. Однажды вы освободитесь от беспокойства, которе загоняет вас в угол; вы сможете извлечь всю выгоду из этой возможности. Способность планировать программы по мере их написания имеет два важных последствия: на написание программ уходит меньше времени, потому что, когда вы пишете и планируете одновременно, у вас есть настоящая программа, чтобы сосредоточить на ней свое внимание; они становятся лучше, потому что окончательная разработка – это всегда продукт эволюции. Таким образом в процессе написания программы вы постоянно переписываете ошибочные части, как только становится очевидна их несостоятельность, в итоге конечный продукт будет более элегантным решением, чем если бы вы потратили недели, планируя его заранее.
Приспособляемость Лисп делает этот вид программирования практической альтернативой. Действительно, самая большая опасность Лисп состоит в том, что он может избаловать вас. Один раз попробовав Лисп, вы можете стать настолько чувствительным к соответствию между языком и приложением, что будете не в состоянии вернуться назад к другому языку без постоянного ощущения того, что он не дает в полной мере ту гибкость, которая вам необходима.
1.2 Программирование снизу-вверх
Давно известное правило методологии программирования состоит в том, что функциональные элементы программы не должны быть слишком большими. Если какой-то из элементов программы вырастает настолько, что перестает быть легким для понимания, то он превращается в запутанный клубок, скрывающий ошибки так же легко, как большой город скрывает беглецов. Такое программное обеспечение будет трудно читать, трудно проверять и трудно отлаживать.
В соответствии с этим правилом большая программа должна быть разбита на части, и чем больше программа, тем больше частей должно быть. Как же разбить программу? Традиционный подход называется нисходящим проектированием. Вы скажете: “Цель программы состоит в том, чтобы сделать семь таких-то вещей, значит, я разбиваю программу на семь главных подпрограмм. Первая подпрограмма должна сделать четыре таких-то вещи, поэтому у нее, в свою очередь, будет четыре собственные подпрограммы, и так далее”. Этот процесс продолжается до тех пор, пока вся программа не достигнет надлежащего уровня модульности – каждая часть достаточно велика, чтобы сделать что-то важное, но и достаточно мала для того, чтобы представлять собой отдельный модуль.
Опытные Лисп-программисты разбивают свои программы по-другому. Так же, как и в нисходящем проектировании, они следуют принципу, который можно было бы назвать восходящим проектированием – изменением языка для решения задачи. В Лисп вы не просто подгоняете свою программу к языку, вы также создаете язык для нее. По мере написания программы вам может прийти в голову: “Мне бы хотелось, чтобы в Лисп был такой-то оператор”. Тогда вы идете и пишете его. Позже вы понимаете, что использование нового оператора упростило бы разработку другой части программы и так далее. Язык и программа развиваются вместе. Как граница между двумя враждующими государствами, граница между языком и программой чертится и перечерчивается, пока в конечном счете черта не остается вдоль гор и рек – естественных границ вашей задачи. В итоге ваша программа будет выглядеть так, как будто язык был разработан специально для нее. И когда язык и программа будут хорошо соответствовать друг другу, вы получите ясный, маленький и эффективный код.
Стоит подчеркнуть, что восходящее проектирование не означает всего лишь написание той же программы в другом порядке. Когда вы работаете в восходящем стиле, то в итоге, как правило, получите другую программу. Вместо единственной, монолитной программы вы получите более объемный язык с более абстрактными операторами и более короткую программу, написанную на нем. Вместо перемычки у вас будет арка.
В типичном коде, как только вы обобщаете части, являющиеся просто бухгалтерией, то что остается намного короче FIXME; чем выше вы создаете язык, тем меньше расстояния вы должны будете проходить сверху вниз. Это дает несколько преимуществ:
1. Заставляя язык быть производительнее, восходящее проектирование помогает создать более шустрые и менее объемные программы. Небольшую программу ни к чему разбивать на слишком большое число компонент, ведь меньшее число компонент означает, что программы легче прочитать или изменить. Также, чем меньше число компонент, тем меньше связей между ними и, таким образом, меньше шанс на ошибки в этих местах. Как промышленные проектировщики стремятся сократить количество движущихся частей в машине,так и опытные программисты Лисп используют восходящее проектирование с целью уменьшения размера и сложности своих программ.
2. Восходящий дизайн способствует многократному использованию кода. Когда вы пишете две или более программы, многие из утилит, написанных вами для первой программы, пригодятся и для последующих. Собрав однажды большое количество утилит, на создание новой программы вы потратите лишь часть тех усилий, которые бы потребовались, начни вы писать на Лисп с нуля.
3. Восходящий дизайн делает программы более легкими для чтения. В случае этого типа абстракции требуется, чтобы читатель понял оператор общего назначения; в случае функциональной абстракции нужно, чтобы читатель понял подпрограмму специального назначения.1)
1) “Но никто не может читать программу без понимания всех ваших новых утилит.” Чтобы понять, почему такие утверждения обычно ошибочны, смотрите Раздел 4.8. FIXME
4. Заставляя вас всегда быть в поисках шаблонов в вашем коде, восходящее проектирование помогает прояснить ваши идеи о модели программы. Если два отдаленных компонента программы похожи по форме, то вы первым заметите сходство и, возможно, перепроектируете программу более простым способом.
Восходящее проектирование на языках, отличных от Лисп, осуществимо лишь до определенной степени. Всякий раз, когда вы видите библиотечные функции, происходит восходящее проектирование. Однако Лисп дает вам намного более широкие возможности в этом случае, и расширяемый язык играет соответственно большую роль в технике программирования на Лисп, настолько большую, что Лисп – это не просто другой язык, а совершенно иной способ программирования.
Это правда, что этот способ разработки лучше подходит для программ, которые могут быть написаны небольшими группами. Однако, в то же самое время, он расширяет пределы того, что может быть сделано небольшой группой. В книге “Мифический человеко-месяц” Фредерик Брукс (Frederick Brooks) предположил, что производительность группы программистов не растет линейно с ее размером. С увеличением размера группы понижается производительность отдельных программистов. Опыт программирования на Лисп предлагает более жизнерадостную формулировку этого закона: с уменьшением размера группы производительность индивидуальных программистов повышается. Небольшая группа побеждает, собственно говоря, просто потому, что она меньше. Когда небольшая группа воспользуется еще и методиками, которые делает доступными Лисп, то она может победить вчистую.
1.3 Расширяемое программное обеспечение
Именно техника программирования на Лисп стала более важна по мере роста сложности программного обеспечения. Искушенные пользователи теперь требуют так много от ПО, что мы не имеем возможности предугадывать все их потребности. Они и сами не могут их предугадать. Но если мы не можем дать им софт, который делает всё, что они хотят, сразу из коробки, то мы можем дать им расширяемое программное обеспечение. Мы преобразуем наше ПО из обычных программ в язык программирования, и продвинутые пользователи могут надстраивать поверх него необходимые им дополнительные функции.
Восходящий дизайн естественным образом ведет к расширяемым программам. Простейшие восходящие программы состоят из двух уровней: язык и программа. Сложные программы могут быть написаны как последовательность уровней, каждый из которых выступает в качестве языка для лежащего над ним. Если эту философию пронести через весь процесс до самого верхнего уровня, то тот уровень и станет программным языком для пользователя. Такая программа, где расширяемость проходит сквозь каждый уровень, вероятно, создает более хороший язык программирования, нежели системы, которые были написаны как традиционный черный ящик, и лишь с запозданием сделаны расширяемыми.
X Windows и TEX – примеры первых программ, основанных на этом принципе. В 1980-е более хорошее оборудование сделало возможным новое поколение программ, которые использовали Лисп в качестве собственного языка расширений. Первым был Gnu Emacs – популярный текстовый редактор Unix. Позже появился Autocad – первый крупномасштабный коммерческий продукт, давший возможность использовать Лисп как расширяемый язык. В 1991 году Interleaf представила новую версию своего продукта, который не только использовал Лисп как расширяемый язык, но и был в значительной степени на нем же и реализован.
Лисп – особенно хороший язык для написания расширяемых программ, потому что он сам расширяем. Если вы напишете свою программу на Лисп так, чтобы передать эту расширяемость пользователю, вы фактически получите расширяемый язык бесплатно. Различие между расширением Лисп-программы в Лисп и тем же самым, но на традиционном языке, равносильно различию между личной встречей с кем-нибудь и почтовой перепиской. В программе, которая сделана расширяемой просто за счет предоставления доступа к внешним программам, лучшее, на что мы можем рассчитывать – это два черных ящика, соединенных один с другим при помощи какого-нибудь заранее определенного протокола. В Лисп же расширения могут иметь прямой доступ ко всем подпрограммам. Это не означает, что вы должны предоставить пользователям доступ к каждой части своей программы – просто теперь у вас есть выбор, давать ли им доступ или нет.
Когда такой уровень доступа встречается с интерактивным окружением, достигается наилучшая расширяемость. Любая программа, которую можно использовать как основу для собственных расширений – это, вероятно, довольно большая программа, возможно, слишком большая для того, чтобы вы держали в уме ее полный образ. Что происходит, когда вы в чем-то не уверены? Если программа написана на Лисп, вы можете испытать ее в диалоговом режиме: вы можете изучить ее структуры данных, можете вызвать ее функции, даже можете посмотреть на изначальный исходный код. Такой вид обратной связи дает вам возможность программировать с большой уверенностью: писать более мощные расширения и писать их быстрее. Диалоговое окружение всегда делает программирование проще, но оно приобретает имеет особую ценность при написании расширений.
Расширяемая программа – это обоюдоострый меч, тем не менее последний опыт показал, что пользователи предпочитают обоюдоострый меч тупому. Расширяемые программы, похоже, одерживают верх, несмотря на присущие им опасности.
1.4 Расширение Лисп
Существует два способа добавления в Лисп новых операторов: функции и макрос. В Лисп определяемые вами функции имеют тот же статус, что и встроенные. Если вы хотите новый вариант mapcar, вы можете сами его определить и использовать точно так же, как использовали бы mapcar. Например, если вам нужен список значений, возвращаемых некой функцией, примененной ко всем целым числам от 1 до 10, вы могли бы создать новый список и передать его mapcar:
(mapcar fn (do* ((x 1 (1+ x)) (result (list x) (push x result))) ((= x 10) (nreverse result))))
но такой подход и уродливый и неэффективный. 2) место этого вы могли бы определить новую отображающую функцию map1-n (см. страницу 54) и затем ее вызвать следующим образом:
2) Вы могли бы написать это более красиво при помощи последовательности новых макросов Коммон Лисп, но это лишь доказывает ту же точку зрения, так как эти макросы сами по себе являются расширением Лисп.
(map1-n fn 10)
Функции определяются относительно просто. Макросы предоставляют более широкое, но и менее понятное средство определения новых операторов. Макросы – это программы, которые пишут программы. Это утверждение имеет глубокий смысл, и его раскрытие – одна из основных целей книги.
Осмысленное использование макросов ведет к программам, удивляющим ясностью и изящностью. Эти сокровища не даются просто так. В итоге макросы, наверное, самая естественная в мире вещь, но вначале они могут быть трудными для понимания. Частично это из-за того, что они имеют больше возможностей, чем функции, поэтому, когда их пишешь, больше приходится держать в голове. Но основная причина, почему макросы трудны для понимания, это потому, что они незнакомы. Никакой другой язык не имеет ничего, подобного Лисп-макросам. Таким образом, изучение макросов позволит рассеять предрассудки, неосторожно приобретенные от других языков. Самый главный из которых – это представление программы в виде чего-то, подверженного трупному окоченению. Почему структуры данных должны быть гибкими и изменяемыми, а программы нет? В Лисп программы – это данные, но требуется время, чтобы вникнуть в смысл этого факта.
Если привыкание к макросам и займет некоторое время, то оно стоит затраченных усилий. Даже в таком простом применении, как итерация, макросы могут сделать программы существенно меньше и чище. Предположим, что программа должна произвести итерацию с некоторым участком кода для x от a до b. Встроенный в Лисп оператор do предназначен для более общих случаев. Для простой итерации он не даст наиболее читаемый код:
(do ((x a (+ 1 x))) ((> x b)) (print x))
Вместо этого, допустим, мы могли бы сказать всего лишь:
(for (x a b) (print x))
Макросы делают это возможным. При помощи шести строчек кода (см. стр. 154) мы можем добавить к языку for, как будто бы он там был с самого начала. И, как покажут последующие главы, написание for только начало того, что вы можете делать с макросами.
Вы не ограничены расширять Лисп за раз только функцией или макросом. Если потребуется, вы можете создать целый язык поверх Лисп и писать на нем свои программы. Лисп – это превосходный язык для написания компиляторов и интерпретаторов, но он предлагает иной способ описания нового языка, который зачастую более изящен и, несомненно, менее трудоемок – описать новый язык как модификацию Лисп. В таком случае части Лисп, которые могут появиться в новом языке без изменений (например, арифметика или ввод/вывод), могут быть использованы как есть, и вам нужно реализовать компоненты, которые отличаются (например, управляющую структуру). Язык, реализованный таким образом, называется встраиваемым языком.
Встраиваемые языки – естественный результат восходящего программирования. Коммон Лисп уже включает в себя несколько. Наиболее известный из них – CLOS – рассматривается в последней главе. Но вы также можете описать и свои собственные встраиваемые языки. Вы можете получить язык, который подходит к программе, даже если он станет выглядеть совершенно отличным от Лисп.
1.5 Почему Lisp (или Когда)
Эти новые возможности не происходят от единственного волшебного ингридиента. В этом отношении Lisp как арка. Какой из клинообразных камней (voussoirs) является тем, что поддерживает арку? Вопрос неверен сам по себе: они все являются. Как и арка, Лисп – это набор взаимосвязанных функций. Мы можем перечислить некоторые из этих функций: динамическое распределение памяти и сборка мусора, динамическая типизация, функции как объекты, встроенный систаксический анализатор, генерирующий списки, компилятор, принимающий программы, представленные в виде списков, диалоговое окружение, и так далее, но ни в одной из них нельзя усмотреть способности Лисп. Это сочетание, делающее программирование на Лисп таким, какое есть.
На протяжении двадцати последних лет подход к программированию изменился. Многие из этих изменений: диалоговые окружения, динамическое связывание, даже объектно-ориентированное программирование – были частичными попытками передать другим языкам немного гибкости Лисп. Метафора арки как бы намекает, насколько хорошо они преуспели.
Широко известно, что Лисп и Фортран – два, пока еще используемых, старейших языка. Что, наверное, наиболее важно, так это то, что они представляют противоположные стороны философии дизайна языков программирования. Фортран был изобретен как расширение ассемблера. Лисп был изобретен как язык для выражения алгоритмов. Такие разные стремления привели к совершенно разным языкам. Фортран облегчает жизнь автору компилятора, Лисп облегчает жизнь программиста. Большинство языков программирования впоследствии распределились где-то между двумя полюсами. Фортран и Лисп переместились ближе к центру. Фортран сейчас выглядит больше, как Алгол, а Лисп лишился некоторых скверных привычек своей юности.
Изначально Фортран и Лисп характеризовались как, своего рода, два враждующих лагеря. В одном из них кричали: “Эффективность!” (За исключением того, что было бы слишком трудно реализовать). В другом лагере кричали: “Абстракция!” (В любом случае это непромышленное ПО). И боги определили из далеких исходов древнегреческих баталий, что исход этой битвы будет определен аппаратным обеспечением. С каждым годом обстоятельства складываются лучше для Лисп. Аргументы против Лисп сейчас стали звучать очень похоже на аргументы программистов на ассемблере, направленные против высокоуровневых языков в середине 1970-х. Вопрос, который сейчас ставится, в том не “Почему Лисп?”, а “Когда?”.
