Бібліотека f для функціонального програмування на Пітоні

Привіт, колеги!
Я розповім про бібліотеці для Пітона з лаконічною назвою
f
. Це невеликий пакет з функціями та класами для вирішення завдань у функціональному стилі.
— Що, ще одна функціональна ліба для Пітона? Автор, ти в курсі, що є fn.py і взагалі цих функціональних виробів мільйон?
— Так, в курсі.
Причини появи бібліотеки
Я займаюся Пітоном досить давно, але кілька років тому всерйоз захопився функціональним програмуванням і Кложей зокрема. Деякі підходи, прийняті в ФП, справили на мене таке сильне враження, що мені захотілося перенести їх у повсякденне розробку.
Підкреслю, що не сприймаю підхід, коли патерни однієї мови грубо впроваджують в інший без урахування його принципів та угод про кодування. Як би я не любив ФП, мене дратує нагромадження мап і лямбд в спробі видати це за функціональний стиль.
Тому я намагався оформити мої функції так, щоб не зустріти опір колег. Наприклад, використовувати всередині стандартні цикли з умовами замість мапов і редьюсов, щоб полегшити розуміння тим, хто не знайомий з ФП.
В результаті деякі з частин бібліотеки побували в бойових проектах і, можливо, все ще в строю. Спершу я копипастил їх з проекту в проект, потім завів файлик-звалище функцій і фрагмента, і, нарешті, оформив всі бібліотекою, пакетом в Pypi та документацій.
Загальні відомості
Бібліотека написана на чистому Пітоні і працює на будь-якій ОС, в т. ч. на Виндузе. Підтримуються обидві гілки Пітона. Конкретно я перевіряв на версіях 2.6, 2.7 та 3.5. Якщо виникнуть труднощі з іншими версіями, дайте знати. Єдина залежність — пакет
six
для гнучкої розробки відразу під обидві гілки.
Бібліотека ставиться стандартним чином через pip:
pip f install

Всі функції і класи доступні в головному модулі. Це значить, що не треба запам'ятовувати
шляхи до сутностей:
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("привіт")
>>> "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, "е": 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, "с"]
>>> 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 by zero modulo

# 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.
Я сподіваюся, фахівці з ФП пробачать неточності у формулюваннях.
Буду радий зауважень в коментарях. Дякую за увагу.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.