понедельник, 22 сентября 2014 г.

Декомпозиция в ФП vs декомпозиция в императивном программировании

Любой программист разбивает программу на модули, модули на файлы, файлы на классы, классы на функции (если язык не объектно-ориентированный, то файлы сразу на функции). В среде императивных программистов есть устоявшийся набор правил, по которому эту декомпозицию надо делать. В основном, люди отталкиваются от принципов SOLID, KISS, DRY, Tell Don't Ask, закона Деметры, шаблонов проектирования, правил вроде "функция должна умещаться в экран" и т.п.
Забавно, но нигде из вышеупомянутых правил вообще нет никакого упоминания о side-эффектах. Схематично, программа, написанная в императивном стиле, выглядит примерно вот так:

Голубые прямоугольники ‒ это функции/классы, красные ‒ это строки с side-эффектами, желтые ‒ строки без side-эффектов.
Всё вроде бы хорошо, однако до тех пор, пока вам не приходится эти функции компоновать, т.е. объединять вызовы функций в более высокоуровневые функции, чтобы получилась программа. Часто бывает, что одна и та же функция может быть вызвана более 1 раза в проекте.
Почему трудно компоновать функции с side-эффектами? Потому что объединяя несколько функций в одну, вам нужно держать в голове все side-эффекты, которые каждая из функций содержит, и всё время анализировать: "устраивает ли меня, что серия вызовов вот этих функций кроме того что делает основную работу, ещё имеет следующий набор side-эффектов?"
Очень часто вот этот итоговый набор side-эффектов нежелателен. К примеру, функция f осуществляет запуск ракеты в космос и возвращает массу ракеты. Если вам нужно в проекте в 10 местах узнать массу ракеты, но запуск ракеты вам не нужен, то функция f для вас бесполезна! Вы её не сможете переиспользовать! Вы ведь не хотите, чтобы ракета запустилась тогда, когда пользователь запустил вашу программу, просто чтобы посмотреть характеристики этой ракеты?!
Что нужно сделать с функцией f(), чтобы её можно было использовать не только в случае реальной необходимости запуска ракеты, но и в других местах? Нужно выделить часть, отвечающую за запуск (side-эффект), в отдельную функцию, а часть, отвечающую за расчет массы ракеты, ‒ в другую функцию. Таким образом, вы отделяете чистые функции от функций с side-эффектами:

Вот эта желтая часть должна содержать большую часть вашей бизнес-логики и составляет где-то 70-80 процентов от всего проекта.
Что самое интересное, никто не мешает вам совмещать вот такое разделение с использованием вышеперечисленных привычных вам правил и законов. Хотите применять ООП, наследование и паттерны GoF (если вам так нравится) ‒ применяйте! Только отделяйте side-эффекты от чистых функций! Несколько паттернов, конечно, придется немного подкорректировать. Вот здесь описан, например, чистофункциональный визитор.
Выделение чистых функций, конечно, даётся не бесплатно. Но время, потраченное, на такую правильную декомпозицию, с лихвой окупается, когда дело доходит до компоновки и переиспользования функций! Чистые функции можно переиспользовать как угодно и сколько угодно, не задумываясь! Вы смотрите только на входные аргументы и на результат функции. Кроме того, чистые функции очень легко тестировать (любители TDD оценят).

вторник, 15 октября 2013 г.

Мысли об ФП

ФП - это программирование без побочных эффектов. На этом определение ФП заканчивается. А лямбды, функции как объекты первого класса, композиция и прочие функциональные штуки - это всё второстепенно.

Вот вполне себе функциональный код на Java:

String reverseString(String str) {
    if (str.length() <= 1) {
        return str;
    } else {
        return
        str.charAt(str.length() - 1) +
        reverseString(str.substring(1, str.length() - 1)) +
        str.charAt(0);
    }
}

Побочных эффектов нету, соблюдается referential transparency.
Проблемы начинаются, когда код становится чуть сложнее, чем просто инвертирование строки. Попробуем написать функцию, которая инвертирует строки текста:

String reverseText(String text) {
    String[] lines = text.split("\n");
    String[] invertedLines = reverseLines(lines, 0, new String[0]);
    return Joiner.on("\n").join(invertedLines);
}

String[] reverseLines(String[] lines, int i, String[] invertedLines) {
    if (lines.length - i == 0) {
        return invertedLines;
    } else {
        return reverseLines(lines, i + 1,
            ObjectArrays.concat(invertedLines, reverseString(lines[i])));
    }
}

Справились, но пришлось добавить вспомогательный метод reverseLines. Естественным образом возникает мысль, что неплохо было бы его спрятать внутрь reverseText. И тогда можно не передавать lines в метод reverseLines, потому что она и так будет видна через замыкание. Таким образом, в функциональном языке мы должны уметь определять функции внутри функций.

Теперь надо написать функцию, которая не просто инвертирует строки текста, а, скажем инвертирует и ещё переводит их в верхний регистр. Очевидно, что функция будет отличаться от предыдущей одной лишь заменой reverseString на reverseStringAndToUpperCase. А дублировать код плохо. Тогда возникает мысль, а можно ли как-нибудь абстрагировать функцию reverseString от инвертирования? Назовём её transformTextа функцию reverseString  будем передавать как аргумент. Вот и возникло желание иметь функции как объекты первого класса.

Далее, наверное не сильно хочется писать функцию reverseStringAndToUpperCase? Ведь у нас уже есть reverseString и toUpperCase. Неплохо было бы уметь писать что-нибудь вроде reverseString.andThen(toUpperCase). Т.е. ещё возникает необходимость композиции функций в нашем языке.

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

понедельник, 14 октября 2013 г.

X vs Y

Windows vs Linux vs OS X
Git vs Mercurial vs SVN
Facebook vs Twitter vs Google+
Skype vs Gtalk
Microsoft vs Google vs Oracle
Eclipse vs IDEA vs Netbeans
Github vs Bitbucket vs Google Code
XML vs JSON vs YAML
Vim vs Emacs vs Sublime Text
Android vs iOS
Tabs vs Spaces
C# vs Java
CLR vs JVM
Ruby vs Python vs PHP vs JavaScript
Scala vs Clojure vs Erlang
Static Typing vs Dynamic Typing
Any X vs Any Y

Может, уже пора просто делать свою работу?