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

From The Joel on Software Translation Project

Jump to: navigation, search

Автор: Джоэл Спольски
Вторник, 1 августа, 2006 г.
В оригинале статья называлась Can Your Programming Language Do This?.


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

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

Все примеры здесь на JavaScript, но даже если вы не знаете JavaScript, вы сможете понять суть.

Повторяющийся код, естественно, выглядит уродливо, поэтому вы создаёте функцию:

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

01BorkBorkBork.PNG

OK, это простейший пример, но вы можете придумать более сложный. Этот код лучше по многим причинам, все из которых вы слышали миллион раз. Сопровождаемость, Читабельность, Абстракция = Хорошо!

Теперь вы заметили два других блока кода, которые выглядят почти одинаково, за исключением того, что один вызывает функцию 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 требует от вас создать отдельный объект с одним методом, так называемый функтор, если вы хотите представить функцию как объект первого рода (прим. пер.: first class object, те сущности языка программирования, которые можно использовать без ограничений при хранении в переменных, хранении в более сложных структурах, передаче в функции как аргумент, и т.д.). Добавьте к этому факт, что множество ОО-языков вынуждают вас создавать отдельный файл для каждого класса, и это делает код неуклюжим очень быстро. Если ваш язык программирования требует от вас использования функторов, вы не получите всех преимуществ современной среды программирования. Посмотрите, сможете ли вы получить ваши деньги обратно.

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

Хорошо, давайте вернемся к функции map. Когда вам нужно сделать что-нибудь с каждым элементом массива по очереди на самом деле вам возможно не важно в какой очередности вы это делаете. Вы можете пробежаться по массиву вперед или назад и получить одинаковый результат, верно? В действительности, если у вас в распоряжении есть два процессора, может быть вы напишете какой-нибудь код для того, чтобы каждый процессор обрабатывал половину элементов, и внезапно map становится в два раза быстрее.

Или, возможно, чисто гипотетически, у вас есть сотни тысяч серверов в нескольких центрах обработки данных по всему миру, и у вас есть действительно большой массив, содержащий, скажем, опять чисто гипотетически, всё содержимое Internet'а. Теперь вы можете запустить map на тысячах компьютеров, каждый из которых будет штурмовать маленькую часть проблемы.

Таким образом сейчас, например, написание какого-либо по-настоящему быстрого кода для поиска во всём содержимом Internet'а просто сводится к вызову функции map с простой функцией поиска строки в качестве аргумента.

Действительно интересная вещь, на которую я хочу обратить ваше внимание здесь, что как только вы подумаете о map и reduce как о функциях, которые все могут использовать (и используют их), вам нужен только супергений, чтобы написать тяжелый код, чтобы запустить map и reduce на глобальном параллельном массиве компьютеров, и весь старый код, который отлично работал, когда вы запускали небольшие циклы, по-прежнему работает, только в огромное число раз быстрее, что значит он может использоваться для решения действительно больших проблем за секунду.

Позвольте мне повторить. Абстрагируя концепцию циклов, вы можете осуществить обработку циклов как вам захочется, включая способ, который отлично масштабируется с появлением дополнительного "железа".

Теперь вы поймете кое-что из того, что я когда-то писал, где я выражал недовольство по поводу студентов, которые никогда не учили ничего кроме Java:

Без понимания функционального программирования вы не сможете придумать MapReduce — алгоритма, который делает Google столь хорошо масштабируемым. Термины Map и Reduce пришли из Lisp и функционального программирования. MapReduce понятен любому, кто помнит из своего курса, эквивалентного 6.001, что истинно функциональные программы не имеют побочных эффектов и поэтому легко распараллеливаемы. Очень показателен тот факт, что в Google изобрели MapReduce, а в Microsoft нет, и это говорит кое-что о том, почему Microsoft до сих пор играет в догонялки, пытаясь заставить работать основные функции поисковой машины, в то время как в Google перешли к следующей проблеме: построению Skynet величайшего в мире параллельного суперкомпьютера. Я не думаю, что Microsoft действительно понимает, насколько они отстали на этом пути.

OK. Надеюсь, я вас уже убедил, что языки программирования, где функции - объекты первого рода, предоставляют вам больше возможностей для абстрагирования, что значит ваш код меньше, надежнее, лучше для повторного использования и более масштабируемый. Множество приложений Google используют MapReduce и они все выигрывают, когда кто-нибудь оптимизирует его или исправляет ошибку.

А сейчас я буду немного непоследовательным и буду утверждать, что наиболее эффективны те среды программированию, которые позволяют работать на разных уровнях абстракции. Старый недобрый FORTRAN даже не позволял создавать функции. C содержит указатели на функции, но они уродливы, не могут быть безымянными и их реализация должна быть написана где-то в месте отличном от того, где они используются. Java заставляет использовать функторы, которые ещё уродливее. Как заметил Steve Yegge Java - это Королевство Существительных (прим. пер.: статья на английском) .

Исправление: Я в последний раз использовал FORTRAN 27 лет назад. Конечно, в нём есть функции. Я должно быть имел в виду GW-BASIC.

Исправление исправления: Когда я утверждал, что "Старый недобрый FORTRAN даже не позволял создавать функции.", я действительно имел в виду старый FORTRAN. Сейчас есь новые версии FORTRAN'а, которые лучше, чем последний FORTRAN, который я использовал (а был это WATFOR), который я в последний раз использовал 27 лет назад, так что простите мне моё невежество.

Personal tools