оригинал: Joel on Software, 1.8.2006
Однажды, когда вы просматриваете свой код, вы обнаруживаете в нём два больших блока, содержимое которых практически не отличается. Они полностью совпадают – за исключением того, что в одном из блоков используются “Спагетти”, а в другом – “Шоколадный Мусс”.
- Код: Выделить всё
// Самый простой пример:
alert("Мне Спагетти!");
alert("Мне Шоколадный Мусс!");
(Здесь для примеров кода используется JavaScript, но даже если вы не знаете этот язык, это вам не помешает понять суть.)
Естественно, что этот повторяющийся код лучше вынести в отдельную функцию:
- Код: Выделить всё
function SwedishChef( food )
{
alert("Мне " + food + "!");
}
SwedishChef("Спагетти");
SwedishChef("Шоколадный Мусс");
Это, конечно, очень простой пример, но в более реалистичных примерах применимы все те же рассуждения. Исправленный код лучше исходного в силу многих причин, каждую из которых вы уже слышали миллион раз. Удобство чтения, удобство поддержки, инкапсуляция, – одним словом, красота!
Теперь вы замечаете два других похожих блока кода: разница между ними лишь в том, что один вызывает функцию BoomBoom, а другой – функцию PutInPot. В остальном же эти блоки совпадают.
- Код: Выделить всё
alert("взять омара");
PutInPot("омара");
PutInPot("воду");
alert("взять курицу");
BoomBoom("курицу");
BoomBoom("кокос");
Теперь нужно каким-то образом передать в функцию аргумент, который сам является функцией. Это очень важная возможность: она расширяет класс повторяющихся блоков кода, которые можно вынести в отдельную функцию.
- Код: Выделить всё
function Cook( i1, i2, f )
{
alert("взять " + i1);
f(i1);
f(i2);
}
Cook( "омара", "воду", PutInPot );
Cook( "курицу", "кокос", BoomBoom );
Смотрите-ка! Одним из аргументов мы передаём функцию.
А в вашем языке так можно?
Так… Теперь предположим, что функции PutInPot и BoomBoom у нас ещё не определены. Не правда ли, было бы весьма удобно просто записать их код в качестве аргумента – вместо того, чтобы объявлять их где-нибудь в другом месте?
- Код: Выделить всё
Cook( "омара",
"воду",
function(x) { alert("положить " + x); } );
Cook( "курицу",
"кокос",
function(x) { alert("стукнуть " + 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 становятся общедоступными, и как только ими начинает пользоваться большая группа людей, достаточно лишь одного супер-гения, который приспособит их для выполнения на глобальной сети параллельных вычислителей, – и сразу же весь старый код, использующий эти функции, будет выполняться в миллионы раз быстрее, чем с обычным циклом по массиву; теперь в одно мгновение можно будет решать задачи, требовавшие раньше месяца вычислений.
Повторюсь ещё раз. Когда инкапсулируется сама суть перебора массива, можно реализовать этот перебор совершенно любым способом – например таким, который будет отлично масштабироваться в параллельных вычислительных системах.
Теперь-то вы понимаете, что я имел в виду, когда я сетовал на преподавание студентам КН одной лишь Java:
Без представления о функциональном программировании невозможно изобрести MapReduce – алгоритм, обеспечивающий такую ошеломляющую масштабируемость Google. Сами термины Map и Reduce происходят из языка Lisp и традиций ФП. Сам же алгоритм MapReduce очевиден любому, кто помнит из курса по программированию, что чисто функциональные программы не имеют побочных эффектов и потому тривиально масштабируются в параллельных системах. То, что в Google догадались до MapReduce, а в Microsoft – нет, уже объясняет, почему в Microsoft всё никак не доделают нормальную поисковую систему, а в Google уже перешли к следующей задаче – создание Скайнета самого крупного параллельного суперкомпьютера в мире. Я не уверен, что в Microsoft по-настоящему понимают, насколько они отстали.
Теперь, надеюсь, я вас убедил, что языки программирования, поддерживающие функции как самостоятельные сущности, предоставляют большие возможности для инкапсуляции, – что, в свою очередь, делает ваш код более простым, компактным и масштабируемым. Многие программы Google используют алгоритм MapReduce, и они все оказываются в выигрыше, когда кто-то оптимизирует его код или исправляет в нём баги.
Лично я считаю, что самые продуктивные языки программирования – те, которые позволяют работать с разными уровнями абстракции. В GW-BASIC вообще нельзя было создавать функции. (прим. перев.: а как же DEF FN?) В Си есть указатели на функции, но они жуууткие, не анонимные, и обязательно должны быть объявлены где-нибудь совсем не в том месте, где они используются. В Java приходится использовать функторы, что ещё жутче: как рассказывает Стив Йегг, Java – Царство Имён.