Май 2002
«Мы гонялись за С++ программистами. Нам удалось перетащить их целую кучу на полпути к Lisp.»
- Гай Стил, соавтор Java спецификации.
В бизнесе программного обеспечения идет вечная борьба между вооруженными до зубов знаниями учеными, и другой, не менее грозной силой, начальниками, в арсенале которых одно сплошное невежество (* в оригинале pointy-haired boss – персонаж серии комиксов «Дилберт» Скота Адамса, отличается необразованностью и полнейшим отсутствием базовых знаний области, которой он управляет). Все ведь знают, что это за зверь такой? Полагаю, большинство людей в мире технологий не только распознают этого карикатурного персонажа, но и знакомы с реальным человеком из своей фирмы, с которого этот образ срисован.
Невежественный начальник чудесным образом сочетает в себе два качества, которые сами по себе довольно распространены, но редко, когда объединяются в одном лице: (а) он ровным счетом ничего не знает о технологиях, и (б) у него всегда есть самые твердые убеждения по любому касающемуся их вопросу.
Предположим, например, Вам надо написать некоторую программу. Ваш начальник-невежда не имеет ни малейшего понятия о том, как она должна работать, и не может отличить один язык программирования от другого, и все же, он знает, на каком языке Вам следует писать эту программу. А именно, он считает, что Вам следует писать ее на Java.
Почему он так думает? Давайте посмотрим, что творится в это время в голове у начальника-профана. То, что он думает, выглядит вот так. Java это стандарт. Я знаю, так должно быть, потому что я постоянно читаю об этом в СМИ. Поскольку это стандарт, у меня не будет проблем из-за его использования, что также означает, что всегда будет куча Java программистов. Поэтому, если программисты, работающие сейчас на меня, уволятся, так как все программисты, работающие у меня, так всегда и делают по какой-то таинственной причине, я могу с легкостью их заменить.
Ну, это не лишено смысла. Но это все основано на одном негласном предположении, которое на самом деле ложно. Такой начальник считает, что все языки программирования довольно сильно схожи. Если бы это было так, он бы попал в самую точку. Если бы все языки были бы взаимозаменяемы, то конечно, можно было бы использовать любой язык, которым пользуются другие.
Но все языки разные, и я полагаю, что могу доказать это Вам без излишних подробностей об их различиях. Если бы Вы спросили начальника в 1992 году на каком языке программирования следует писать программу, он бы без сомнения ответил так, как и сегодня. Программное обеспечение следует писать на С++. Но если все языки равносильны, почему тогда мнение начальства должно вообще измениться? То есть почему Java разработчикам вообще следовало беспокоиться о создании нового языка?
По всей видимости, если Вы создаете новый язык, то только потому, что думаете, что он, в некотором роде, лучше, чем то, что уже есть у людей. И действительно, Гослинг дает понять в первой официальной документации по Java, что тот (язык программирования Java), в свою очередь, был создан, чтобы решить некоторые проблемы С++. И вот, пожалуйста. Не все языки равносильны. Если Вы проследите за ходом мыслей в голове нашего начальника до Java и обратно, через историю Java к его истокам, у Вас возникнет догадка, идущая в разрез с предположением, с которого Вы начали.
Так кто же прав? Джеймс Гослинг, или наш невежественный начальник? Неудивительно, что прав Гослинг. Некоторые языки лучше других для конкретных задач. И, знаете ли, это поднимает ряд интересных вопросов. Java был разработан, чтобы быть лучшим для некоторого круга задач, по сравнению с С++. Каких задач? Когда лучше использовать Java, а когда С++? Существуют ли ситуации, когда другие языки лучше, чем эти.
Как только Вы начнете раздумывать над этим вопросом, Вы попадете в запутанную ситуацию. Если бы начальнику пришлось думать о задачах комплексно, у него бы мозги взорвались. Как только он определит все языки программирования как равнозначные, все, что ему нужно сделать, это выбрать тот, у которого окажутся наиболее мощные темпы развития. И, т.к. это больше вопрос моды, чем технологий, даже он, вероятно, сможет получить верный ответ. Но если языки разные, ему, вдруг, приходится решать две системы уравнений, пытаясь найти оптимальный баланс между двумя вещами, о которых ему ничего неизвестно: относительная приемлемость 20, или около того, ведущих языков для задачи, которую ему нужно решить, и сложность поиска программистов, библиотек, и т.д. для каждого из них. Если это то, что кроется за дверью, неудивительно, что невежественный начальник не захочет ее открывать.
Недостаток убеждения в том, что все языки программирования эквивалентны, состоит в том, что это на самом деле не так. Но преимущество заключается в том, что это значительно упрощает Вашу жизнь. И я думаю, это основная причина, по которой данное убеждение так широко распространено. Так удобно.
Нам известно, что Java должен быть довольно хорош, потому что это новый, современный язык программирования. Так ли это? Если взглянуть на мир языков программирования свысока, то будет казаться, что Java это самая последняя новинка. (Если смотреть с достаточно большой дистанции, все, что можно будет увидеть, это большой сияющий рекламный щит, оплаченный корпорацией Sun.) Но если взглянуть на этот мир с довольно близкого расстояния, обнаружится, что существует некоторая степень этой крутости. В субкультуре хакеров есть другой язык, под названием Perl, который считается намного круче Java. Сайт Slashdot, например, сгенерирован на Perl. Я не думаю, что Вы бы увидели, как эти парни используют Java Server Pages. Но существует еще один, более новый язык, который называется Python, чьи пользователи стараются смотреть на Perl свысока, но и это еще не все.
Если рассмотреть эти языки в таком порядке Java, Perl, Python, то можно заметить интересную схему. По крайней мере, эта схема заметна, если Вы используете Lisp. Каждый из них все больше и больше похож на Lisp. Python копирует даже такие особенности, которые многие Lisp программисты принимают за ошибки. Можно было бы построчно перевести простые Lisp программы на Python. На дворе 2002 год, и языки программирования почти сравнялись тем, что было разработано в 1958 году.
Идя в ногу с математикой
Что я хочу сказать, так это то, что Lips был впервые открыт Джоном Маккарти в 1958 году, а популярные языки программирования только сейчас подхватывают идеи, которые он в то время развивал.
Итак, как же такое может быть? Разве компьютерные технологии не меняются очень быстро? Я только хочу сказать, что в 1958 году компьютеры были гигантами величиной с холодильник с мощностью процессоров как у наручных часов. Как могла такая старая технология оставаться актуальной, не говоря уже о том, чтобы превзойти последние разработки?
Я расскажу Вам как. Все потому, что Lisp, в действительности, не разрабатывался как язык программирования, по крайней мере, не в том смысле, что мы понимаем сегодня. То, что мы подразумеваем под языком программирования, является тем, чем мы пользуемся для указания компьютеру, что делать. Маккарти, в конце концов, планировал разрабатывать язык программирования в этом смысле, но Lisp, к которому мы пришли, был основан на некоторой отдельной его работе, чисто теоретического характера, — попытке определить более удобную альтернативу машине Тьюринга. Как Маккарти позже говорил:
А потом произошло вот что. Спустя некоторое время в конце 1958 года, Стив Рассел, один из аспирантов Маккарти, взглянул на определение eval и осознал, что если бы он перевел это в машинный язык, то результатом был бы интерпретатор для Lisp.
Это было большой неожиданностью в те времена. И вот что позже в интервью Маккарти сказал по этому поводу:
Внезапно, полагаю, за какие-то несколько недель, Маккарти обнаружил, что это чисто теоретическое мероприятие преобразовалось в настоящий язык программирования, и даже более мощный, чем он предполагал.
Таким образом, кратким объяснением того, почему этот язык 1950-х годов не относят к устаревшим, является тот факт, что его основой была не технология, а математика. Ведь математика не обладает сроком годности. Будет правильнее сравнивать Lisp не с аппаратурой 1950 года, а, скажем, с алгоритмом быстрой сортировки, который был открыт в 1960 году и все еще является самым быстрым алгоритмом сортировки общего назначения.
Существует другой язык, выживший со времен 1950-х годов, Fortran, и он представляет противоположный подход к разработке языков. Lisp был теорией, которая неожиданно превратилась в язык программирования. Fortran изначально разрабатывался как язык программирования, но, как бы мы сейчас оценили, в качестве низкоуровневого.
Fortran I, разработанный в 1956 году, был совсем другого поля ягода, в отличие от нынешнего языка Fortran. Fortran I в большей степени был языком ассемблера с математикой. В некотором роде он был слабее более новых языков ассемблера. Например, в нем отсутствовали подпрограммы, только операции перехода. Современный Fortran сейчас, возможно, ближе к Lisp, чем к Fortran I.
Lisp и Fortran были ветвями двух различных древ эволюции. Один брал свое начало из математики, а второй – из архитектуры машин. Эти два древа с тех пор переплетаются. Lisp мощно выстрелил и на протяжении последующих 20 лет ускорился. Так называемые господствующие (mainstream) языки быстро стартовали, но на данный момент наиболее продвинутые из них едва ли могут поравняться с Lisp. Они недалеко от него ушли, но им все еще не хватает пары вещей.
Чем отличается Lisp
Когда Lisp был впервые разработан, он воплощал в себе 9 новых принципов. Сегодня некоторые из них мы воспринимаем как само собой разумеющееся, другие можно увидеть только в более продвинутых языках, а два все еще остаются прерогативой Lisp. Эти 9 принципов перечислены ниже в порядке их применения в основном IT-течении.
Когда Lisp впервые появился, эти принципы были далеко за пределами обычной практики программирования, что, в основном, было продиктовано аппаратурой, появившейся в конце 1950-ых годов. Со временем, базовый язык, воплощенный в последователях популярных языков, постепенно эволюционировал в Lisp. Принципы 1-5 сейчас широко распространены. Принцип под номером 6 еще только начинает проявляться в мейнстриме. Python находится на 7 стадии, хотя, кажется, для этого принципа отсутствуют какие-либо синтаксические правила.
Что касается пункта 8, то он, возможно, является одним из самых интересных. Принципы 8 и 9 стали частью Lisp случайно, потому что Стив Рассел внедрил то, что Маккарти никогда и не собирался делать. И все же, эти принципы оказались лежащими в основе как странного появления Lisp, так и его наиболее отличительных черт. Lisp выглядит настолько странно не потому что у него своеобразный синтаксис, а потому что у него нет синтаксиса как такового. Вы пишете программы прямо в деревьях грамматического разбора, которые строятся за кулисами во время парсинга в других языках программирования, и эти деревья состоят из списков, которые являются структурами данных в Lisp.
Выражение языка в его собственных структурах данных оказывается довольно мощным свойством. Принципы 8 и 9 в совокупности означают, что можно написать программы, которые пишут программы. Это, возможно, звучит как бред, но в 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 составляют 200 000 строк программы на Common Lisp, которая ищет на порядок больше возможностей, чем их конкуренты, все еще, по-видимому, использующие технологии эпохи программирования мейнфреймов. (Хотя ITA также в некотором смысле использует язык программирования эпохи мейнфреймов). Я ни разу не видел ни строчки кода из программы ITA, но, согласно одному из их лучших специалистов, они используют много макросов, чему я нисколько не удивляюсь.
Центростремительные силы
Я и не утверждаю, что при использовании нестандартных технологий не требуется никаких затрат. Не такими уж беспочвенными оказались опасения нашего начальника-профана. Но без осознания всех рисков он склонен к их преувеличению.
Мне на ум приходят три проблемы, которые могут возникнуть при использовании менее популярных языков программирования. Ваши программы могут некорректно работать с программами, написанными на других языках. В вашем распоряжении может быть гораздо меньше различных библиотек. Вы также столкнетесь с трудностями при найме программистов.
Насколько страшна каждая из перечисленных проблем? Важность первой варьируется в зависимости от того, есть ли у вас контроль над всей системой. Если вы создаете ПО, которое будет запускаться на удаленной пользовательской машине, управляющей кадиллаком, на закрытой операционной системе (не буду приводить названия), то, возможно, есть преимущества в том, чтобы писать свое приложение на том же языке, на котором написана сама ОС. Но, если вы держите под контролем всю систему и владеете исходными кодами всех ее частей, как, вероятно, в случае с ITA, то вы можете использовать любой язык, какой захотите. При возникновении несовместимости, вы можете самостоятельно это исправить.
В серверных приложениях вы можете выйти из положения посредством использования передовых технологий, и я думаю, что это главная причина того, что Джонатан Эриксон называет «возрождением языка программирования». Вот почему мы узнаем о новых языках, таких как Perl и Python. Мы знаем про эти языки не потому, что люди используют их для написания Windows приложений, а потому, что люди используют их на серверах. И, поскольку ПО переходит с десктопов на серверную платформу (будущее, с которым даже Microsoft, кажется, смирился), вынужденная необходимость использования попсовых технологий со временем сойдет на нет.
Что касается библиотек, то их важность также зависит от самого приложения. Для менее требовательных задач наличие библиотек может перевесить подлинную мощь языка. Так где же эта точка безубыточности? Это непросто точно сформулировать, но где бы она ни находилась, в ней будет наблюдаться нехватка всего того, под чем подразумевается приложение. Если фирма решилась выйти на рынок ПО, и в ней люди занимаются созданием приложения, которое будет позиционироваться как один из ее продуктов, тогда эта компания, вероятнее всего, вовлечет в работу нескольких опытных специалистов, и ей потребуется минимум 6 месяцев, чтобы создать его. В проектах таких масштабов мощные языки программирования начинают перевешивать удобство наличия библиотек.
Третья причина беспокойства начальства, трудность в найме программистов, является ложным опасением. В конце концов, сколько специалистов вам нужно нанять? Конечно, сейчас нам всем известно, что лучше всего ПО разрабатывается командами до десяти человек. И у вас не должно возникнуть проблем при найме сотрудников в таком масштабе для любого языка программирования, включая тот, о котором некоторые едва ли слышали. Если вы не можете найти десять Lisp программистов, тогда ваша фирма, вероятнее всего, расположена не в том городе, чтобы заниматься там разработкой ПО.
На самом деле, выбор более мощного языка программирования немного снизит разрмер необходимой вам команды, потому что (а) если вы используете более мощный язык, то вам, вероятно, не понадобится так много специалистов, а также (б) программисты, работающие с более продвинутыми языками, являются более умными.
Я не утверждаю, что вы не будете вынуждены использовать то, под чем понимают «стандартные» технологии. В Viaweb (ныне Yahoo Store) мы вызвали всеобщее недоумение среди венчурных капиталистов и потенциальных покупателей фирмы самим фактом использования Lisp. Но мы также всех удивили использованием в качестве серверов универсальных коробочных сборок Intel вместо серверов «профессионального уровня» таких фирм как Sun, эксплуатацией, в те времена непонятного, опенсорсного варианта Unix-подобной системы FreeBSD вместо настоящих коммерческих решений наподобие Windows NT, игнорированием предполагаемого стандарта электронной коммерции SET, который сейчас никто даже и не вспомнит, и прочими вещами.
Нельзя позволять управленцам принимать технические решения за вас. Встревожило ли некоторых потенциальных покупателей фирмы то, что мы использовали Lisp? Некоторых да, слегка. Но если бы мы не работали с Lisp, мы бы не смогли создать ПО, которое бы пробудило в них желание нас купить. То, что казалось для них аномальным, было, на самом деле, причиной и следствием.
Если вы начинаете startup, то не проектируйте свой продукт таким образом, чтобы понравиться инвесторам или потенциальным покупателям. Разрабатывайте свой продукт так, чтобы понравиться пользователям. Если вы завоюете пользователей, то все остальное приложится. А если этого сделать не удастся, то никому не будет и дела до того, насколько угодно правоверными были ваши технологические решения.
Цена посредственности
Сколько вы теряете при использовании менее мощного языка программирования? На этот счет существуют некоторые данные.
Самой удобной для измерения мерой является, скорее всего, размер кода. Цель высокоуровневых языков состоит в том, чтобы предоставить вам большее число абстракций, более крупные кирпичики, так сказать, чтобы вы обходились без их огромного количества в ходе построения стены определенных размеров. Поэтому, чем мощнее язык, тем короче программа (не только по количеству символов, конечно, но и по числу отдельных элементов).
Как же более мощный язык программирования позволяет писать более короткие программы? Одним из подходов, которым вы можете воспользоваться, если язык позволит, является восходящее программирование. Вместо того, чтобы просто писать ваше приложение на базовом языке, вы поверх базового языка конструируете язык для создания программ подобно вашей, а затем уже на нем пишете вашу программу. Скомпонованный таким образом код может быть гораздо короче, чем если бы вы писали всю вашу программу на базовом языке. На самом деле именно так и работают большинство алгоритмов сжатия. Такую программу будет легче модифицировать, т.к. в большинстве случаев языковой слой вообще не придется изменять.
Объем кода важен, т.к. время на написание программы зависит в основном от длины кода. Если бы ваша программа была в три раза длиннее на другом языке программирования, то вам бы потребовалось написать в три раза больше исходных текстов. И с этим не справиться с помощью найма большего числа людей, потому что по достижению определенного размера дополнительный найм рабочей силы превратится в чистый убыток. Фред Брукс описал этот феномен в своей известной книге «Мифический человеко-месяц», и все, что я видел, лишь подтверждает его слова.
Так на сколько же короче будут ваши программы, если вы будете писать их на Lisp? Чаще всего при сравнении Lisp с С, например, отмечают примерно семи-десятикратное уменьшение. Но в недавней заметке про ITA в журнале New Architect указано, что «одна строка на Lisp может заменить 20 строк на С», а поскольку эта статья сплошь состоит из цитат президента фирмы ITA, я допускаю, что они это число узнали от самой ITA. Если это так, тогда этому утверждению можно доверять. ITA приложения включают в себя много кода на С и С++, так же как и на Lisp, следовательно, они опираются на свой опыт.
Смею предположить, что эти численные показатели даже не являются постоянными. Думаю, они возрастают, когда вы сталкиваетесь с проблемами посерьезнее, а также когда у вас работают более умные программисты. Из лучших инструментов настроящий специалист может выжать больше.
В крайнем случае, если бы вы конкурировали с ITA и решили бы писать программы на С, они бы смогли разработать ПО в 20 раз быстрее вас. Если бы вы потратили целый год на новую функцию, они бы умудрились продублировать ее менее чем за три недели. Тогда как если бы они провели всего три месяца за разработкой чего-то нового, то прошло бы пять лет, прежде чем вам бы удалось это реализовать.
И знаете что? Это только в лучшем случае. Когда мы обсуждаем размеры кода, вы неявно допускаете, что можно писать программы, как ни странно, на менее эффективных языках программирования. Но в действительности существуют пределы возможностей программистов. Если вы пытаетесь решить сложную задачу с помощью слишком низкоуровневого языка, то вы достигнете той точки, когда возникнет чрезвычайно много вещей, которые нужно одновременно удерживать в голове.
Поэтому, когда я говорю, что вооброжаемому конкуренту ITA потребовалось бы пять лет на дублирование того, что в самой ITA могли бы написать на Lisp за три недели, я имею в виду такие пять лет, когда все идет по плану и без единой ошибки. На самом деле, в большинстве компаний это работает так: любой разрабатываемый проект, требующий пяти лет работы, возможно, вообще никогда не будет закончен.
Признаю, это крайний случай. Специалисты из ITA кажутся необычайно смекалистыми, а С – довольно низкоуровневым языком. Но на конкурирующем рынке, даже разница два или три к одному была бы гарантией того, что вы всегда будете позади.
Самый верный способ
Существует некоторая вероятность, что наш невежественный начальник даже не захочет и время тратить на раздумья обо всем вышеперечисленном. Как большинство из них и делает. А все потому, что, знаете ли, когда до этого дойдет дело, невежественному начальству все равно, отвесит ли люлей за это его фирма или нет, пока невозможно доказать его вину. Самый безопасный лично для него план состоит в том, чтобы держаться поближе к центру стада.
В крупных организациях, чтобы описать такой подход, используют фразу «лучшие отраслевые практики». Ее целью является оградить невежественного начальника от ответственности: если он что-то решил в соответствии с «лучшими отраслевыми практиками», а фирма понесла убытки, то его нельзя в этом винить. Не он принял такое решение, а отрасль.
Я считаю, что данное понятие было изначально придумано для описания бухгалтерского учета и т.п. Грубо говоря, его смысл в том, чтобы не делать ничего странного. А для бухгалтерии, вероятно, это очень даже хорошая идея. Термины «передовой» и «бухгалтерия» не очень хорошо смотрятся вместе. Но когда вы вносите данный критерий в решения относительно используемых технологий, вы начинаете получать неверные ответы.
Технологии часто должны быть передовыми. В языках программирования, как указал Эранн Гет (Erann Gat), то, что на самом деле дают вам «лучшие отраслевые практики» не является наилучшим, а всего лишь относится к среднему уровню. Когда решение вынуждает вас разрабатывать ПО в темпе более агрессивных конкурентов, то «лучшие практики» это ошибочное использование термина.
Итак, перед нами два факта, которые я считаю очень ценными. На самом деле, я знаю это из своего собственного опыта. Первый факт: языки программирования различны по своим возможностям. Второй факт: большинство менеджеров намеренно это игнорируют. Эти сведения в буквальном смысле рассматриваются как самый верный способ зарабатывания денег. Организация 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 сохраняется различие между операторами и выражениями, поэтому для возврата значений нужен явный оператор 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 интерпретатора на менее мощном языке.
Это похоже на шутку, но это так часто происходит в той или иной степени в крупных программных проектах, что данный феномен назвали десятым правилом Гринспена:
Если вы попытаетесь решать сложную задачу, вопрос будет состоять не в том, будете ли вы использовать достаточно мощный язык, а в том, (а) будете ли вы использовать мощный язык, (б) напишете ли реальный интерпретатор для одного такого, или (в) станете ли сами компилятором в человеческом обличии для одного из таких языков. Мы уже видим, как это начинает происходить на примере для Python, где мы имитируем код, который сгенерировал бы сам компилятор для реализации лексической переменной.
Такая практика не только является общей тенденцией, но и превратилась в институционализированную деятельность. Например, в ОО мире вы много слышите о «шаблонах». Интересно, не являются ли эти шаблоны воплощением случая (в), о человеке-компиляторе, в действии. Когда я в своих программах вижу шаблоны, я рассматриваю это как сигнал тревоги. Модель программы должна отражать только ту проблему, которую ей нужно решить. Любая другая закономерность в коде, по крайней мере для меня, является признаком того, что я использую не достаточно мощные абстракции, а часто и то, что я вручную порождаю детализацию некоторого макроса, который мне нужно написать.
Примечания
— Процессор IBM 704 был размером с холодильник, но намного тяжелее. Процессор весил 3150 фунтов (около 1428 кг – прим. пер.), а ОЗУ объемом 4K располагалось в отдельной коробке, которая дополнительно весила еще 4000 фунтов (около 1814 кг – прим. пер.). Один из крупнейших холодильников для домашнего использования Sub-Zero 690 весил 656 фунтов (около 298 кг – прим. пер.).
— Стив Рассел также написал первую (цифровую) компьютерную игру 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 шаблонов в Шаблонах проектирования были «скрыты или проще» в реализации на Lisp.
Дополнительно:
Многие люди откликнулись на эту беседу, поэтому я открыл дополнительную страницу, чтобы иметь возможность рассматривать сложности, с которыми они столкнулись: Re: Revenge of the Nerds.
Эта статья также повлекла за собой всесторонние и часто полезные обсуждения в списке адресатов LL1. См. в частности письмо Антона ванн Штратена по семантическому сжатию.
Некоторые из писем на LL1 побудили меня глубже копнуть в области способностей языков программирования в Succinctness is Power.
Расширенный набор канонических реализаций эталона генератора накопительного параметра представлен на отдельной странице.