В индустрии программного обеспечения идет непрекращающаяся борьба между яйцеголовыми учеными и другой, не менее внушительной силой — островолосыми начальниками. Все ведь знают, кто такой островолосый начальник? Думаю, большинство людей из мира технологий не просто узнают этого персонажа комиксов, но и знают в своей компании конкретного человека, с которого он срисован.
Островолосый начальник чудесным образом сочетает в себе два качества, которые часто встречаются по отдельности, но редко — вместе: (а) он совершенно ничего не смыслит в технологиях и (б) имеет о них крайне твердое мнение.
Представьте, к примеру, что вам нужно написать программу. Островолосый начальник понятия не имеет, как эта программа должна работать, и не отличает один язык программирования от другого, но при этом точно знает, на каком языке ее нужно писать. Разумеется. Он считает, что ее нужно писать на Java.
Почему он так считает? Давайте заглянем в мозг островолосого начальника. Думает он примерно следующее. Java — это стандарт. Я знаю, что это непременно так, ведь я постоянно читаю об этом в прессе. А раз это стандарт, у меня не будет неприятностей за его использование. И это также означает, что программистов на Java всегда будет предостаточно, так что если программисты, работающие на меня сейчас, уволятся — а они у меня почему-то всегда увольняются, — я легко найду им замену.
Что ж, звучит не так уж неразумно. Но все это строится на одном невысказанном предположении, и это предположение оказывается ложным. Островолосый начальник верит, что все языки программирования более-менее эквивалентны. Если бы это было правдой, он попал бы в самую точку. Если все языки одинаковы, конечно же, используйте тот, который используют все остальные.
Но все языки не эквивалентны, и, думаю, я могу доказать вам это, даже не вдаваясь в различия между ними. Если бы вы спросили островолосого начальника в 1992 году, на каком языке следует писать софт, он ответил бы столь же не колеблясь, как и сегодня. Софт нужно писать на C++. Но если все языки эквивалентны, с какой стати мнению островолосого начальника вообще меняться? И более того, зачем разработчикам Java вообще нужно было утруждать себя созданием нового языка?
По всей видимости, если вы создаете новый язык, то потому, что считаете его в чем-то лучше того, что у людей уже есть. И действительно, Гослинг в первой же белой книге по Java прямо заявляет, что Java была разработана для исправления некоторых проблем C++. Вот вам и ответ: языки не эквивалентны друг другу. Если проследить цепочку мыслей в мозгу островолосого начальника до Java, а затем обратно через историю Java к ее истокам, вы придете к мысли, противоречащей предположению, с которого вы начали.
Так кто же прав? Джеймс Гослинг или островолосый начальник? Неудивительно, что прав Гослинг. Некоторые языки для определенных задач действительно лучше других. И знаете, это порождает несколько интересных вопросов. Java разрабатывалась так, чтобы для определенных задач быть лучше C++. Для каких именно? В каких случаях лучше Java, а в каких — C++? Бывают ли ситуации, когда другие языки лучше их обоих?
Стоит только задуматься над этим вопросом, как вы открываете настоящий ящик Пандоры. Если бы островолосому начальнику пришлось обдумывать эту проблему во всей ее сложности, его мозг просто взорвался бы. Пока он считает все языки эквивалентными, все, что ему нужно сделать, — это выбрать тот, который, кажется, пользуется наибольшей популярностью, а поскольку это скорее вопрос моды, чем технологий, даже он, вероятно, сможет угадать правильный ответ. Но если языки различаются, ему внезапно приходится решать систему из двух уравнений, пытаясь найти оптимальный баланс между двумя вещами, в которых он ничего не смыслит: относительной пригодностью примерно двадцати ведущих языков для задачи, которую ему нужно решить, и шансами найти программистов, библиотеки и т. д. для каждого из них. Если за дверью скрывается именно это, неудивительно, что островолосый начальник не желает ее открывать.
Минус веры в эквивалентность всех языков программирования состоит в том, что это неправда. Но плюс — в том, что это делает жизнь намного проще. И я думаю, это главная причина, почему эта идея столь распространена. Это удобная идея.
Мы знаем, что Java непременно должна быть весьма хороша, потому что это крутой, новый язык программирования. Или нет? Если взглянуть на мир языков программирования издалека, кажется, что Java — это последний писк моды. (С достаточно большого расстояния видно только огромный мигающий рекламный щит, оплаченный Sun.) Но если присмотреться к этому миру поближе, обнаружится, что у крутости есть степени. В хакерской субкультуре есть другой язык, Perl, который считается намного круче Java. Slashdot, к примеру, генерируется на Perl. Не думаю, что вы застанете этих ребят за использованием Java Server Pages. Но есть и другой, более новый язык под названием Python, пользователи которого склонны смотреть на Perl свысока, и другие уже на подходе.
Если взглянуть на эти языки по порядку — Java, Perl, Python, — можно заметить интересную закономерность. По крайней мере, вы заметите эту закономерность, если вы Lisp-хакер. Каждый следующий шаг становится все ближе к Lisp. Python копирует даже те особенности, которые многие Lisp-хакеры считают ошибками. Простые программы на Lisp можно переводить на Python строчка за строчкой. На дворе 2002 год, и языки программирования почти догнали 1958-й.
В погоне за математикой
Я имею в виду, что Lisp был впервые открыт Джоном Маккарти в 1958 году, и популярные языки программирования только сейчас догоняют идеи, которые он тогда разработал.
Но как такое вообще может быть? Разве компьютерные технологии не развиваются стремительно? Я хочу сказать, в 1958 году компьютеры были громадинами размером с холодильник и обладали вычислительной мощностью наручных часов. Как технология столь почтенного возраста может быть вообще актуальной, не говоря уже о превосходстве над новейшими разработками?
Я скажу вам как. Дело в том, что Lisp на самом деле не проектировался как язык программирования — по крайней мере, не в том смысле, который мы вкладываем в это понятие сегодня. Под языком программирования мы понимаем инструмент, с помощью которого мы говорим компьютеру, что делать. Маккарти со временем действительно намеревался разработать язык программирования в этом смысле, но Lisp, который у нас в итоге получился, основывался на совершенно отдельной работе, которую он проделал в качестве теоретического упражнения — попытки определить более удобную альтернативу машине Тьюринга. Как позже сказал сам Маккарти,
Еще одним способом показать, что Lisp изящнее машин Тьюринга, было написать универсальную функцию Lisp и продемонстрировать, что она короче и понятнее описания универсальной машины Тьюринга. Этой функцией была Lisp-функция eval..., которая вычисляет значение Lisp-выражения... Написание eval потребовало изобретения нотации для представления Lisp-функций в виде Lisp-данных, и такая нотация была разработана исключительно для целей статьи, без мысли о том, что она будет использоваться для написания реальных программ на Lisp на практике.
Далее произошло следующее: где-то в конце 1958 года Стив Рассел, один из аспирантов Маккарти, взглянул на это определение eval и понял, что если перевести его на машинный язык, то результатом станет интерпретатор Lisp.
В то время это стало огромным сюрпризом. Вот что Маккарти сказал об этом позже в интервью:
Стив Рассел сказал: слушайте, почему бы мне не запрограммировать этот eval..., а я ответил ему: хо-хо, вы путаете теорию с практикой, этот eval предназначен для чтения, а не для вычислений. Но он взял и сделал это. То есть он скомпилировал eval из моей статьи в машинный код [IBM] 704, исправил ошибки, а затем объявил это интерпретатором Lisp, коим он, безусловно, и являлся. Так что в тот момент Lisp по сути обрел ту форму, которую имеет сегодня...
Внезапно, буквально за считаные недели, Маккарти обнаружил, что его теоретическое упражнение превратилось в настоящий язык программирования — и притом более мощный, чем он планировал.
Так что краткое объяснение того, почему этот язык из 1950-х не устарел, заключается в том, что он был не технологией, а математикой, а математика не устаревает. Сравнивать Lisp следует не с «железом» 1950-х годов, а, скажем, с алгоритмом Quicksort, который был открыт в 1960 году и до сих пор остается быстрейшей сортировкой общего назначения.
Есть еще один язык, доживший до наших дней с 1950-х годов, — Fortran, и он представляет противоположный подход к проектированию языков. Lisp был частью теории, которая неожиданно превратилась в язык программирования. Fortran создавался намеренно как язык программирования, но такой, который мы сегодня посчитали бы очень низкоуровневым.
Fortran I, язык, созданный в 1956 году, был совсем другим зверем по сравнению с современным Fortran. Fortran I был, по сути, ассемблером с математикой. В чем-то он даже уступал более поздним ассемблерам: в нем, например, не было подпрограмм, только переходы. Сегодняшний Fortran теперь, пожалуй, ближе к Lisp, чем к Fortran I.
Lisp и Fortran были стволами двух разных эволюционных деревьев: одно уходило корнями в математику, другое — в архитектуру машин. С тех пор эти два дерева сближаются. Lisp начинал как мощный язык и в течение следующих двадцати лет стал быстрым. Так называемые мейнстримные языки начинали как быстрые и в течение следующих сорока лет постепенно становились мощнее, пока сегодня самые передовые из них не подобрались к Lisp довольно близко. Близко, но кое-чего им все еще не хватает...
Что делало Lisp особенным
Когда Lisp только появился, он воплотил в себе девять новых идей. Некоторые из них мы сегодня принимаем как должное, другие встречаются только в более продвинутых языках, а две до сих пор остаются уникальными для Lisp. Вот эти девять идей в порядке их принятия мейнстримом: Условные конструкции. Условная конструкция — это структура вида if-then-else. Сейчас мы воспринимаем ее как должное, но в Fortran I ее не было. Был только условный goto, тесно привязанный к базовой машинной инструкции.
Тип «функция». В Lisp функции являются таким же типом данных, как целые числа или строки. У них есть литеральное представление, их можно сохранять в переменных, передавать в качестве аргументов и так далее.
Рекурсия. Lisp был первым языком программирования, который стал ее поддерживать.
Динамическая типизация. В Lisp все переменные фактически являются указателями. Типами обладают значения, а не переменные, и присваивание или связывание переменных означает копирование указателей, а не того, на что они указывают.
Сборка мусора.
Программы, состоящие из выражений. Программы на Lisp представляют собой деревья выражений, каждое из которых возвращает значение. Это контрастирует с Fortran и большинством последующих языков, проводящих различие между выражениями и инструкциями (statements).
Такое различие было естественным для Fortran I, потому что в нем нельзя было делать вложенные инструкции. И хотя для работы математики выражения были необходимы, не имело никакого смысла заставлять что-либо еще возвращать значение, поскольку никто не мог ожидать этого результата.
Это ограничение исчезло с появлением блочно-структурированных языков, но к тому моменту было уже слишком поздно. Разделение на выражения и инструкции укоренилось. Из Fortran оно перекочевало в Algol, а затем и к потомкам обоих языков.
Тип «символ». Символы — это, по сути, указатели на строки, хранящиеся в хеш-таблице. Таким образом, проверять равенство можно простым сравнением указателя, вместо посимвольного сравнения строк.
Нотация для кода с использованием деревьев из символов и констант.
Язык целиком доступен в любое время. Нет реального разделения между временем чтения, временем компиляции и временем выполнения. Вы можете компилировать или выполнять код во время чтения, читать или выполнять код во время компиляции, а также читать или компилировать код во время выполнения.
Выполнение кода во время чтения позволяет пользователям перепрограммировать синтаксис Lisp; выполнение кода во время компиляции лежит в основе макросов; компиляция во время выполнения — основа использования Lisp в качестве языка расширения в таких программах, как Emacs; а чтение во время выполнения позволяет программам взаимодействовать с помощью s-выражений — идея, которую недавно заново изобрели под видом XML. Когда Lisp только появился, эти идеи были бесконечно далеки от повседневной практики программирования, которая в значительной степени диктовалась аппаратным обеспечением конца 1950-х годов. Со временем язык «по умолчанию», воплощавшийся в череде популярных языков, постепенно эволюционировал в сторону Lisp. Идеи 1–5 сегодня распространены повсеместно. Номер 6 начинает проникать в мейнстрим. В Python есть некоторая форма 7-й идеи, хотя для нее, кажется, нет явного синтаксиса.
Что касается номера 8, то это, пожалуй, самая интересная идея из всех. Идеи 8 и 9 стали частью Lisp совершенно случайно — просто потому, что Стив Рассел реализовал то, что Маккарти вовсе не планировал реализовывать. И все же именно эти идеи ответственны как за странный внешний вид Lisp, так и за его самые отличительные черты. Lisp выглядит странно не столько потому, что у него странный синтаксис, сколько потому, что у него вообще нет синтаксиса; вы пишете программы прямо в виде деревьев синтаксического анализа, которые в других языках строятся незаметно на этапе парсинга, и эти деревья состоят из списков — структур данных самого Lisp.
Выражение языка в его собственных структурах данных оказывается невероятно мощной возможностью. Идеи 8 и 9 в сочетании означают, что вы можете писать программы, которые пишут программы. Это может звучать как дикая идея, но в Lisp это обычное дело. Самый распространенный способ делать это — использовать так называемые макросы.
Термин «макрос» в Lisp означает вовсе не то, что в других языках. Макрос в Lisp может быть чем угодно — от простого сокращения до компилятора нового языка. Если вы действительно хотите понять Lisp или просто расширить свой кругозор в программировании, я рекомендую узнать больше о макросах.
Макросы (в понимании Lisp), насколько мне известно, до сих пор уникальны для Lisp. Отчасти это связано с тем, что ради наличия макросов вам, вероятно, придется сделать свой язык столь же странным на вид, как Lisp. Возможно, дело еще и в том, что если вы добавите эту последнюю каплю мощи, вы больше не сможете утверждать, что изобрели новый язык, — это будет всего лишь новый диалект Lisp.
Я говорю это скорее в шутку, но это чистая правда. Если вы определите язык, в котором есть car, cdr, cons, quote, cond, atom, eq и нотация для функций, выраженных в виде списков, то вы сможете построить весь остальной Lisp на этой базе. В сущности, это и есть определяющее свойство Lisp: именно ради того, чтобы это стало возможным, Маккарти и придал Lisp ту форму, которую он имеет.
Где языки имеют значение
Итак, допустим, Lisp действительно представляет собой некий предел, к которому мейнстримные языки приближаются асимптотически — означает ли это, что вам действительно стоит использовать его для написания софта? Сколько вы теряете, используя менее мощный язык? Не разумнее ли иногда воздержаться от нахождения на самом острие инноваций? И разве популярность в какой-то мере не оправдывает сама себя? Разве островолосый начальник не прав, например, в своем желании использовать язык, под который можно легко нанять программистов?
Безусловно, есть проекты, где выбор языка программирования не имеет большого значения. Как правило, чем требовательнее приложение, тем большую отдачу дает использование мощного языка. Но масса проектов вовсе не требовательна. Большая часть программирования, вероятно, состоит в написании небольших программ-«склеек», а для них можно использовать любой язык, с которым вы уже знакомы и для которого есть хорошие библиотеки под вашу задачу. Если вам нужно просто перегнать данные из одного приложения для Windows в другое, конечно, используйте Visual Basic.
Писать маленькие программы-«склейки» можно и на Lisp (я использую его как настольный калькулятор), но наибольший выигрыш от языков вроде Lisp проявляется на другом конце спектра — там, где вам нужно писать сложные программы для решения трудных задач в условиях жесткой конкуренции. Хороший пример — программа поиска авиабилетов, которую компания ITA Software лицензирует сервису Orbitz. Эти ребята вышли на рынок, где уже доминировали два крупных, прочно обосновавшихся конкурента — Travelocity и Expedia, — и, похоже, просто раскатали их в технологическом плане.
Сердцем приложения ITA является программа на Common Lisp объемом 200 000 строк, которая перебирает на много порядков больше вариантов, чем их конкуренты, по-видимому, все еще использующие методы программирования эпохи мейнфреймов. (Хотя и ITA в некотором смысле использует язык программирования эпохи мейнфреймов.) Я никогда не видел исходный код ITA, но, по словам одного из их ведущих хакеров, они активно используют макросы, и меня это ничуть не удивляет.
Центростремительные силы
Я не утверждаю, что использование непопулярных технологий ничего не стоит. Островолосый начальник не совсем уж неправ, когда беспокоится об этом. Но поскольку он не понимает рисков, он склонен их преувеличивать.
Мне видятся три проблемы, которые могут возникнуть из-за использования менее распространенных языков. Ваши программы могут плохо взаимодействовать с программами, написанными на других языках. В вашем распоряжении может оказаться меньше библиотек. И у вас могут возникнуть трудности с наймом программистов.
Насколько серьезна каждая из этих проблем? Важность первой зависит от того, контролируете ли вы всю систему целиком. Если вы пишете софт, который должен работать на удаленной машине пользователя поверх глючной закрытой операционной системы (не будем называть имен), написание приложения на том же языке, что и ОС, может дать преимущества. Но если вы контролируете всю систему и у вас есть исходный код всех компонентов, как, вероятно, у ITA, вы можете использовать любые языки. Если возникнет какая-либо несовместимость, вы сможете исправить ее сами.
В серверных приложениях вполне можно позволить себе использовать самые передовые технологии, и я думаю, что именно в этом кроется главная причина того, что Джонатан Эриксон называет «ренессансом языков программирования». Вот почему мы вообще слышим о новых языках вроде Perl и Python. Мы слышим о них не потому, что люди пишут на них программы для Windows, а потому, что люди используют их на серверах. И по мере того как софт перемещается с настольных компьютеров на серверы (будущее, с которым, похоже, смирилась даже Microsoft), давления в пользу выбора средненьких, шаблонных технологий будет становиться все меньше.
Что касается библиотек, то их важность также зависит от приложения. Для менее требовательных задач наличие библиотек способно перевесить внутреннюю мощь языка. Где находится точка безубыточности? Трудно сказать точно, но где бы она ни была, она не дотягивает до уровня того, что можно было бы назвать полноценным приложением. Если компания считает себя разработчиком программного обеспечения и пишет приложение, которое станет одним из ее продуктов, над ним, вероятно, будет работать несколько хакеров, и это займет как минимум полгода. В проекте такого масштаба мощные языки, скорее всего, начнут перевешивать удобство готовых библиотек.
Третье опасение островолосого начальника — трудности с наймом программистов — я считаю ложным следом. В конце концов, сколько хакеров вам нужно нанять? Наверняка к настоящему моменту мы все усвоили, что софт лучше всего разрабатывать командами менее десяти человек. А уж найти хакеров в таком количестве под любой язык, о котором хоть кто-то слышал, не должно составить труда. Если вы не можете найти десять Lisp-хакеров, то ваша компания, вероятно, обосновалась в неподходящем для разработки программного обеспечения городе.
На самом деле выбор более мощного языка, скорее всего, уменьшит размер необходимой вам команды, потому что (а) если вы используете более мощный язык, вам, вероятно, понадобится меньше хакеров, и (б) хакеры, работающие на более продвинутых языках, как правило, умнее.
Я не утверждаю, что на вас не будут давить с требованием использовать то, что считается «стандартными» технологиями. В Viaweb (ныне Yahoo Store) мы вызвали недоумение у венчурных инвесторов и потенциальных покупателей тем, что использовали Lisp. Но мы также вызывали недоумение тем, что использовали обычные компьютеры на базе процессоров Intel вместо «промышленных» серверов вроде машин от Sun; тем, что использовали малоизвестный тогда открытый вариант Unix под названием FreeBSD вместо настоящей коммерческой ОС вроде Windows NT; тем, что игнорировали так называемый стандарт электронной коммерции SET, о котором сейчас уже никто и не помнит, и так далее.
Нельзя позволять людям в костюмах принимать за вас технические решения. Напугало ли некоторых потенциальных покупателей то, что мы использовали Lisp? Некоторых слегка насторожило, но если бы мы не использовали Lisp, мы не смогли бы написать софт, из-за которого они вообще захотели нас купить. То, что казалось им аномалией, на самом деле было причиной и следствием.
Если вы запускаете стартап, не создавайте свой продукт ради ублажения венчурных капиталистов или потенциальных покупателей. Создавайте свой продукт ради пользователей. Если вы завоюете пользователей, все остальное приложится. А если не завоюете, никого не будет волновать, насколько успокаивающе ортодоксальным был ваш выбор технологий.
Цена посредственности
Сколько вы теряете, используя менее мощный язык? На этот счет на самом деле есть некоторые данные.
Наиболее удобным показателем мощности, вероятно, является размер кода. Смысл языков высокого уровня заключается в предоставлении более крупных абстракций — так сказать, более крупных кирпичей, благодаря чему их требуется меньше для возведения стены заданного размера. Таким образом, чем мощнее язык, тем короче программа (конечно, не просто в символах, а в смысловых элементах).
Каким образом более мощный язык позволяет писать более короткие программы? Один из приемов, к которому можно прибегнуть, если язык это позволяет, называется программированием снизу вверх (bottom-up). Вместо того чтобы просто писать приложение на базовом языке, вы создаете поверх него специализированный язык для написания программ вроде вашей, а затем пишете саму программу на нем. Получившийся совокупный код может оказаться намного короче, чем если бы вы писали всю программу на базовом языке, — собственно, именно так и работает большинство алгоритмов сжатия. Программу, построенную снизу вверх, к тому же должно быть легче модифицировать, поскольку во многих случаях языковой слой вообще не придется менять.
Размер кода важен, потому что время, необходимое для написания программы, зависит главным образом от ее длины. Если ваша программа на другом языке окажется втрое длиннее, ее написание займет в три раза больше времени — и вы не сможете обойти это, наняв больше людей, потому что сверх определенного предела новые сотрудники фактически приносят чистый убыток. Фред Брукс описал этот феномен в своей знаменитой книге «Мифический человеко-месяц», и все, что я наблюдал в жизни, лишь подтверждало его слова.
Так насколько же короче получаются программы, если писать их на Lisp? Большинство цифр, которые я слышал при сравнении Lisp с C, например, крутятся вокруг соотношения 7–10x. Однако в недавней статье об ITA в журнале New Architect говорилось, что «одна строка Lisp способна заменить 20 строк C», а поскольку в этой статье было полно цитат президента ITA, я предполагаю, что эти данные журналисты получили непосредственно от ITA. Если так, мы можем вполне им доверять: в программном обеспечении ITA используется много C и C++, наряду с Lisp, поэтому они говорят со знанием дела.
Подозреваю, что эти коэффициенты даже не являются постоянной величиной. Мне кажется, они возрастают при столкновении с более сложными задачами, а также при наличии более умных программистов. По-настоящему классный хакер способен выжать из более совершенных инструментов гораздо больше.
Как бы то ни было, вот вам одна точка на графике: если бы вы решили конкурировать с ITA и стали писать свой софт на C, они разрабатывали бы его в двадцать раз быстрее вас. Если бы вы потратили год на добавление новой функции, они смогли бы повторить ее менее чем за три недели. Тогда как если бы они потратили всего три месяца на разработку чего-то нового, вам потребовалось бы пять лет, чтобы догнать их.
И знаете что? Это ещё оптимистичный сценарий. Рассуждая о соотношении объёмов кода, вы неявно предполагаете, что программу на более слабом языке вообще удастся написать. Но на самом деле возможности программистов не безграничны. Если пытаться решить сложную задачу на слишком низкоуровневом языке, наступает момент, когда удерживать всё в голове одновременно становится просто невозможно.
Поэтому, когда я говорю, что воображаемому конкуренту ITA потребовалось бы пять лет, чтобы повторить то, что ITA могла написать на Lisp за три месяца, я имею в виду пять лет при условии, что всё пойдёт гладко. На самом же деле, судя по тому, как устроена работа в большинстве компаний, любой проект разработки, рассчитанный на пять лет, скорее всего, не будет закончен вовсе.
Признаю, это крайний случай. Хакеры из ITA, похоже, необычайно умны, а C — язык довольно низкоуровневый. Но в условиях конкурентного рынка даже разницы в два или три раза будет достаточно, чтобы гарантировать вам вечное отставание.
Рецепт
О такой перспективе волосаторогий босс (pointy-haired boss) даже думать не желает. И большинство из них не думают. Потому что, по правде говоря, волосаторогому боссу плевать, если его компании надерут задницу, лишь бы никто не мог доказать, что это его вина. Самый безопасный план для него лично — держаться ближе к центру стада.
В крупных организациях для описания такого подхода используется выражение «лучшие отраслевые практики» (industry best practice). Его цель — защитить волосаторогого босса от ответственности: если он выбирает то, что считается «лучшей отраслевой практикой», и компания терпит крах, его нельзя обвинить. Это не он так решил, это решила отрасль.
Полагаю, изначально этот термин использовался для описания методов бухгалтерского учёта и тому подобного. Грубо говоря, он означает: не делай ничего странного. И в бухгалтерии это, вероятно, хорошая идея. Слова «передовой» и «бухучёт» плохо сочетаются друг с другом. Но когда вы переносите этот критерий на решения о технологиях, вы начинаете получать неверные ответы.
Технологии часто должны быть передовыми. Что касается языков программирования, то, как заметил Эранн Гат, следуя «лучшим отраслевым практикам», вы на самом деле получаете не лучшее, а всего лишь среднее. Когда решение приводит к тому, что вы разрабатываете программное обеспечение со скоростью, составляющей лишь малую долю от скорости более агрессивных конкурентов, называть это «лучшей практикой» — явное заблуждение.
Итак, перед нами две мысли, которые я считаю весьма ценными. Собственно, я знаю это по собственному опыту. Во-первых, языки различаются по своей выразительной силе. Во-вторых, большинство менеджеров намеренно закрывают на это глаза. Вместе эти два факта представляют собой буквально рецепт зарабатывания денег. ITA — пример этого рецепта в действии. Если вы хотите победить в софтверном бизнесе, просто беритесь за самую сложную задачу, какую сможете найти, используйте самый мощный язык, какой сможете получить, и ждите, пока волосаторогие боссы ваших конкурентов скатятся обратно к посредственности.
Приложение: Мощность языков
В качестве иллюстрации того, что я имею в виду под относительной мощностью языков программирования, рассмотрим следующую задачу. Мы хотим написать функцию, создающую аккумуляторы, — функцию, которая принимает число n и возвращает функцию, которая принимает другое число i и возвращает n, увеличенное на i.
(Именно увеличенное на, а не сумму. Аккумулятор должен аккумулировать.)
На Common Lisp это будет (defun foo (n) (lambda (i) (incf n i))), а на Perl 5 — sub foo { my ($n) = @_; sub {$n += shift} }, где элементов больше, чем в версии на Lisp, поскольку в Perl приходится извлекать параметры вручную.
На Smalltalk код получается чуть длиннее, чем на Lisp: foo: n |s| s := n. ^[:i| s := s+i. ], потому что, хотя лексические переменные в целом работают, присвоить значение параметру нельзя, и приходится создавать новую переменную s.
На Javascript пример опять же немного длиннее, поскольку Javascript сохраняет различие между инструкциями (statements) и выражениями (expressions), так что вам требуются явные инструкции return для возврата значений: function foo(n) { return function (i) { return n += i } } (Справедливости ради, Perl тоже сохраняет это различие, но обходится с ним в типичной для Perl манере, позволяя опускать return.)
Если попытаться перевести код с Lisp/Perl/Smalltalk/Javascript на Python, вы столкнётесь с некоторыми ограничениями. Поскольку Python не поддерживает лексические переменные в полной мере, вам придётся создать структуру данных для хранения значения n. И хотя в Python есть тип данных «функция», для него нет литерального представления (если только тело не состоит из одного-единственного выражения), поэтому вам нужно создать именованную функцию для возврата. В итоге получается следующее: def foo(n): s = [n] def bar(i): s[0] += i return s[0] return bar Пользователи Python могут резонно спросить, почему нельзя просто написать def foo(n): return lambda i: return n += i или даже def foo(n): lambda i: n += i, и я полагаю, что однажды они, вероятно, смогут это делать. (Но если они не хотят ждать, пока Python пройдёт оставшуюся часть пути эволюции в Lisp, они всегда могут просто...)
В объектно-ориентированных языках можно в ограниченной степени симулировать замыкание (функцию, которая обращается к переменным, определённым в объемлющих областях видимости), определив класс с одним методом и полем для замены каждой переменной из объемлющей области видимости. Это заставляет программиста выполнять анализ кода, который в языке с полной поддержкой лексической области видимости делал бы компилятор; это не сработает, если к одной и той же переменной обращается более одной функции, но в простых случаях вроде этого такого подхода достаточно.
Эксперты по Python, похоже, сходятся во мнении, что именно так предпочтительнее решать эту задачу на Python, записывая её либо так: def foo(n): class acc: def __init__(self, s): self.s = s def inc(self, i): self.s += i return self.s return acc(n).inc, либо так: class foo: def __init__(self, n): self.n = n def __call__(self, i): self.n += i return self.n. Я привожу их здесь, потому что не хочу, чтобы сторонники Python заявили, будто я искажаю возможности языка, однако оба варианта кажутся мне сложнее первой версии. Вы делаете то же самое — создаёте отдельное место для хранения аккумулятора; просто это поле в объекте, а не начало списка. Да и использование этих специальных зарезервированных имён полей, особенно __call__, выглядит некоторым костылём.
В соперничестве между Perl и Python хакеры на Python обычно утверждают, что Python — более элегантная альтернатива Perl, но данный случай показывает, что истинная элегантность заключается в выразительной мощи: программа на Perl проще (содержит меньше элементов), пусть даже её синтаксис немного уродливее.
А что насчёт других языков? В остальных языках, упомянутых в этом докладе — Fortran, C, C++, Java и Visual Basic, — неясно, можно ли вообще решить эту задачу. Кен Андерсон говорит, что следующий код — это примерно максимум того, к чему можно приблизиться на Java: public interface Inttoint { public int call(int i); } public static Inttoint foo(final int n) { return new Inttoint() { int s = n; public int call(int i) { s = s + i; return s; }}; } Это не соответствует условию задачи, поскольку работает только для целых чисел. После многочисленных обсуждений по переписке с хакерами на Java я бы сказал, что написание полноценной полиморфной версии, которая вела бы себя так же, как в предыдущих примерах, балансирует между чертовски неудобным и невозможным. Если кто-то захочет такую написать, мне было бы очень любопытно взглянуть, но моё личное время на попытки истекло.
Конечно, нельзя буквально утверждать, что эту задачу невозможно решить на других языках. Тот факт, что все эти языки полны по Тьюрингу, означает, что, строго говоря, на любом из них можно написать любую программу. Но как бы вы это сделали? В крайнем случае — написав интерпретатор Lisp на менее мощном языке.
Звучит как шутка, однако в крупных проектах по программированию это в той или иной степени происходит настолько часто, что для этого явления даже есть название — десятое правило Гринспена:
Любая достаточно сложная программа на C или Fortran содержит написанную заново, неспецифицированную, кишащую ошибками и медленно работающую реализацию половины Common Lisp.
Если вы пытаетесь решить трудную задачу, вопрос не в том, будете ли вы использовать достаточно мощный язык, а в том, будете ли вы: (а) использовать мощный язык, (б) писать фактически интерпретатор для него, или (в) сами станете компилятором-человеком для него. Мы уже видим начало этого процесса в примере на Python, где мы фактически симулируем код, который компилятор генерировал бы для реализации лексической переменной.
Эта практика не просто распространена, она институционализирована. Например, в мире ООП часто слышно о «паттернах». Интересно, не являются ли эти паттерны порой свидетельством работы сценария (в) — когда работает человек-компилятор? Когда я вижу паттерны в своих программах, я считаю это признаком неприятностей. Форма программы должна отражать только ту задачу, которую ей нужно решить. Любая иная регулярность в коде для меня, по крайней мере, служит знаком того, что я использую недостаточно мощные абстракции — часто это означает, что я вручную разворачиваю макрос, который мне просто нужно написать.
Примечания
Процессор IBM 704 был размером примерно с холодильник, но намного тяжелее. Процессор весил 3150 фунтов, а 4 КБ оперативной памяти находились в отдельном блоке весом ещё 4000 фунтов. Sub-Zero 690, один из крупнейших бытовых холодильников, весит 656 фунтов.
Стив Рассел также написал первую (цифровую) компьютерную игру, Spacewar, в 1962 году.
Если вы хотите перехитрить волосаторогого босса и заставить его разрешить вам писать софт на Lisp, можете попробовать сказать ему, что это XML.
Вот генератор аккумуляторов на других диалектах Lisp: Scheme: (define (foo n) (lambda (i) (set! n (+ n i)) n)) Goo: (df foo (n) (op incf n _))) Arc: (def foo (n) [++ n _]) Грустная история Эранна Гата о «лучших отраслевых практиках» в JPL вдохновила меня обратиться к этой фразе, которую обычно употребляют не к месту.
Питер Норвиг обнаружил, что 16 из 23 паттернов в Design Patterns оказались «невидимыми или более простыми» в Lisp.
Спасибо многим людям, которые отвечали на мои вопросы о различных языках и/или читали черновики этой статьи, включая Кена Андерсона, Тревора Блэквелла, Эранна Гата, Дэна Гиффина, Сару Харлин, Джереми Хилтона, Роберта Морриса, Питера Норвига, Гая Стила и Антона ван Страатена. Они не несут ответственности за высказанные здесь мнения.
Связанные материалы:
Многие люди откликнулись на этот доклад, поэтому я создал дополнительную страницу, чтобы разобрать затронутые ими вопросы: Re: Месть ботаников.
Он также вызвал обширную и зачастую полезную дискуссию в списке рассылки LL1. Обратите особое внимание на письмо Антона ван Страатена о семантическом сжатии.
Некоторые письма из рассылки LL1 побудили меня попытаться глубже погрузиться в тему выразительной силы языков в статье Лаконичность — это сила.
Более широкий набор канонических реализаций бенчмарка генератора аккумуляторов собран на отдельной странице.
Перевод на японский, Перевод на испанский, Перевод на китайский