Привет, коллеги!
Я расскажу о библиотеке для Питона с лаконичным названием f. Это небо��ьшой пакет с функциями и классами для решения задач в функциональном стиле.
— Что, еще одна функциональная либа для Питона? Автор, ты в курсе, что есть fn.py и вообще этих функциональных поделок миллион?
— Да, в курсе.
Причины появления библиотеки
Я занимаюсь Питоном довольно давно, но пару лет назад всерьез увлекся функциональным программированием и Кложей в частности. Некоторые подходы, принятые в ФП, произвели на меня столь сильное впечатление, что мне захотелось перенести их в повседневную разработку.
Подчеркну, что не приемлю подход, когда паттерны одного языка грубо внедряют в другой без учета его принципов и соглашений о кодировании. Как бы я не любил ФП, меня раздражает нагромождение мап и лямбд в попытке выдать это за функциональный стиль.
Поэтому я старался оформить мои функции так, чтобы не встретить сопротивление коллег. Например, использовать внутри стандартные циклы с условиями вместо мапов и редьюсов, чтобы облегчить понимание тем, кто не знаком с ФП.
В результате некоторые из частей библиотеки побывали в боевых проектах и, возможно, все еще в строю. Сперва я копипастил их из проекта в проект, потом завел файлик-свалку функций и сниппетов, и, наконец, оформил все библиотекой, пакетом в Pypi и документаций.
Общие сведения
Библиотека написана на чистом Питоне и работает на любой ОС, в т.ч. на Виндузе. Поддерживаются обе ветки Питона. Конкретно я проверял на версиях 2.6, 2.7 и 3.5. Если возникнут трудности с другими версиями, дайте знать. Ед��нственная зависимость — пакет six для гибкой разработки сразу под обе ветки.
Библиотека ставится стандартным образом через pip:
pip install fВсе функции и классы доступны в головном модуле. Это значит, не нужно запоминать
пути к сущностям:
import f
f.pcall(...)
f.maybe(...)
f.io_wraps(...)
f.L[1, 2, 3]Пакет несет на борту следующие подсистемы:
- набор различных функций для удобной работы с данными
- модуль предикатов для быстрой проверки на какие-либо условия
- улучшенные версии коллекций — списка, кортежа, словаря и множества
- реализация дженерика
- монады Maybe, Either, IO, Error
В разделах ниже я приведу примеры кода с комментариями.
Функции
Первой функцией, которую я перенес в Питон из другой экосистемы, стала pcall из языка Луа. Я программировал на ней несколько лет назад, и хотя язык не функциональный, был от него в восторге.
Функция pcall (protected call, защищенный вызов) принимает другую функцию и возвращает пару (err, result), где либо err — ошибка и result пуст, либо наоборот. Этот подход знаком нам по другим языкам, например, Джаваскрипту или Гоу.
import f
f.pcall(lambda a, b: a / b, 4, 2)
>>> (None, 2)
f.pcall(lambda a, b: a / b, 4, 0)
>>> (ZeroDivisionError('integer division or modulo by zero'), None)Функцию удобно использовать как декоратор к уже написанным функциям, которые кидают исключения:
@f.pcall_wraps
def func(a, b):
return a / b
func(4, 2)
>>> (None, 2)
func(4, 0)
>>> (ZeroDivisionError('integer division or modulo by zero'), None)Используя деструктивный синтаксис, можно распаковать результат на уровне сигнатуры:
def process((err, result)):
if err:
logger.exception(err)
return 0
return result + 42
process(func(4, 2))К большому сожалению, деструктивный синтаксис выпилен в третьем Питоне. Приходится распаковывать вручную.
Интерсно, что использование пары (err, result) есть ни что иное, как монада Either, о которой мы еще поговорим.
Вот более реалистичный пример pcall. Часто приходится делать ХТТП-запросы и получать структуры данных из джейсона. Во время запроса может произойти масса ошибок:
- кривые хосты, ошибка резолва
- таймаут соединения
- сервер вернул 500
- сервер вернул 200, но парсинг джейсона упал
- сервер вернул 200, но в ответе ошибка
Заворачивать вызов в try с отловом четырех исключений означает сделать код абсолютно нечитаемым. Рано или поздно вы забудете что-то перехватить, и программа упадет. Вот пример почти реального кода. Он извлекает пользователя из локального рест-сервиса. Результат всегда будет парой:
@f.pcall_wraps
def get_user(use_id):
resp = requests.get("http://local.auth.server",
params={"id": user_id}, timeout=3)
if not resp.ok:
raise IOError("<log HTTP code and body here>")
data = resp.json()
if "error" in data:
raise BusinesException("<log here data>")
return dataРассмотрим другие функции библиотеки. Мне бы хотелось выделить f.achain и f.ichain. Обе предназначены для безопасного извлечения данных из объектов по цепочке.
Предположим, у вас Джанго со следующими моделями:
Order => Office => Department => ChiefПри этом все поля not null и вы без страха ходите по смежным полям:
order = Order.objects.get(id=42)
boss_name = order.office.department.chief.nameДа, я в курсе про select_related, но это роли не играет. Ситуация справедлива не только для ОРМ, но и для любой другой структуры класов.
Так было в нашем проекте, пока один заказчик не попросил сделать некоторые ссылки пустыми, потому что таковы особенности его бизнеса. Мы сделали поля в базе nullable и были рады, что легко отделались. Конечно, из-за спешки мы не написали юнит-тесты для моделей с пустыми ссылками, а в старых тестах модели были заполнены правильно. Клиент начал работать с обновленными моделями и получил ошибки.
Функция f.achain безопасно проходит по цепочке атрибутов:
f.achain(model, 'office', 'department', 'chief', 'name')
>>> JohnЕсли цепочка нарушена (поле равно None, не существуте), результат будет None.
Функция-аналог f.ichain пробегает по цепочке индексов. Она работает со словарями, списками и кортежами. Функция удобна для работы с данными, полученными из джейсона:
data = json.loads('''{"result": [{"kids": [{"age": 7, "name": "Leo"},
{"age": 1, "name": "Ann"}], "name": "Ivan"},
{"kids": null, "name": "Juan"}]}''')
f.ichain(data, 'result', 0, 'kids', 0, 'age')
>>> 7
f.ichain(data, 'result', 0, 'kids', 42, 'dunno')
>> NoneОбе функции я забрал из Кложи, где их предок называется get-in. Удобство в том, что в микросерверной архитектуре структура ответа постоянно меняется и может не соответствовать здравому смыслу.
Например, в ответе есть поле-объект "user" с вложенными полями. Однако, если пользователя по какой-то причине нет, поле будет не пустым объектом, а None. В коде начнут возникать уродливые конструкции типа:
data.get('user', {]}).get('address', {}).get('street', '<unknown>')Наш вариант читается легче:
f.ichain(data, 'user', 'address', 'street') or '<unknown>'Из Кложи в библиотеку f перешли два threading-макроса: -> и ->>. В библиотеке они называются f.arr1 и f.arr2. Оба пропускают исходное значение сквозь функиональные формы. Этот термин в Лиспе означает выражение, которе вычисляется позже.
Другими словами, форма — это либо функция func, либо кортеж вида (func, arg1, arg2, ...). Такую форму можно передать куда-то как замороженное выражение и вычислить позже с изменениями. Получается что-то вроде макросов в Лиспе, только очень убого.
f.arr1 подставляет значение (и дальнейший результат) в качестве первого
аргумента формы:
f.arr1(
-42, # начальное значение
(lambda a, b: a + b, 2), # форма
abs, # форма
str, # форма
)
>>> "40"f.arr2 делает то же самое, но ставит значение в конец формы:
f.arr2(
-2,
abs,
(lambda a, b: a + b, 2),
str,
("000".replace, "0")
)
>>> "444"Далее, функция f.comp возвращает композицию функций:
comp = f.comp(abs, (lambda x: x * 2), str)
comp(-42)
>>> "84"f.every_pred строит супер-предикат. Это такой предикат, который истиннен только если все внутренние предикаты истинны.
pred1 = f.p_gt(0) # строго положительный
pred2 = f.p_even # четный
pred3 = f.p_not_eq(666) # не равный 666
every = f.every_pred(pred1, pred2, pred3)
result = filter(every, (-1, 1, -2, 2, 3, 4, 666, -3, 1, 2))
tuple(result)
>>> (2, 4, 2)Супер-предикат ленив: он обрывает цепочку вычислений на первом же ложном значении. В примере выше использованы предикаты из модуля predicate.py, о котором мы еще поговорим.
Функция f.transduce — наивная попытка реализовать паттерн transducer (преобразователь) из Кложи. Короткими словами, transducer — это комбинация функций map и reduce. Их суперпозиция дает преобразование по принципу "из чего угодно во что угодно без промежуточных данных":
f.transduce(
(lambda x: x + 1),
(lambda res, item: res + str(item)),
(1, 2, 3),
""
)
>>> "234"Модуль функций замыкет f.nth и его синонимы: f.first, f.second и f.third для безопасного обращения к элементам коллекций:
f.first((1, 2, 3))
>>> 1
f.second((1, 2, 3))
>>> 2
f.third((1, 2, 3))
>>> 3
f.nth(0, [1, 2, 3])
>>> 1
f.nth(9, [1, 2, 3])
>>> NoneПредикаты
Предикат — это выражение, возвращающие истину или ложь. Предикаты используют в математике, логике и функциональном программировании. Часто предикат передают в качестве переменной в функции высшего порядка.
Я добавил несколько наиболее нужных предикатов в библиотеку. Предикаты могут унарными (без параметров) и бинарными (или параметрическими), когда поведение предиката зависит от первого аргумента.
Ра��смотрим примеры с унарными предикатами:
f.p_str("test")
>>> True
f.p_str(0)
>>> False
f.p_str(u"test")
>>> True
# особый предикат, который проверяет на int и float одновременно
f.p_num(1), f.p_num(1.0)
>>> True, True
f.p_list([])
>>> True
f.p_truth(1)
>>> True
f.p_truth(None)
>>> False
f.p_none(None)
>>> TrueТеперь бинарные. Создадим новый предикат, который утверждает, что что-то больше нуля. Что именно? Пока неизвесто, это абстракция.
p = f.p_gt(0)Теперь, имея предикат, проверим любое значение:
p(1), p(100), p(0), p(-1)
>>> True, True, False, FalseПо аналогии:
# Что-то больше или равно нуля:
p = f.p_gte(0)
p(0), p(1), p(-1)
>>> True, True, False
# Проверка на точное равенство:
p = f.p_eq(42)
p(42), p(False)
>>> True, False
# Проверка на ссылочное равенство:
ob1 = object()
p = f.p_is(ob1)
p(object())
>>> False
p(ob1)
>>> True
# Проверка на вхождение в известную коллекцию:
p = f.p_in((1, 2, 3))
p(1), p(3)
>>> True, True
p(4)
>>> FalseЯ не буду приводить примеры всех предикатов, это утомительно и долго. Предикаты прекрасно работают с функциями композиции f.comp, супер-предиката f.every_pred, встроенной функцией filter и дженериком, о котором речь ниже.
Дженерики
Дженерик (общий, обобщенный) — вызываемый объект, который имеет несколько стратегий вычисления результата. Выбор стратегии определяется на основании входящий параметров: их состава, типа или значения. Дженерик допускает наличие стратегии по умолчанию, когда не найдено ни одной другой для переданных параметров.
В Питоне нет дженериков из коробки, и особо они не нужны. Питон достаточно гибок, чтобы построить свою систему подбора функции под входящие значения. И все же, мне настолько понравилась реализация дженериков в Коммон-Лиспе, что из спортивного интереса я решил сделать что-то подобное в своей библиотеке.
Выглядит это примерно так. Сначала создадим экземпляр дженерика:
gen = f.Generic()Теперь расширим его конкретными обработчиками. Декоратор .extend принимает набор предикатов для этого обработчика, по одному на аргумент.
@gen.extend(f.p_int, f.p_str)
def handler1(x, y):
return str(x) + y
@gen.extend(f.p_int, f.p_int)
def handler2(x, y):
return x + y
@gen.extend(f.p_str, f.p_str)
def handler3(x, y):
return x + y + x + y
@gen.extend(f.p_str)
def handler4(x):
return "-".join(reversed(x))
@gen.extend()
def handler5():
return 42
Логика под капотом проста: декоратор подшивает функцию во внутренний словарь вместе с назначенными ей предикатами. Теперь дженерик можно вызывать с произвольными аргументами. При вызове ищется функция с таким же количеством предикаторв. Если каждый предикат возвращает истину для соответствующего аргумента, считается, что стратегия найдена. Возвращается результат вызова найденной функции:
gen(1, "2")
>>> "12"
gen(1, 2)
>>> 3
gen("fiz", "baz")
>>> "fizbazfizbaz"
gen("hello")
>>> "o-l-l-e-h"
gen()
>>> 42Что случится, если не подошла ни одна стратегия? Зависит от того, был ли задан обработчик по умолчанию. Такой обработчик должен быть готов встретить произвольное число аргументов:
gen(1, 2, 3, 4)
>>> TypeError exception goes here...
@gen.default
def default_handler(*args):
return "default"
gen(1, 2, 3, 4)
>>> "default"После декорирования функция становится экземпляром дженерика. Интересный прием — вы можете перебрасывать исполнение одной стратегии в другую. Получаются функции с несколькими телами, почти как в Кложе, Эрланге или Хаскеле.
Обработчик ниже будет вызван, если передать None. Однако, внутри он перенаправляет нас на другой обработчик с двумя интами, это handler2. Который, в свою очередь, возвращает сумму аргументов:
@gen.extend(f.p_none)
def handler6(x):
return gen(1, 2)
gen(None)
>>> 3Коллекции
Библиотека предоставляет "улучшенные" коллекции, основанные на списке, кортеже, словаре и множестве. Под улучшениями я имею в виду дополнительные методы и некоторые особенности в поведении каждой из коллекций.
Улучшенные коллекции создаются или из обычных вызовом класса, или особым синтаксисом с квадратными скобками:
f.L[1, 2, 3] # или f.List([1, 2, 3])
>>> List[1, 2, 3]
f.T[1, 2, 3] # или f.Tuple([1, 2, 3])
>>> Tuple(1, 2, 3)
f.S[1, 2, 3] # или f.Set((1, 2, 3))
>>> Set{1, 2, 3}
f.D[1: 2, 2: 3]
>>> Dict{1: 2, 2: 3} # или f.Dict({1: 2, 2: 3})Коллекции имеют методы .join, .foreach, .map, .filter, .reduce, .sum.
Список и кортеж дополнительно реализуют .reversed, .sorted, .group, .distinct и .apply.
Методы позволяют получить результат вызовом его из коллекции без передачи в функцию:
l1 = f.L[1, 2, 3]
l1.map(str).join("-")
>>> "1-2-3"result = []
def collect(x, delta=0):
result.append(x + delta)
l1.foreach(collect, delta=1)
result == [2, 3, 4]
>>> Truel1.group(2)
>>> List[List[1, 2], List[3]]Не буду утомлять листингом на каждый метод, желающие могут посмотреть исходный код с комментариями.
Важно, что методы возвращают новый экземпляр той же коллекции. Это уменьшает вероятность ее случайного измнения. Операция .map или любая другая на списке вернет список, на кортеже — кортеж и так далее:
f.L[1, 2, 3].filter(f.p_even)
>>> List[2]f.S[1, 2, 3].filter(f.p_even)
>>> Set{2}Словарь итерируется по парам (ключ, значение), о чем я всегда мечтал:
f.D[1: 1, 2: 2, 0: 2].filter(lambda (k, v): k + v == 2)
>>> Dict{0: 2, 1: 1}Улучшенные коллекции можно складывать с любой другой коллекцией. Результатом станет новая коллекция этого (левого) типа:
# Слияние словарей
f.D(a=1, b=2, c=3) + {"d": 4, "e": 5, "f": 5}
>>> Dict{'a': 1, 'c': 3, 'b': 2, 'e': 5, 'd': 4, 'f': 5}
# Множество + стандартный спосок
f.S[1, 2, 3] + ["a", 1, "b", 3, "c"]
>>> Set{'a', 1, 2, 3, 'c', 'b'}
# Список и обычный кортеж
f.L[1, 2, 3] + (4, )
List[1, 2, 3, 4]Любую коллекцию можно переключить в другую:
f.L["a", 1, "b", 2].group(2).D()
>>> Dict{"a": 1, "b": 2}
f.L[1, 2, 3, 3, 2, 1].S().T()
>>> Tuple[1, 2, 3]Комбо!
f.L("abc").map(ord).map(str).reversed().join("-")
>>> "99-98-97"def pred(pair):
k, v = pair
return k == "1" and v == "2"
f.L[4, 3, 2, 1].map(str).reversed() \
.group(2).Dict().filter(pred)
>>> Dict{"1": "2"}Монады
Последний и самый сложный раздел в библиотеке. Почитав цикл статей о монадах, я отважился добавить в библиотеку их тоже. При этом позволил себе следующие отклонения:
Проверки входных значений основаны не на типах, как в Хаскеле, а на предикатах, что делает монады гибче.
Оператор
>>=в Хаскеле невозможно перенести в Питон, поэтому он фигурирует как>>(он же__rshift__, битовый сдвиг вправо). Проблема в том, что в Хаскеле тоже есть оператор>>, но используется он реже, чем>>=. В итоге, в Питоне под>>мы понимаем>>=из Хаскела, а оригинальный>>просто не используем.
- Не смотря на усилия, я не смог реализовать do-нотацию Хаскелла из-за ограничений синтаксиса в Питоне. Пробовал и цикл, и генератор, и контекстные менеджеры — все мимо.
Maybe
Монада Maybe (возможно) так же известна как Option. Этот класс монад представлен двумя экземплярами: Just (или Some) — хранилище положительного результата, в которм мы заинтересованы. Nothing (в других языках — None) — пустой результат.
Простой пример. Определим монадный конструктор — объект, который будет преобразовывать скалярные (плоские) значения в монадические:
MaybeInt = f.maybe(f.p_int)По-другому это называется unit, или монадная единица. Теперь получим монадные значения:
MaybeInt(2)
>>> Just[2]
MaybeInt("not an int")
>>> NothingВидим, что хорошим результатом будет только то, что проходит проверку на инт. Теперь попробуем в деле монадный конвеер (monadic pipeline):
MaybeInt(2) >> (lambda x: MaybeInt(x + 2))
>>> Just[4]
MaybeInt(2) >> (lambda x: f.Nothing()) >> (lambda x: MaybeInt(x + 2))
>>> NothingИз примера видно, что Nothing прерывает исполнения цепочки. Если быть совсем точным, цепочка не обрывается, а проходит до конца, только на каждом шаге возвращается Nothing.
Любую функцию можно накрыть монадным декоратором, чтобы получать из нее монадические представления скаляров. В примере ниже декоратор следит за тем, чтобы успехом считался только возрат инта — это значение пойдет в Just, все остальное — в Nothing:
@f.maybe_wraps(f.p_num)
def mdiv(a, b):
if b:
return a / b
else:
return None
mdiv(4, 2)
>>> Just[2]
mdiv(4, 0)
>>> NothingОператор >> по другому называется монадным связыванием или конвеером (monadic binding) и вызывается методом .bind:
MaybeInt(2).bind(lambda x: MaybeInt(x + 1))
>>> Just[3]Оба способа >> и .bind могут принять не только функцию, но и функциональную форму, о которой я уже писал выше:
MaybeInt(6) >> (mdiv, 2)
>>> Just[3]
MaybeInt(6).bind(mdiv, 2)
>>> Just[3]Чтобы высвободить скалярное значение из монады, используйте метод .get. Важно помнить, что он не входит в классическое определение монад и является своего рода поблажкой. Метод .get должен быть строго на конце конвеера:
m = MaybeInt(2) >> (lambda x: MaybeInt(x + 2))
m.get()
>>> 3Either
Эта монада расширяет предыдущую. Проблема Maybe в том, что негативный результат отбрасывается, в то время как мы всегда хотим знать причину. Either состоит из подтипов Left и Right, левое и правое значения. Левое значение отвечает за негативный случай, а правое — за позитивный.
Правило легко запомнить по фразе "наше дело правое (то есть верное)". Слово right в английском языке так же значит "верный".
А вот и флешбек из прошлого: согласитесь, напоминает пару (err, result) из начала статьи? Коллбеки в Джаваскрипте? Результаты вызовов в Гоу (только в другом порядке)?
То-то же. Все это монады, только не оформленные в контейнеры и без математического аппарата.
Монада Either используется в основном для отлова ошибок. Ошибочное значение уходит влево и становится результатом конвеера. Корректный результат пробрысывается вправо к следующим вычислениям.
Монадический конструк��ор Either принимает два предиката: для левого значения и для правого. В примере ниже строковые значения пойдут в левое значение, числовые — в правое.
EitherStrNum = f.either(f.p_str, f.p_num)
EitherStrNum("error")
>>> Left[error]
EitherStrNum(42)
>>> Right[42]Проверим конвеер:
EitherStrNum(1) >> (lambda x: EitherStrNum(x + 1))
>>> Right[2]
EitherStrNum(1) >> (lambda x: EitherStrNum("error")) \
>> (lambda x: EitherStrNum(x + 1))
>>> Left[error]Декоратор f.either_wraps делает из функции монадный конструктор:
@f.either_wraps(f.p_str, f.p_num)
def ediv(a, b):
if b == 0:
return "Div by zero: %s / %s" % (a, b)
else:
return a / b
@f.either_wraps(f.p_str, f.p_num)
def esqrt(a):
if a < 0:
return "Negative number: %s" % a
else:
return math.sqrt(a)
EitherStrNum(16) >> (ediv, 4) >> esqrt
>>> Right[2.0]
EitherStrNum(16) >> (ediv, 0) >> esqrt
>>> Left[Div by zero: 16 / 0]IO
Монада IO (ввод-вывод) изолирует ввод-вывод данных, например, чтение файла, ввод с клавиатуры, печать на экран. Например, нам нужно спросить имя пользователя. Без монады мы бы просто вызвали raw_input, однако это снижает абстракцию и засоряет код побочным эффектом.
Вот как можно изолировать ввод с клавиатуры:
IoPrompt = f.io(lambda prompt: raw_input(prompt))
IoPrompt("Your name: ") # Спросит имя. Я ввел "Ivan" и нажал RET
>>> IO[Ivan]Поскольку мы получили монаду, ее можно пробросить дальше по конвееру. В примере ниже мы введем имя, а затем выведем его на экран. Декоратор f.io_wraps превращает функцию в монадический конструктор:
import sys
@f.io_wraps
def input(msg):
return raw_input(msg)
@f.io_wraps
def write(text, chan):
chan.write(text)
input("name: ") >> (write, sys.stdout)
>>> name: Ivan # ввод имени
>>> Ivan # печать имени
>>> IO[None] # результатError
Монада Error, она же Try (Ошибка, Попытка) крайне полезна с практической точки зрения. Она изолирует исключения, гарантируя, что результатом вычисления станет либо экземпляр Success с правильным значением внутри, либо Failture с зашитым исключением.
Как и в случае с Maybe и Either, монадный конвеер исполняется только для положительного результата.
Монадический конструктор принимает функцию, поведение которой считается небезопасным. Дальнейшие вызовы дают либо Success, либо Failture:
Error = f.error(lambda a, b: a / b)
Error(4, 2)
>>> Success[2]
Error(4, 0)
>>> Failture[integer division or modulo by zero]Вызов метода .get у экземпляра Failture повторно вызовет исключение. Как же до него добраться? Поможет метод .recover:
Error(4, 0).get()
ZeroDivisionError: integer division or modulo by zero
# value variant
Error(4, 0).recover(ZeroDivisionError, 42)
Success[2]Этот метод принимает класс исключения (или кортеж классов), а так же новое значение. Результатом становится монада Success с переданным значением внутри. Значение может быть и функцией. Тогда в нее передается экземпляр исключения, а результат тоже уходит в Success. В этом месте появляется шанс залогировать исключение:
def handler(e):
logger.exception(e)
return 0
Error(4, 0).recover((ZeroDivisionError, TypeError), handler)
>>> Success[0]Вариант с декоратором. Функции деления и извлечения корня небезопасны:
@f.error_wraps
def tdiv(a, b):
return a / b
@f.error_wraps
def tsqrt(a):
return math.sqrt(a)
tdiv(16, 4) >> tsqrt
>>> Success[2.0]
tsqrt(16).bind(tdiv, 2)
>>> Success[2.0]Конвеер с расширенным контекстом
Хорошо, когда функции из конвеера требуют данные только из предыдущей монады. А что делать, если нужно значение, полученное два шага назад? Где хранить контекст?
В Хаскеле это проблему решает та самая do-нотация, которую не удалось повторить в Питоне. Придется воспользоваться вложенными функциями:
def mfunc1(a):
return f.Just(a)
def mfunc2(a):
return f.Just(a + 1)
def mfunc3(a, b):
return f.Just(a + b)
mfunc1(1) >> (lambda x: mfunc2(x) >> (lambda y: mfunc3(x, y)))
# 1 2 1 2
>>> Just[3]В примере выше затруднения в том, что функции mfunc3 нужно сразу два значения, полученных из других монад. Сохранить контекст пересенных x и y удается благодаря замыканиям. После выхода из замыкания цепочку можно продолжить дальше.
Заключение
Итак, мы рассмотрели возможности библиотеки f. Напомню, проект не ставит цель вытеснить другие пакеты с функциональным уклоном. Это всего лишь попытка обобщить разрозненную практику автора, желание попробовать себя в роли мейнтейнера проекта с открытым исходным кодом. А еще — привлечь интерес начинающих разработчиков к функциональному подходу.
Ссылка на Гитхаб. Документация и тесты — там же. Пакет в Pypi.
Я надеюсь, специалисты по ФП простят неточности в формулировках.
Буду рад замечаниям в комментариях. Спасибо за внимание.