Книга: Джоэл и снова о программировании
Назад: Глава двадцатая. Планирование с учетом прежних результатов (EBS)
Дальше: Глава двадцать третья. Как заставить неправильный код выглядеть неправильно

Глава двадцать первая. Шестое письмо о стратегии

18 сентября 2007 года, вторник

IBM только что выпустила офисный пакет с открытым исходным кодом под названием IBM Lotus Symphony. Похоже на очередной дистрибутив StarOffice. Но я подозреваю, что они просто хотят вычеркнуть из памяти настоящий Lotus Symphony, в свое время объявленный чуть ли не вторым пришествием, но оказавшийся совершенно неудачным. Что-то вроде «Джильи» в мире программирования.
В конце 1980-х в Lotus ломали голову над тем, что же делать дальше с их флагманом электронных таблиц и графики Lotus 1-2-3. Было два очевидных пути. Во-первых, можно было добавить функции, например встроить текстовый редактор. Получившийся продукт был назван Symphony. Другим очевидным путем было сделать таблицу трехмерной. Так появился 1-2-3 версии 3.0.
В обоих случаях сразу возникала серьезная проблема старого DOS-ограничения на размер занимаемой памяти — 640 Кбайт. IBM начинала поставки компьютеров с чипами 80286, способными адресовать больше памяти, но в Lotus решили, что у программы, для которой нужен компьютер за 10 000 долларов, количество покупателей будет невелико. Поэтому они все ужимали и ужимали программу. Потратив 18 месяцев на то, чтобы втиснуть 1-2-3 для DOS в 640 Кбайт, они потеряли уйму времени и в конце концов отказались от трехмерности, чтобы таки запихнуть все в память. В случае же Symphony они просто крошили все функции налево и направо.
Ни один из путей не оказался верным. К моменту выпуска 1-2-3 версии 3.0 у всех уже были 80386-е машины с двумя или четырьмя мегабайтами
оперативной памяти. К тому же в Symphony были несовершенные таблицы, несовершенный текстовый редактор, да и вообще много несовершенного.
«Все это мило, старик, — скажете вы. — Но кому сегодня нужны старые программы в текстовом режиме?»
Потерпите еще немного, потому что история повторяется по трем разным направлениям, и самое правильное — ожидать тех же результатов.

Мало памяти и слабые процессоры

Испокон веков и примерно до 1989 года программистов крайне волновала эффективность. Тогда им просто не хватало памяти и процессорных тактов.
В конце 1990-х некоторые компании, включая Microsoft и Apple, обратили внимание (чуть раньше всех остальных), что закон Мура позволяет не очень сильно переживать из-за производительности и использования памяти. Делай что-нибудь крутое и жди, пока подоспеет железо! Microsoft выпустила первую версию Excel для Windows в то время, когда 80386-й был еще слишком дорог, но они были терпеливы. Через пару лет появился 80386SX, и каждый, кому по карману был клон за 1500 долларов, мог работать в Excel.
Благодаря резкому падению цен на память и ежегодному удвоению скорости процессоров у программиста появился выбор. Можно потратить полгода, переписывая внутренние циклы на Ассемблере, а можно те же полгода играть на ударных в рок-группе — в обоих случаях ваша программа будет работать быстрее. У программистов на Ассемблере нет толпы поклонниц.
В общем, нас теперь не сильно беспокоят проблемы производительности и оптимизации.
Кроме одного случая: исполнение JavaScript из Ajax-приложений броузерами. А так как в этом направлении движется практически все программирование, тут есть о чем подумать.
У многих из сегодняшних Ajax-приложений на клиентской стороне наберется больше мегабайта кода. На этот раз нас ограничивают не память и не такты процессора, а ширина канала и время компиляции. В любом случае, нужно сильно постараться, чтобы заставить прилично работать сложные Ajax-приложения.
Впрочем, история имеет свойство повторяться. Каналы становятся дешевле. Люди придумывают, как заранее скомпилировать JavaScript.
Разработчики, вложившие много труда в оптимизацию, заставляя приложения загружаться и работать быстро, в один прекрасный день обнаружат, что все их труды были в значительной мере напрасны или, по крайней мере, «не принесли конкурентного преимущества в долгосрочной перспективе», на языке экономистов.
Разработчики, которые ринулись добавлять в свои приложения крутые возможности, не заботясь о производительности, в конечном счете, предложат более удачные приложения.

Переносимый язык программирования

Язык программирования С был изобретен с явной целью облегчить перенос приложений с одного набора команд на другой. И он неплохо справился с этой задачей, но не был 100%-переносимым, в результате чего мы получили язык Java, более переносимый, чем С. Якобы.
Сейчас крупная проблема переносимости — это клиентский JavaScript, и особенно DOM в веб-броузерах. Писать приложение так, чтобы оно работало во всех броузерах, — сущий кошмар. Единственный выход — последовательно тестировать его на Firefox, IE6, IE7, Safari и Opera. И знаете, что? Мне некогда тестировать под Opera. Фигово быть Opera. У начинающих броузеров нет никаких шансов.
Что же теперь будет? Можете попросить Microsoft и Firefox лучше заботиться о совместимости. Удачи. Можете пойти по пути p-code/Java и соорудить небольшую песочницу поверх базовой системы. Но песочницы -сущее наказание: они медленные и противные, вот почему апплетов Java нет, нет, нет. Создавая песочницу, вы практически обрекаете себя на одну десятую скорости базовой платформы без возможности поддерживать все модные функции, которые есть на одной платформе и отсутствуют на других. (Я все еще жду, когда мне покажут такой апплет Java для телефона, который умеет работать со всеми функциями телефона, то есть с камерой, адресной книгой, SMS-сообщениями и GPS-приемником.)
Песочницы не работали тогда, не работают они и сейчас.
Что будет дальше? Победят те, кто сделает то же, что оказалось успешным в Bell Labs в 1978-м: создадут язык программирования, такой же переносимый и эффективный, как С. Он будет компилироваться в «родной» код (родным в данном случае будет JavaScript и DOM-реализации) с разными кодогенераторами для различных целевых платформ, а производительность будет волновать не вас, а разработчиков компиляторов. Он будет таким же производительным, как обычный JavaScript, и иметь единообразный доступ к DOM, и он будет компилироваться в родной код для IE и родной код для Firefox автоматически и без проблем с переносимостью. Да, он заберется к вам в CSS и провернет все устрашающим, но все же корректным способом, поэтому вам никогда больше не придется думать о несовместимости CSS. Никогда. О, этот счастливый день!..

Интерактивность и стандарты интерфейсов пользователя

Интерфейс пользователя для мэйнфрейма IBM 360 назывался CICS — вы и сейчас можете увидеть его в аэропорту, если перегнетесь через стойку регистрации. Это зеленый экран размером 80x24, разумеется, работающий только в текстовом режиме. Мэйнфрейм отправляет форму «клиенту» (умному терминалу 3270). Умный он потому, что может отобразить форму и позволяет вам ввести в нее данные, не обращаясь при этом к самому мэйнфрейму. Это одна из причин, по которым мэйнфреймы были гораздо мощнее UNIX: процессор не участвовал во вводе вами строки, за это отвечал умный терминал. (Если вы не могли поставить всем умные терминалы, то покупали миникомпьютер System/1, подключаемый между терминалами и мэйнфреймом и управляющий вводом.)
Как бы то ни было, после заполнения формы вы нажимали кнопку ОТПРАВИТЬ, и все ваши ответы отправлялись на сервер для обработки. Потом машина присылала вам новую форму. И так снова и снова.
Ужас. Ну как сделать текстовый редактор для такой среды? (Вообще-то, никак. На мэйнфреймах никогда не было приличного текстового редактора.)
Это был первый этап. Он в точности соответствует эпохе HTML в Интернете. HTML — это CICS со шрифтами.
Второй этап начался, когда у каждого на столе появился персональный компьютер, и программисты внезапно получили возможность распихивать текст по всему экрану, где и когда угодно, и считывать каждое нажатие клавиши пользователем, что позволило делать симпатичные быстрые приложения, которым не требовалось ждать, пока вы нажмете кнопку ОТПРАВИТЬ, чтобы подключить к работе процессор. Например, можно было сделать текстовый редактор, автоматически переносящий слова на новую строку, когда текущая строка была заполнена до конца. Сразу. О, боже. Можете себе это представить?
Проблемой второго этапа было отсутствие четких стандартов для пользовательских интерфейсов. Программисты получили такую свободу, что каждый делал все, что хотел, и потому умение пользоваться программой Х ничуть не облегчало вам работу с программой Y. В WordPerfect и Lotus 1-2-3 были совершенно разные системы меню, клавиатурные интерфейсы и структура команд. А о копировании данных между этими приложениями и речи не было.
То же происходит сегодня с Ajax-приложениями. Нет, конечно, пользоваться ими гораздо удобнее, чем первыми приложениями для DOS, — с тех пор мы все же кое-чему научились. Но Ajax-приложения бывают очень непохожими между собой и с трудом работают друг с другом — например, нельзя вырезать и вставлять объекты из одного Ajax-приложения в другое, поэтому я не знаю, как перетащить картинку из Gmail во Flickr. Эй, ребята, копирование и вставку изобрели четверть века назад!
Третий этап для персональных компьютеров — это Макинтош и Windows. Стандартизированный, единообразный пользовательский интерфейс с такими средствами, как множественные окна и буфер обмена, разработанные с учетом возможности совместной работы приложений. Благодаря новым графическим интерфейсам компьютер стал удобным и эффективным, что привело к взрыву популярности персональных компьютеров.
И если история повторяется, то можно ожидать какой-то стандартизации пользовательских интерфейсов Ajax, как это было в свое время с Microsoft Windows. Кто-нибудь напишет хороший SDK, позволяющий создавать мощные Ajax-приложения со стандартными элементами пользовательского интерфейса, способными работать совместно. И тот, чей SDK сумеет завоевать больше умов разработчиков, получит такое же прочное преимущество в конкурентной борьбе, как когда-то Microsoft с ее Windows API.
Если вы занимаетесь веб-разработками и не хотите поддерживать SDK, с которым работают все остальные, то обнаружите, что пользователям все меньше нравится работать с вашим веб-приложением, из-за того что оно не поддерживает копирование и вставку, синхронизацию адресной книги и еще какие-нибудь замысловатые функции обмена, которые в 2010 году окажутся нужными всем.
Представьте, например, что вы Google с его Gmail, и все у вас хорошо. Но вдруг появляется никому не известный проказник-стартап из Y Combi-nator и вызывает сумасшедший интерес, продавая NewSDK, сочетающий отличный переносимый язык программирования, компилирующийся в JavaScript, и, что еще лучше, огромную Ajax-библиотеку со всевозможными средствами организации взаимодействия между программами — не простыми копированием и вставкой, а мощными сложными функциями вроде синхронизации и единого управления личными данными (благодаря чему не придется сообщать обо всех своих делах Facebook и Twitter, можно все вводить в одном месте). Можете только посмеяться над ними, потому что их NewSDK занимает целых 232 мегабайта... 232 мегабайта JavaScript, и страница загружается за 76 секунд. Поэтому вашему приложению Gmail не грозит потеря пользователей.
Но пока вы сидите в своем googleкресле, попивая googleчино и чуть не лопаясь от самодовольства, выходят новые версии броузеров, которые поддерживают кэшированный скомпилированный JavaScript. И внезапно NewSDK становится очень быстрым. А Пол Грэм выдает им 6000 коробок быстрого супа, чтобы они еще три года не думали о еде, совершенствуя свой продукт.
А ваши программисты разводят руками: Gmail слишком велик, мы не можем перенести Gmail на этот дурацкий NewSDK. Нам придется переписать каждую строчку кода. Черт, да это же все надо писать заново, вся программная модель перевернута с ног на голову, она рекурсивная, и там столько скобок, сколько Google не купить. Последняя строка почти каждой функции состоит из 3296 закрывающих скобок. Чтобы их посчитать, придется купить специальный редактор.
А ребята, пишущие на NewSDK, выдают приличный текстовый редактор, приличный клиент электронной почты и убийственный публикатор событий для Facebook и Twitter, который синхронизируется с чем угодно, и люди начинают ими пользоваться.
И пока вы спали, все стали писать приложения на NewSDK, и они замечательные, и вдруг компаниям становятся нужны ТОЛЬКО приложения на NewSDK, а все эти классические чистые Ajax-приложения выглядят жалко, потому что они не умеют копировать и вставлять, синхронизировать и как следует работать друг с другом. И Gmail становится «историческим» приложением электронной почты, как WordPerfect в мире редакторов. Вы рассказываете детям, в каком были восторге от двухгигабайтного почтового ящика, а они смеются над вами. В их пилке для ногтей больше двух гигабайт.
Безумная история? Подставьте вместо «Google Gmail» «Lotus 1-2-3». NewSDK будет вторым пришествием Microsoft Windows, именно так Lotus потерял власть над рынком электронных таблиц. И это случится снова, на сей раз в Сети, потому что законы природы остаются прежними. Каких-то деталей мы можем не знать, но так будет.

А ваш язык программирования такое умеет?

1 августа 2006 года, вторник

Однажды, просматривая свой код, вы замечаете два больших, почти одинаковых с виду блока. Они действительно совершенно одинаковы, за исключением того, что в одном упоминается Spaghetti, а в другом— Chocolate Mousse.

 

// Элементарный пример:
alert("I'd like some Spaghetti!");
alert("I'd like some Chocolate Mousse!");

 

(Все примеры написаны на JavaScript, но даже не зная JavaScript, можно понять суть.)
Дублировать код, как вы знаете, нехорошо, поэтому вы создаете функцию:

 

function SwedishChef( food )
{
alert("I'd like some " + food + "!");
}
SwedishChef("Spaghetti");
SwedishChef("Chocolate Mousse");

 

Ладно, это простейший пример, но вы можете представить, что он гораздо сложнее. Этот код лучше по многим причинам, о которых вы слышали миллион раз. Сопровождаемость, Читабельность, Абстракция = Хорошо!
Теперь вы замечаете еще два блока кода, которые выглядят почти одинаково, за исключением того, что один несколько раз вызывает функцию BoomBoom, а другой — функцию PutInPot. В остальном код почти одинаковый.

 

alert("get the lobster");
PutInPot("lobster");
PutInPot("water");
alert("get the chicken");
BoomBoom("chicken");
BoomBoom("coconut");

 

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

 

function Cook( i1, i2, f )
{
alert("get the " + i1);
f(i1);
f(i2);
}
Cook("lobster", "water", PutInPot );
Cook("chicken", "coconut", BoomBoom );

 

Ура! Мы передали функцию как аргумент.
А ваш язык программирования такое умеет?
Подождите... предположим, вы еще не определили функции PutInPot и BoomBoom. Не будет ли изящнее написать их прямо в вызове, вместо того чтобы объявлять где-то в другом месте?

 

Cook( "lobster",
"water",
function(x) { alert("pot" + x); } );
Cook( "chicken",
"coconut",
function(x) { alert("boom " + x); } );

 

Черт возьми, это удобно. Заметьте, я создаю здесь функцию на лету, даже не беспокоясь о том, чтобы дать ей имя, — просто подымаю ее за уши и опускаю в функцию.
Как только вы привыкнете к идее безымянных функций как аргументов, вы начнете повсюду замечать код, который, скажем, делает что-то с каждым элементом массива.

 

var a = [1,2,3];
for (i=0; i<a.length; i++)
{
a[i] = a[i] * 2;
}
for (i=0; i<a.length; i++)
{
alert(a[i]);
}

 

Очень часто возникает необходимость что-то сделать с каждым элементом массива, поэтому можно написать функцию, которая выполнит эту работу вместо вас:

 

function map(fn, a)
{
for (i = 0; i < a.length; i++)
{
a[i] = fn(a[i]);
}
}

 

Теперь можно переписать предыдущий код:

 

map( function(x){return x*2;}, a );
map( alert, a );

 

Другая распространенная операция с массивом — какое-нибудь объединяющее действие со всеми его элементами.

 

function sum(a)
{
var s = 0;
for (i = 0; i < a.length; i++) s += a[i]; return s;
}
function join(a) {
var s =
for (i = 0; i < a.length; i++) s += a [i ];
return s;
}
alert(sum([1,2,3]));
alert(join(["a","b","c"]));

 

Функции sum и join выглядят так похоже, что возникает желание абстрагировать их сущность в общую функцию, объединяющую элементы массива в одно значение:

 

function reduce(fn, a, init)
{
var s = init;
for (i = 0; i < a.length; i++) s = fn( s, a[i] );
return s;
}
function sum(a)
{
return reduce( function(a, b){ return a + b; }, a, 0 );
}
function join(a)
{
return reduce( function(a, b){ return a + b; },
a "" );
}

 

Многие старые языки программирования просто не позволяют проделывать такие трюки. Другие языки позволят вам сделать это, но с трудом (например, в С есть указатели на функции, но вы должны объявить и определить функцию где-либо в другом месте). Объектно-ориентированные языки программирования еще не вполне уверены, что стоит разрешить вам делать что-нибудь с функциями.
Java требует от вас создать отдельный объект с одним методом, так называемый функтор, если вы хотите представить функцию как объект первого рода. Добавьте к этому тот факт, что многие ОО-языки требуют создавать отдельный файл для каждого класса, от чего код быстро делается неуклюжим. Если ваш язык программирования требует применения функторов, у вас не будет всех преимуществ современной среды программирования. Узнайте, не возвращают ли они деньги.
Много ли вы выиграете от возможности писать крошечные функции, которые всего лишь пробегают по массиву, проделывая что-нибудь с каждым элементом?
Что ж, вернемся к функции map. Когда вам нужно поочередно проделать что-нибудь с каждым элементом, очень часто порядок не имеет значения. Вы же можете пройти массив в прямом или обратном направлении, и результат будет одинаковым, верно? В действительности, если в вашем распоряжении есть два процессора, можно написать какой-нибудь код для того, чтобы каждый процессор обрабатывал половину элементов, и тогда map вдруг станет в два раза быстрее.
А может, чисто гипотетически, у вас сотни тысяч серверов в нескольких центрах обработки данных, разбросанных по всему миру, и действительно большой массив, содержащий, скажем, опять чисто гипотетически, все содержимое Интернета. Теперь вы можете запустить map на тысячах компьютеров, каждый из которых будет решать маленькую часть задачи.
Таким образом, сегодня написание, к примеру, какого-то по-настоящему быстрого кода для поиска во всем содержимом Интернета просто сводится к вызову функции map с простой функцией поиска в строке в качестве аргумента.
Самое интересное, на что я хочу обратить здесь ваше внимание, это что если представить себе, что map и reduce — функции, которыми могут пользоваться и пользуются все, то вам нужен только супергений, который напишет сложный код для запуска map и reduce на глобальном параллельном массиве компьютеров, и весь старый код, который отлично работал, когда вы запускали небольшие циклы, по-прежнему будет работать, только во много раз быстрее, а значит, позволит мгновенно решать действительно большие задачи.
Позвольте повториться. Абстрагируя концепцию циклов, вы можете обрабатывать их любым желаемым образом, включая такую реализацию, которая хорошо масштабируется при появлении дополнительного «железа».
Теперь вам понятно мое недавнее недовольство по поводу того, что студентов не учат ничему, кроме Java:
Тот, кто не понимает функционального программирования, не смог бы изобрести MapReduce— алгоритм, благодаря которому Google работает с гигантскими объемами данных, размещенных на многочисленных серверах. Термины «Map» и «Reduce» пришли из Lisp и функционального программирования. Понять, как работает MapReduce, может всякий, кто помнит из своего курса 6.001 или аналогичного ему, что у чисто функциональных программ нет побочных эффектов, поэтому их просто распараллеливать. Тот факт, что в Google смогли придумать MapReduce, а в Microsoft — нет, отчасти объясняет, почему Microsoft по-прежнему отстает в своих попытках наладить базовые функции поиска, тогда как Google ужерешает новую задачу — построить Skynet^HHHHHH, крупнейший в мире суперкомпьютер с массовым параллелизмом. Я не уверен, что в Microsoft в достаточной мере понимают, как сильно они отстали в этом отношении, (глава 8)
OK. Надеюсь, теперь я вас убедил, что языки программирования, где функции — это объекты первого рода, предоставляют больше возможностей для абстрагирования, благодаря чему можно уменьшить объем кода, сделать код более надежным, облегчить его многократное использование и масштабирование. Множество приложений Google используют MapReduce, и все они выигрывают, когда кто-нибудь оптимизирует его или исправляет ошибку.
А сейчас я войду в раж и стану доказывать, что наиболее эффективны среды программирования, позволяющие работать на разных уровнях абстракции. Паршивый старый FORTRAN даже не позволял создавать функции. В языке С есть указатели на функции, но они ооочень уродливы, не могут быть безымянными, и их реализация должна помещаться не там, где они используются. Java заставляет использовать функторы, которые еще уродливее. Как заметил Стив Егге (Steve Yegge), Java — это Королевство существительных (steve-yegge.blogspot.com/2006/03/execution-in-kingdom-of-nouns.html).
Примечание.Я не использовал FORTRAN уже 27 лет. Конечно, в нем есть функции. Должно быть, я имел в виду GW-BASIC.

 

Назад: Глава двадцатая. Планирование с учетом прежних результатов (EBS)
Дальше: Глава двадцать третья. Как заставить неправильный код выглядеть неправильно