Функциональное программирование для Android-разработчика. Часть первая

Пятница, 09 Июнь 2017 11:59

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

В этой части мы рассмотрим пять концепций:

Что такое функциональное программирование и почему мы должны его использовать?

Прим. перев. Советуем посмотреть наше руководство по функциональному программированию c примерами на JavaScript. Также у нас на сайте есть цикл статей о функциональном C#.

Термин «функциональное программирование» является объединением ряда концепций программирования. Это стиль программирования, который рассматривает программу как последовательность математических функций и предполагает отсутствие изменяемых данных и побочных эффектов.

Основы ФП, которые стоит выделить:

  • используется декларативный код;
  • ясность — код должен быть как можно более очевидным. В частности, побочные эффекты должны быть изолированы, а поток данных и обработка ошибок — описаны явно;
  • параллелизм — функциональный код по умолчанию конкурентен из-за концепции, известной как функциональная чистота;
  • функции высшего порядка;
  • неизменяемость — переменные не подлежат изменению после их инициализации.

Чистые функции

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

 

Рассмотрим простую функцию, которая складывает два числа. Она читает одно число из файла, а другое принимает в качестве параметра:

Java

Kotlin

Выходные данные этой функции зависят не только от входных. В зависимости от того, что возвращает readNumFromFile(), выходные значения для одного и того же x могут быть разными. Эта функция называется функцией с побочным эффектом. Превратим ее в чистую функцию.

Java

Kotlin

Теперь выходные значения функции зависят только от входных. Для заданных x и yфункция всегда будет возвращать один и тот же результат. Теперь эта функция чистая. Математические функции работают так же: их выходные значения зависят только от входных. Поэтому функциональное программирование гораздо ближе к математике, чем к привычному стилю программирования.

Побочные эффекты

Рассмотрим эту концепцию с тем же примером. Модифицируем функцию, чтобы мы могли записать результат в файл:

Java

Kotlin

Функция записывает результат вычисления в файл, т.е. меняет состояние «внешнего мира». Такую функцию уже не называют чистой, теперь у неё есть побочный эффект.

Любая функция, которая изменяет переменную, удаляет что-то, записывает в файл или в БД, имеет побочный эффект и не используется в ФП.

Предположим, что вы пишете фрагмент кода, который зависит от кэша. Теперь результат выполнения вашего кода зависит от того, что находилось в кэше и валидны ли данные в нём. Вы не сможете понять, что делает программа, если не понимаете все возможные состояния кэша. Добавим сюда другие вещи, от которых зависит приложение: сеть, база данных, файлы, ввод пользователя. Становится очень сложно узнать, что именно происходит. Значит ли это, что в ФП не используются базы данных, кэши и т.д.? Конечно же, нет.

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

Порядок

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

Предположим, у нас есть функция, которая вызывает 3 чистых функции:

Java

Kotlin

Мы знаем, что эти функции не зависят друг от друга и что они ничего не изменят в системе. Это делает порядок, в котором они выполняются, полностью взаимозаменяемым. Обратите внимание, что если бы doThing2() была результатом doThing1(), то функции должны были бы выполняться по порядку, но doThing3() могла бы быть выполнена перед doThing1().

Что нам это даёт? Конкурентность, вот что! Мы можем запустить эти функции на 3 отдельных ядрах процессора, ни о чём не беспокоясь.

В основном, компиляторы функциональных языков, таких как Haskell, могут проанализировать ваш код и сказать, является ли он параллельным. Теоретически эти компиляторы могут распараллелить ваш код автоматически (но на практике пока такое не реализовано).

Неизменяемость

Идея неизменяемости заключается в том, что созданное значение никогда не может быть изменено.

Предположим, что у нас есть класс Car:

Java

Kotlin

Поскольку у класса есть метод в Java и он является изменяемым в Kotlin, я могу изменить имя машины после того, как я его создал:

Java

Kotlin

Этот класс не является неизменяемым. Его можно изменить после создания. Давайте сделаем его неизменяемым. Чтобы сделать это на Java, мы должны:

  • создать переменную final;
  • удалить метод;
  • создать класс final так, чтобы другой класс не смог расширить и изменить его.

Java

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

Kotlin

Теперь, если кому-то нужно создать новый автомобиль, им нужно инициализировать новый объект. Никто не сможет изменить наш автомобиль, когда он будет создан. Этот класс теперь неизменяемый.

Но что насчет метода getName() в Java? В нем строки по умолчанию неизменяемы. Даже если кто-то получил ссылку на нашу строку и попытается ее изменить, то они получат копию этой строки, а исходная строка осталась бы неизменной. Но что относительно вещей, которые не являются неизменяемыми? Возможно, список? Давайте модифицируем класс Car, чтобы иметь список людей, которые им управляют.

Java

В этом случае кто-то может использовать метод getListOfDrivers(), чтобы получить ссылку на наш внутренний список и изменить его, что делает наш класс изменяемым.

Чтобы сделать его неизменяемым, мы должны передать глубокую копию списка, чтобы новый список мог быть безопасно изменен вызывающим. Глубокая копия означает, что мы копируем все зависимые данные рекурсивно. Например, если список был списком объектов «Водитель» вместо простых строк, нам также пришлось бы копировать каждый из объектов «Водитель». В противном случае мы создадим новый список со ссылками на исходные объекты, которые могут быть изменены. В нашем классе список состоит из неизменяемых строк, поэтому мы сделаем глубокую копию следующим образом:

Java

Теперь наш класс действительно неизменяем.

В Kotlin мы можем просто объявить список неизменяемым в определении класса, а затем безопасно использовать его (если вы, конечно, не вызываете его из Javа).

Kotlin

Конкурентность

Как мы уже говорили выше, чистые функции позволяют нам упростить конкурентность. Если объект неизменяем, то его очень просто использовать в чистых функциях, поскольку вы не можете его изменить и тем самым вызвать побочные эффекты.

Давайте посмотрим на пример. Предположим, что мы добавили метод getNoOfDrivers() в наш класс Car в Java. Мы делаем его изменяемым как в Kotlin, так и в Java, позволяя пользователю изменять количество переменных водителей следующим образом:

Java

Kotlin

Разделим экземпляр класса Car через 2 потока: Thread_1 и Thread_2Thread_1 хочет сделать некоторые вычисления на основе количества водителей. Он вызывает метод getNoOfDrivers() в Java или обращается к свойству noOfDrivers в Kotlin. Тем временем Thread_2 изменяет переменную noOfDriversThread_1 не знает об этом изменении и продолжает свои расчеты. Эти вычисления будут неправильными, поскольку состояние мира было изменено без Thread_2 и Thread_1.

Следующая диаграмма иллюстрирует эту проблему:

Эта проблема называется Read-Modify-Write. Традиционный способ решить ее — использовать блокировки и мьютексы, чтобы только один поток мог работать с данными. В нашем случае Thread_1 будет удерживать блокировку до тех пор, пока не завершит расчет.

Этот тип управления ресурсами трудно сделать безопасным, и он приводит к ошибкам конкурентности, которые трудно анализировать.

Как это исправить? Давайте сделаем класс CAR снова неизменяемым:

Java

Kotlin

Теперь Thread_1 может выполнять вычисления без проблем, так как гарантировано, что Thread_2 не сможет изменить класс Car. Если Thread_2 хочет изменить класс, тогда он создаст собственную копию. Никаких блокировок не потребуется.

Неизменяемость гарантирует, что данные, которые не должны быть изменены, не будут изменены.

Перевод статьи «Functional Programming for Android Developers — Part 1»

Источник: https://tproger.ru/translations/functional-android-1/