Библиотека f для функционального программирования в Питоне
Привет, коллеги!
Я расскажу о библиотеке для Питона с лаконичным названием 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("")
data = resp.json()
if "error" in data:
raise BusinesException("")
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', '')
Наш вариант читается легче:
f.ichain(data, 'user', 'address', 'street') or ''
Из Кложи в библиотеку 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]
>>> True
l1.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()
>>> 3
Either
Эта монада расширяет предыдущую. Проблема 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.
Я надеюсь, специалисты по ФП простят неточности в формулировках.
Буду рад замечаниям в комментариях. Спасибо за внимание.
Комментарии (8)
15 июля 2016 в 16:40 (комментарий был изменён)
+2↑
↓
«Джаваскрипту или Гоу» — первый раз встречаю не латиницей. Красивее будет если написать в более привычном стиле, imho.15 июля 2016 в 18:08
+2↑
↓
При слове «Виндуз» у меня возникло ощущение, будто открыл «Навигатор игрового мира» за 1999 год.15 июля 2016 в 18:31
+2↑
↓
, а как же «джейсону»?
15 июля 2016 в 18:30
0↑
↓
Очень много «левых сдвигов», в итоге вкусные вещи лежит в той же библиотеке, что и примитивнейшие предикаты или коллекции, для которых есть collection. Ну то есть тебя не устроила стандартная либа, и ты написал свою, чтобы не ковыряться в collection? В таком случае, кто будет использовать твои коллекции?
Кроме того, заимствования из Go лежат рядом с заимствованиями из Haskell. pcall — интересно, но в одной библиотеке с дженериками? А вообще знающие люди пишут свой контекст «with as», если надо избежать множественных «try, except».15 июля 2016 в 18:31
0↑
↓
Интересно, как они это пишут? Примеры?
15 июля 2016 в 19:42 (комментарий был изменён)
0↑
↓
примерно так:class SocketContext:
def __init__(self, addr, port):
self.sock = socket.socket ()
self.sock.connect ((addr, port))def __enter__(self):
return self.sockdef __exit__(self, type, value, tb):
print_tb (tb)
sock.close ()
return TrueЭто менеджер контекста, если далее
with SocketContext («example.com», 80) as sock:
# Do smthСрабатывает __init__(«example.com», 80), затем __enter__ передаёт значение в sock (разумеется можно несколько, я в последний раз передавал функцию, чтобы получить замыкание), при ЛЮБОМ выходе (исключение, корректное завершение, return внутри блока) из контекста вызывается __exit__, если было исключение, то его можно обработать как tb, если __exit__ вернёт False, то вроде бросится исключение наружу.
Вспоминаем известный with open (name) as fin: — суть в том, что что бы мы не делали, файл будет закрыт после выхода из контекста.
15 июля 2016 в 20:01
+1↑
↓
Спасибо, я знаю, как работает контекстный менеджер. Мне не ясно, как он позволит, цитирую, «избежать множественных «try, except».
15 июля 2016 в 21:33
0↑
↓
pcall_wraps как я понимаю обёртывает функцию в
try:
return (func (*args, **kwargs), None)
except Exception, e:
return (e, None)Для единообразной арифметики, это, пожалуй, сэкономит код, но если ты хочешь «деструктор», который бы освобождал ресурсы/закрывал дескрипторы и по-разному обрабатывал разные ошибки, то всё равно надо каждый писать что-то кастомное. В сетях и парсинге всякого текста приходится как раз таки «закрывать дескрипторы». И опять таки это 7 строчный декоратор, который не лень и самому написать.