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

Чистофункциональный ввод/вывод в Java 8

Любая сколько-нибудь полезная программа обязательно должна как-то взаимодействовать с внешним миром: печатать текст на экран, рисовать фигуры, считывать содержимое файла и т.п. Почему-то в кругу императивных программистов часто бытует мнение, что раз функциональные языки могут использовать только чистые функции, то значит на них нельзя написать программы, взаимодействующие с внешним миром.
Это совершенно не так. Да, функции в функциональных языках действительно не должны содержать side-эффектов, но нигде ведь не сказано, что среда, выполняющая код, не может взаимодействовать с внешним миром!
Давайте используем этот факт, и напишем пример программы, выполняющую чистофункциональный консольный ввод/вывод. В качестве языка будем использовать обычную Java 8, чтобы императивные программисты не думали, что для такого обязательно нужно иметь на руках Haskell.

Итак, основная идея заключается в том, что функция взаимодействия с внешним миром, будет содержать не код, выполняющий набор императивных команд с side-эффектами, а будет возвращать функциональное выражение, описывающее это взаимодействие.
Для этого заведём интерфейс IO:

interface IO<A{
    A run() throws IOException;
}

Интерфейс IO следует понимать так: если у меня есть некоторый объект, реализующий IO, то этот объект  инкапсуляция некоторого взаимодействия с внешним миром (например, чтение строки из консоли). Логика взаимодействия находится в методе run(), и реальные side-эффекты начнут происходить только когда метод run() вызовется. Сам же по себе объект IO это просто ссылка, которую можно передавать в качестве аргумента методов или возвращать из методов, не нарушая чистоты этих методов!
Вот так будет выглядеть реализация методов, которые инкапсулируют вывод в консоль и чтение из неё:

interface IO<A{
    A run() throws IOException;

    static IO<Void> printLine(String str) {
        return () -> { System.out.println(str); return null; };
    }
   
    static IO<String> readLine() {
        return () -> new BufferedReader(new InputStreamReader(System.in)).readLine();
    }
}

Мы добавили два метода, которые создают объекты IO, описывающие вывод и чтение строки. Так как вывод не возвращает никакого осмысленного значения, то IO параметризован классом Void, который является материализованной версией ключевого слова void в Java. Заметьте, в данном коде мы использовали новую фичу, появившуюся в Java 8  возможность писать статические методы в интерфейсах. Это очень классная фича, я считаю.
Теперь, наконец, напишем чистофункциональный Hello, world:

public static void main(String[] args) throws IOException {
    mainPure().run();
}

public static IO<Void> mainPure() {
    return IO.printLine("Hello, world!");
}

mainPure()  это чистая функция, которая возвращает выражение, описывающее печатание строки Hello, world! в консоль. А main()  это среда выполнения, которая уже непосредственно исполняет это выражение и осуществляет side-эффекты.
Хорошо. Но пока, похоже, мы сильно ограничены в экспрессивности нашего языка. Что должна вернуть функция mainPure(), если мы захотим, например, написать echo-программу, читающая строку, а потом просто печатающая её обратно в консоль?
Не вопрос! Только сначала нужно реализовать один очень важный комбинатор под названием flatMap():

interface IO<A{

    A run() throws IOException;

    default IO<BflatMap(Function<A, IO<B>> f) {
        return () -> f.apply(run()).run();
    }
}

Смысл flatMap() в том, что он возвращает объект IO, являющийся результатом выполнения первого IO, а потом над результатом выполняется другой IO. Заметьте, что в этом коде мы тоже использовали ещё одну новую классную фичу Java 8  default-методы.
Теперь echo-программа будет выглядеть так:

public static IO<Void> mainPure() {
    return IO.readLine().flatMap(IO::printLine);
}

Как коротко, правда? Почти не уступает аналогичному императивному коду.
В IO можно добавлять сколько угодно фабричных методов и комбинаторов. Добавим очень важный метод immediate(), который просто немедленно возвращает значение без всяких side-эффектов, и комбинатор map():

interface IO<A{
    A run() throws IOException;

    default IO<Bmap(Function<A, Bf) {
        return () -> f.apply(run());
    }

    default IO<BflatMap(Function<A, IO<B>> f) {
        return () -> f.apply(run()).run();
    }

    static IO<Aimmediate(A a) {
        return () -> a;
    }
}

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

public static IO<Void> mainPure() {
    return IO.readLine().map(Double::valueOf).flatMap(num1 ->
        IO.readLine().map(Double::valueOf).flatMap(num2 ->
            IO.printLine("Sum of " + num1 + " " + num2 + " = " + (num1 + num2))
    ));
}

Расширим IO ещё дальше и добавим поддержку исключений! Добавим метод except(), который выполняет обработчик в случае исключения (аналог императивного catch), и метод onException(), который гарантированного выполняет блок кода, даже если вылетело исключение (аналог императивного finally):

interface IO<A{
    ...

    default IO<Aexcept(Function<Throwable, IO<B>> handler) {
        return () -> {
            try {
                return run();
            } catch (Throwable t) {
                return handler.apply(t).run();
            }
        };
    }

    default <B> IO<AonException(IO<B> action) {
        return () -> {
            try {
                return run();
            } catch (Throwable t) {
                action.run();
                throw t;
            }
        };
    }
}

Расширять API интерфейса IO можно до бесконечности. Можно добавить условия, циклы, автоматическое закрывание ресурсов и т.д. Например, добавим цикл until:

default IO<Auntil(Predicate<Ap) {
    return flatMap(a -> p.test(a) ? immediate(a) : until(p));
}

Используя этот цикл, можно написать простейший REPL, который либо считывает число, умножает его на 2 и продолжает дальше, либо если встречает слово quit, то выходит:

enum Command {
    CONTINUE, QUIT
}

public static IO<Command> mainPure() {
    IO<Command> command = IO.readLine().flatMap(str -> {
        if (str.equals("quit")) {
            return IO.immediate(Command.QUIT);
        } else {
            return IO.immediate(Double.valueOf(str)).flatMap(num ->
                IO.printLine(num + " * 2 = " + Double.toString(num * 2)).flatMap(unit ->
                    IO.immediate(Command.CONTINUE)));
        }
    });

    return command.until(c -> c == Command.QUIT);
}

Таким образом, расширив API до нужно уровня, можно построить полноценный каркас для написания программ, взаимодействующих с внешним миром. Сам каркас, конечно, кое-где внутри  будет использовать побочные эффекты, но его использование со стороны будет полностью функциональным. Мы строим программу, соединяя примитивные выражения в более сложные, а в конце одно большое выражение возвращается методом mainPure(), которое затем выполняется Java-машиной.
Важно отметить, что вышеописанная реализация IO является очень примитивной и неэффективной. Например, в этой реализации никак не учитывается переполнение стека. Поэтому, если возникнет желание написать чистофункциональный production-код с внешними эффектами, то лучше найти готовую библиотеку. Хотя пока я ничего конкретного посоветовать не могу. Вроде как возобновилась работа над развитием библиотеки Functional Java (после того как все авторы переключились на Scalaz), но я пока не исследовал, в каком она состоянии и насколько удачно там применяются фичи Java 8.

суббота, 27 сентября 2014 г.

Простая таблица языков программирования

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

Языки
Императивные
Функциональные
Динамические
JavaScript, Ruby, Python, PHP
Clojure, Erlang, Scheme
Статические
Java, C#, C++, Go
Scala, Haskell, OCaml

понедельник, 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 оценят).