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

Это заставило меня задуматься: почему бы не использовать некоторые концепции и методы из функционального мира в программировании Android?

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

Что ж, к черту! Я здесь, чтобы сказать вам, что вы тоже можете этому научиться. Вы тоже можете это использовать. С его помощью вы тоже можете создавать красивые приложения. Приложения с элегантным, читаемым кодом и меньшим количеством ошибок.

Добро пожаловать в Функциональное программирование (FP) для разработчиков Android. В этой серии мы узнаем основы FP и то, как мы можем использовать их в старой доброй Java и новом замечательном Kotlin. Идея состоит в том, чтобы концепции были основаны на практичности и избегали как можно большего количества академического жаргона.

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

Готовый? Пойдем.

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

Хороший вопрос. Термин функциональное программирование - это зонтик для ряда концепций программирования, которым это прозвище не совсем соответствует. По сути, это стиль программирования, который рассматривает программы как оценку математических функций и избегает изменяемого состояния и побочных эффектов (мы поговорим об этом достаточно скоро).

По сути, FP подчеркивает:

  • Декларативный код. Программисты должны беспокоиться о what и позволить компилятору и среде выполнения беспокоиться о как
  • Явность. Код должен быть максимально очевидным. В частности, следует изолировать побочные эффекты , чтобы избежать сюрпризов. Поток данных и обработка ошибок определены явно, а такие конструкции, как операторы GOTO и Исключения, избегаются, поскольку они могут привести ваше приложение в непредвиденное состояние.
  • Параллелизм. Большая часть функционального кода по умолчанию является параллельным из-за концепции, известной как функциональная чистота. Похоже, что общее согласие заключается в том, что именно эта черта вызывает рост популярности функционального программирования, поскольку ядра ЦП не становятся быстрее с каждым годом, как раньше (см. Закон Мура), и мы должны сделать наши программы более параллельными для воспользоваться преимуществами многоядерных архитектур.
  • Функции высшего порядка. Функции являются членами первого класса, как и все другие языковые примитивы. Вы можете передавать функции так же, как строку или int.
  • Неизменяемость. Переменные нельзя изменять после инициализации. Однажды созданная вещь остается неизменной навсегда. Если вы хотите, чтобы это изменилось, вы создаете новую вещь. Это еще один аспект ясности и избежания побочных эффектов. Если вы знаете, что вещь не может измениться, вы гораздо больше уверены в ее состоянии, когда используете ее.

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

В этой первой части серии давайте начнем с некоторых из наиболее фундаментальных концепций FP: Чистота, Побочные эффекты и Порядок.

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

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

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

Java

int add(int x) {
    int y = readNumFromFile();
    return x + y;
}

Котлин

fun add(x: Int): Int {
    val y: Int = readNumFromFile()
    return x + y
}

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

Давайте превратим его в чистую функцию.

Java

int add(int x, int y) {
    return x + y;
}

Котлин

fun add(x: Int, y: Int): Int {
    return x + y
}

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

P.S. Пустой ввод по-прежнему остается вводом. Если функция не принимает входных данных и каждый раз возвращает одну и ту же константу, она все еще чиста.

P.P.S. Свойство всегда возвращать один и тот же вывод для данного ввода также известно как ссылочная прозрачность, и вы можете увидеть его использование, когда говорите о чистых функциях.

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

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

Java

int add(int x, int y) {
    int result = x + y;
    writeResultToFile(result);
    return result;
}

Котлин

fun add(x: Int, y: Int): Int {
    val result = x + y
    writeResultToFile(result)
    return result
}

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

Считается, что любой код, который изменяет состояние внешнего мира - изменяет переменную, записывает в файл, записывает в БД, удаляет что-то и т. Д., Имеет побочный эффект.

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

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

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

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

Заказ

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

Допустим, у нас есть функция, которая вызывает 3 чистые функции внутри:

Java

void doThings() {
    doThing1();
    doThing2();
    doThing3();
}

Котлин

fun doThings() {
    doThing1()
    doThing2()
    doThing3()
}

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

Порядок выполнения можно изменить и оптимизировать для независимых чистых функций. Обратите внимание: если ввод doThing2 () был результатом doThing1 (), то они должны были выполняться по порядку, но doThing3 () все еще можно было переупорядочить для выполнения перед doThing1 ().

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

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

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

Резюме

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

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

Читать дальше



Если вам это понравилось, нажмите 👏 ниже. Я замечаю каждого и благодарен каждому из них.

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