Может ли ваш язык программирования делать такое?

в 15:26, , рубрики: c#.net, functional programming, refactoring, Программирование, метки: , ,

Недавно я прочитал статью Джоэла Спольски “Can your programming language do this?” и она настолько сильно пришлась мне по душе, что я решил перевести ее. Но не просто так, а добавить немного от себя. А именно вместо примеров на JavaScript (использованных Джоэлем в оригинале) я решил написать примеры на С#, который мне на сегодняшний день ближе. Собственно результат и представляю на суд сообществу под катом.

Однажды, просматривая свой код, вы натыкаетесь на два больших блока которые выглядят очень похоже. По-сути, они одинаковые, просто один из блоков относится к “Спагетти”, а второй — к “Шоколадному муссу”.

// где-то в статическом методе Main вашего консольного приложения
Console.WriteLine(“I’d like some Spaghetti!”);
Console.WriteLine(“I’d like some Chocolate Moose!”);

Примеры написаны на языке C#, но даже если вы его не знаете, но знаете какой-нибудь другой объектно-ориентированные язык, вы наверняка проследите идею.

Дублирование кода выглядит не очень хорошо, поэтому вы создаете метод:

public void SwedishChief(string food)
{
    Console.WriteLine(string.Format(“I’d like some {0}!”, food));	
}

SwedishChief(“Spaghetti”);
SwedishChief(“Chocolate Moose”);

Да, пример конечно достаточно таки простой, но вы вполне себе можете представить что-нибудь более реальное. Наш код стал лучше по целому ряду причин, о которых вы уже слышали миллион раз — Поддерживаемость, Читабельность, Абстракция = Хорошо!

Следом вы обнаруживаете два других блока кода, которые выглядят похоже, за исключение того, что один многократно вызывает метод BoomBoom, а другой блок — не менее многократно метод PutInPot. За исключением этого, они одинаковые.

Console.WriteLine(“get the lobster”);
PutInPot(“water”);
PutInPot(“lobster”);

Console.WriteLine(“get the chicken”);
BoomBoom(“chicken”);
BoomBoom(“coconut”);

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

// Этот метод не имеет прямого отношения к статье и создан ради двух целей:
// Инкапсулировать печать в консоль в нечто более компактное, а также
// Чтобы быть чуть ближе к источнику с его функцией alert.
public static T Alert<T>(T message)
{
     Console.WriteLine(message.ToString());
     return message;
}

public void Cook(string ingredientOne, string ingredientTwo, Action<string> function)
{
    Console.WriteLine(string.Format("get the {0}",ingredientOne));
    function(ingredientOne);
    function(ingredientTwo);
}

Cook(“lobster”, “water”, PutInPot);
Cook(“chicken”, “coconut”, BoomBoom);

Смотрите! Мы передаем метод в качестве аргумента.

А ваш язык может такое?

Но погодите… Допустим что вы еще не написали методы PutInPot и BoomBoom. Разве не было бы круто иметь возможность написать их встроенными (inline) вместо того, чтобы определять их где-то.

Cook("lobster",
          "water",
          x => Alert("pot " + x));

Cook("chicken",
          "coconut",
          x => Alert("boom " + x));

Блин, как же это удобно. Обратите внимание, что я создаю здесь метод на лету, и даже не заботясь о его имени, просто тащу зауши внутрь метода Cook()…

Как только вы стали мыслить терминами “Аннонимные функции в качестве аргументов метода”, то вы сразу же начнете замечать повсюду код, который, скажем, делает какую-то операцию над каждым элементом списка.

...
var list = new List<int> { 1, 2, 3 };

for (var i = 0; i < list.Count; i++)
{
     list[i] = list[i] * 2;
}

foreach (var el in list)
{
    Alert(el.ToString());
}
...

Делать что-то над элементом массива достаточно популярная вещь, потому вы сможете написать метод, который будет делать это за вас.

public static void Map<T>(Func<T, T> action, IList<T> list)
{
    for (var i = 0; i < list.Count; i++)
        list[i] = action(list[i]);
}

И затем вы можете переписать вышеуказанный код следующим образом.

...
Map((x) => x * 2, list);
Map(Alert, list);
...

Еще одна достаточно распространенная штука связанная с массивами, это как-нибудь скомбинировать все их элементы.

…
Alert(Sum(list));
Alert(Join(new[] { "a", "b", "c" }));
...

public static int Sum(IEnumerable<int> list)
{
    var sum = 0;
    foreach (var el in list)
        sum += el;

    return sum;
}

public static string Join(IEnumerable<string> list)
{
    var result = string.Empty;
    foreach (var el in list)
        result += el;

    return result;
}

Sum и Join настолько похожи, что вам точно захочется вынести их суть в какой-то один общий метод, который будет комбинировать элементы списка в одно значение.

public static T Reduce<T>(Func<T, T, T> func, IEnumerable<T> list, T init)
{
    var accumulator = init;
    foreach (var el in list)
        accumulator = func(accumulator, el);

    return accumulator;
}

public static int Sum(IEnumerable<int> list)
{
    return Reduce((x, y) => x + y, list, 0);
}

public static string Join(IEnumerable<string> list)
{
    return Reduce((a,b) => a + b, list, string.Empty);
}

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

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

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

Однако же, давайте вернемся к нашей функции map. Когда вам необходимо что-то сделать с каждым элементом списка, правда заключается в том, что зачастую вам не важно в каком порядке вы это будете делать. Иными словами вы можете пробегать список как спереди-назад, так и сзади-наперед и получить один и тот же результат, верно? Даже больше. Получается, что если у вас оказался вдруг двухядерный процессор, вы могли бы написать такой код, который бы отдал по полсписка каждому из ядер на обработку и заставили бы map работать в два раза быстрее!

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

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

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

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

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

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

Но а теперь я чуток смягчусь и выскажу мысль, что наиболее эффективная среда разработки та, что позволяет вам работать на различных уровнях абстракции. Старый кривой FORTRAN вам, по сути, даже функций не даст реализовать. В С есть указатели на функции, но они уродливые, и совсем не аннонимные, а значит должны быть определены где-нибудь в отличном от использования месте. Java позволяет вам сделать функторы, которые еще уродливей. Как отметил Стив Ягги, Java — это Королевство существительных.

З.Ы. Последний раз я использовал FORTRAN лет так 27 назад. Вероятно в нем появились функции. Я наверное думал о GW-BASIC.

Автор: fxdxPZ

* - обязательные к заполнению поля