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

Чистота

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

Эта функция чистая:

def addition(x, y):
    return x + y

Эта функция нечиста:

def call_server():
    return requests.get("google.com")

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

В любой день недели addition вернет 4 для ввода 2, 2. call_server однако зависит от таких вещей, как ваше интернет-соединение и от того, какой дудл есть у google.com в этот день.

Вот еще один пример нечистой функции:

import datetime
def what_day_is_it():
    return datetime.datetime.today()

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

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

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

В императивном программировании довольно часто встречаются присваивания и мутации:

x = 7
x = 9
y = []
y.append(1)

И x, и y означают разные вещи, в зависимости от строки кода, которую мы смотрим. Это приводит к появлению тонких ошибок, таких как этот:

x  = (x for x in [1,2,3])
sum_of_first_ten_digits = sum(x)
max_of_first_ten_digits = max(x)
# ValueError: max() arg is an empty sequence

Генератор x изменяется на месте после суммирования, в результате чего max получает пустую итерацию. Однако это сработает:

x  = (1,2,3)
sum_of_first_ten_digits = sum(x)
max_of_first_ten_digits = max(x)

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

Противоположностью неизменности является мутация и состояние - значения, которые меняются со временем.

Сочинение

Функциональное программирование - это красивое название, но не лучшее. Несмотря на то, что функциональное программирование вращается вокруг композиции функций, это больше касается композиции, чем функций. Лучшим названием для этого было бы «составное программирование».

Вот базовая реализация compose.

def compose(f, g):
    def composition(x):
         return f(g(x))
    return composition
double_then_increment = compose(lambda x: x + 1, lambda x: x * 2)
double_then_increment(2)  # Will return 5.

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

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

Давайте посмотрим на следующий пример.

Эта функция берет набор текстов и производит количество слов, встречающихся в этих текстах. Таким образом, для ["hello how are you", "hello you"] мы получим {"hello": 2, "how": 1, "are": 1, "you": 2}.

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

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

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

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

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

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

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

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

С меньшим уровнем вложенности все становится намного приятнее.

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

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

При рассмотрении реализации remove это выглядит необычно и может быть нетривиальным для понимания с первого взгляда. Это конвейер более высокого порядка, он получает функцию и возвращает функцию, сначала превращая ее в ее complement, а затем встраивая в filter.

Хорошо, это мило, но это становится трудновато читать. К счастью, мы можем использовать compose, чтобы сгладить эту структуру, и, естественно, мы также вынесем ее за скобки. Но если мы просто используем compose, мы получим странный обратный порядок чтения. Вместо этого давайте использовать compose_left (то же, что и compose, но с обратным порядком выполнения).

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

groupby каким-то образом преобразует элемент в его «ключ», а затем построит словарь из этих ключей в элементы, которым они принадлежат. Так, например, если мы хотим сгруппировать по первому символу, мы должны сделать что-то вроде groupby(lambda x: x[0])(["hello", "there", "hi"]), что даст нам {"h": ["hello", "hi"], "t": ["there]}. В нашем случае мы просто хотим посчитать, поэтому наша ключевая функция будет просто identity.

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

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

Эти функции, которые мы извлекли, и многие другие доступны в gamla python lib (отказ от ответственности: я один из ее авторов).

Асинхронный

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

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

Но такая реализация потенциально неоптимальна. Рассмотрим случай, когда is_word_important вызывает какой-то удаленный сервер. Это означает, что мы будем await на каждый запрос слова, прежде чем перейти к следующему, что будет ужасно медленным. Вместо этого мы хотели бы сделать это параллельно:

Это оптимально, но код начинает выглядеть довольно запутанным. Используя gamla, мы можем игнорировать тот факт, что все это async, и сосредоточиться на логике. Базовая реализация gamla.map позаботится об этом, оптимально, используя те же примитивы async.

Разве это не хорошо? Программист может меньше заботиться о деталях и сосредоточиться на логике, в то время как базовая структура оптимизирует работу.

Conclusion

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

Однако обратите внимание на это предупреждение - вы можете перестараться, определив неправильный шаблон, злоупотребив абстракцией или принудительно добавив в него свой код, даже если он не подходит. Объективная проверка работоспособности - это длина кода до и после использования абстракции. Если ваш код не сжался, возможно, вы перестарались. Если это так - хорошая работа, вы, вероятно, нашли полезный образец!

На этом мы завершаем краткое введение в мир функционального программирования. Удачного вам дня.

Больше контента на plainenglish.io