Библиотека 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("<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]
    >>> 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.


    Я надеюсь, специалисты по ФП простят неточности в формулировках.


    Буду рад замечаниям в комментариях. Спасибо за внимание.

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 53

      0
      «Джаваскрипту или Гоу» — первый раз встречаю не латиницей. Красивее будет если написать в более привычном стиле, imho.
        +12
        При слове «Виндуз» у меня возникло ощущение, будто открыл «Навигатор игрового мира» за 1999 год.
          0

          Значит, в 1999 году в журнале работал толковый редактор.

          +3
          а как же «джейсону»?
            +1

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

              +2
              Если есть желание быть понятным большинству и, самое главное, избежать в комментариях уже которых по счету споров на эту тему, лучше писать латиницей. Не понимаю твоего упрямства)
                +3

                Большинство научится, а своё «упрямство» я неоднократно объяснял со всех возможных сторон.

                –6
                Читайте больше на русском, дальше не читал.
                  +3

                  Как вы изящно над собой же и пошутили в контексте нашего разговора!


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

                    0
                    Смешно. Никогда не понимал людей, которые вместо того чтобы использовать терминологию и характер написания принятые в интернациональной среде, обязательно пытаются «локализовать». Я открою вам небольшой секрет, в современном мире, если вы хотите быть на переднем крае в любой области необходимо бОльшую часть времени уделять именно международным источникам. Зачем это искусственное усложнение? Есть и без этого огромное количество вещей, которые затрудняют чтение профессиональных статей.
                    Если же вы используете источники только на русском языке… это наверное очень круто, быть всегда на шаг позади? Зачем это искусственное огораживание от свежей информации? Зачем все эти усложнения с локализацей? Лень? Нежелание менять привычки? Ненависть ко всему нерусскому?
                    К автору поста у меня претензий нет, мне все равно как он написал, хотя и читается так себе. Но вот к таким как вы, лучше не раздавать людям советов.
                      +2

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


                      Если же вы посмотрите на журналы и статьи, издававшиеся до этого печального времени, вы увидите, что всё писали на русском, как и должно быть — вот это и есть традиция.


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


                      Если же вы используете источники только на русском языке… это наверное очень круто, быть всегда на шаг позади?

                      Вы что-то перепутали. Я вам посоветовал читать побольше по-русски. Не пользовать источниками только на русском, а просто больше читать по-русски.


                      Но вот к таким как вы, лучше не раздавать людям советов.

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

                        +1

                        Как будто для вас написал: http://grishaev.me/eng-terms

                  +6

                  Я написал именно в привычном стиле.

                    0
                    Привет! Добавь сюда свою либу https://github.com/xgrommx/awesome-functional-programming#python
                  0
                  Очень много «левых сдвигов», в итоге вкусные вещи лежит в той же библиотеке, что и примитивнейшие предикаты или коллекции, для которых есть collection. Ну то есть тебя не устроила стандартная либа, и ты написал свою, чтобы не ковыряться в collection? В таком случае, кто будет использовать твои коллекции?
                  Кроме того, заимствования из Go лежат рядом с заимствованиями из Haskell. pcall — интересно, но в одной библиотеке с дженериками? А вообще знающие люди пишут свой контекст «with as», если надо избежать множественных «try, except».
                    0

                    Интересно, как они это пишут? Примеры?

                      0
                      примерно так:

                      class SocketContext:
                          def __init__(self, addr, port):
                              self.sock = socket.socket()
                              self.sock.connect((addr, port))

                          def __enter__(self):
                              return self.sock

                          def __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: — суть в том, что что бы мы не делали, файл будет закрыт после выхода из контекста.
                        0

                        Спасибо, я знаю, как работает контекстный менеджер. Мне не ясно, как он позволит, цитирую, "избежать множественных «try, except»".

                          0
                          pcall_wraps как я понимаю обёртывает функцию в
                          try:
                              return (func(*args, **kwargs), None)
                          except Exception, e:
                              return (e, None)

                          Для единообразной арифметики, это, пожалуй, сэкономит код, но если ты хочешь «деструктор», который бы освобождал ресурсы/закрывал дескрипторы и по-разному обрабатывал разные ошибки, то всё равно надо каждый писать что-то кастомное. В сетях и парсинге всякого текста приходится как раз таки «закрывать дескрипторы». И опять таки это 7 строчный декоратор, который не лень и самому написать.
                            0
                            но если ты хочешь «деструктор»
                            Я не хочу никакого деструктора, и ничего не хочу закрывать. О чем вы?

                    +4
                    Например, использовать внутри стандартные циклы с условиями вместо мапов и редьюсов, чтобы облегчить понимание тем, кто не знаком с ФП.

                    Я, конечно, не гуру Python, но мне кажется, что это несколько не python-way, и мне казалось, что обычно стараются наоборот писать декларативно.


                    Сам я больше использую C# и стараюсь, где возможно, использовать лямбды и LINQ (не перебарщивая, конечно). ИМХО, декларативный стиль куда нагляднее

                      0

                      Реализация неважна, главное — интерфейс. Всегда можно поменять цикл на мап, и тесты не упадут.

                      –4
                      Используя деструктивный синтаксис, можно распаковать результат на уровне сигнатуры.

                      Whoops


                        +5

                        Написано же:


                        К большому сожалению, деструктивный синтаксис выпилен в третьем Питоне. Приходится распаковывать вручную.

                          –2

                          Тогда мне тем более не понятно, для чего давать заведомо нерабочий пример.

                            +1

                            Это рабочий пример. Вы запускаете его не в том окружении. В тексте это оговорено.

                        –4
                        Вы пишите «питоном занимаюсь довольно давно», а давно это сколько?
                        Я вот лично считаю, что тоже давольно давно работаю с питоном, лет семь, это давно по-вашему? Давнее вашего?
                          0

                          Тоже 7 лет.

                            –4
                            Тогда вы должны понимать, что ваша затея — жуткий велосипед, мало что никому не нужный, так порой еще и нарушающий дзен питона.
                              +2

                              Не уловил связь между "давно" и "Тогда вы должны понимать", объясните?

                                –2
                                Если вы работаете давно, значит у вас есть опыт разработки, вы понимаете язык, не пытаетесь писать на питоне в стиле С++, знаете библиотеку и уже прошли этап экспериментов и наивного новаторства.
                          +4

                          Знатная солянка, узнал много нового, спасибо. В особенности monadic pipeline — ваще огонь )
                          А вот такое легаси я бы переписал на "длинно и некрасиво":


                          f.L("abc").map(ord).map(str).reversed().join("-")
                          '99-98-97'
                          
                          foo = [ord(x) for x in 'abc']
                          '-'.join(str(x) for x in foo[::-1])
                          '99-98-97'

                          Поинт в том, что если это есть в питоне, этого не надо объяснять новому человеку, а если он этого не знает, то обязан научиться. Ну и еще ваши типы и их методы могут быть сильно медленнее встроенных:


                          def test_f():
                              f.L("abc").map(ord).map(str).reversed().join("-")
                          
                          def test_p():
                              foo = [ord(x) for x in 'abc']
                              '-'.join(str(x) for x in foo[::-1])
                          
                          print(timeit.timeit(test_f, number=1000), timeit.timeit(test_p, number=1000))
                          0.0127332210541, 0.00602507591248

                          И еще я думал так тоже работать будет:


                          f.p_num(Decimal(10)), f.p_num('1')
                          False, False
                          
                          Decimal(10) + 4, '1'.isdigit()
                          Decimal('14'), True

                          Хотя, конечно, вы об этом не писали.

                            +2

                            Да, и спасибо за вклад в Open Source. Это важно и нужно.

                              +2

                              Спасибо за полезный отзыв.


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


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


                              Насчет предикатов — да, Decimal тоже нужно учесть. А строка не долна проходить проверку на число.

                              0
                              Чисто ради интереса, ваш код:
                              foo = [ord(x) for x in 'abc']
                              '-'.join(str(x) for x in foo[::-1])
                              '99-98-97'

                              , можете сказать, чем он лучше, например, такого:
                              '-'.join(map(str, [ord(x) for x in 'abc'[::-1]]))
                              '99-98-97'

                              или даже такого:
                              '-'.join(map(str, map(ord, 'abc'[::-1])))

                              Читаемость? Или есть ещё какой-то смысл разделить более сложное выражение с мапами на два с генераторами?

                              По скорости map выигрывает генераторы (на хабре статья была с измерениями, не найду уже), но если мой код в test_p2 и test_p3, то как-то так:

                              print(timeit.timeit(test_p, number=1000), timeit.timeit(test_p2, number=1000), timeit.timeit(test_p3, number=1000))
                              0.0027002159040421247 0.0017352891154587269 0.001657433109357953
                                0

                                Вы просто оптимизировали, а я небольшой фанат матрешек, вот без мап:


                                def test_map(): '-'.join(map(str, [ord(x) for x in 'abc'[::-1]]))
                                def test_comp(): '-'.join(str(ord(x)) for x in 'abc'[::-1])
                                print(timeit.timeit(test_map, number=1000), timeit.timeit(test_comp, number=1000))
                                (0.003298044204711914, 0.0029828548431396484)

                                У меня comprehension быстрее.

                                  0
                                  Если в test_map второй генератор тоже в map засунуть, то на моём железе отличие лишь в четвёртом знаке после запятой. В общем, на мой взгляд, ваш последний вариант наиболее ясен и элегантен.
                                    0

                                    Проморгал, что не тот вариант засунул.


                                    2.7.11:


                                    In [1]: import timeit
                                    In [2]: def test_map(): '-'.join(map(str, map(ord, 'abc'[::-1])))
                                    In [3]: def test_comp(): '-'.join(str(ord(x)) for x in 'abc'[::-1])
                                    In [4]: print(timeit.timeit(test_map, number=1000), timeit.timeit(test_comp, number=1000))
                                    (0.0022649765014648438, 0.0031321048736572266)

                                    3.5.1:


                                    In [1]: import timeit
                                    In [2]: def test_map(): '-'.join(map(str, map(ord, 'abc'[::-1])))
                                    In [3]: def test_comp(): '-'.join(str(ord(x)) for x in 'abc'[::-1])
                                    In [4]: print(timeit.timeit(test_map, number=1000), timeit.timeit(test_comp, number=1000))
                                    0.001268998021259904 0.001320684008533135

                                    Мап быстрее в обоих случаях.

                              0
                              Я что-то не врубился в pcall. Можете прояснить?
                              Заворачивать вызов в try с отловом четырех исключений означает сделать код абсолютно нечитаемым.
                              А зачем заворачивать в 4? заверните в один, и будет то же что и в pcall
                              try:
                              user = get_user(use_id)
                              except Exception as e:
                              # Do something

                              А если исключения все же нужно обрабатывать по разномо, то такой код помоему читабельней
                              try:
                              user = get_user(use_id)
                              except Exception1 as e:
                              # Do something
                              except Exception2 as e:
                              # Do something
                              except Exception3 as e:
                              # Do something

                              чем

                              err, user = pcall(get_user(use_id))
                              if isinstance(err, Exception1):
                              # Do something
                              elif isinstance(err, Exception2):
                              # Do something
                              elif isinstance(err, Exception3):
                              # Do something

                              Или не так я использую pcall?
                                0
                                не знаю почему форматирование слетело. возможно как-то связано с моим read-only статусом
                                  0

                                  преимущество в том, что не приходится таскать за собой блок try-catch, который вы рано или поздно забудете.

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

                                      нет, потому что кортеж вы не сможете обработать как плоский результат.

                                        +1
                                        Те кто забывают try-catch, особо не парясь будут использовать функции завернутые вашими декораторами в нижеприведенном стиле) И это гораздо хуже, чем если бы они просто забыли обработать исключение.

                                        @f.pcall_wraps
                                        def func(a, b):
                                            return a / b
                                        
                                        func(4, 2)[1]
                                        >>> 2
                                        

                                –2
                                Предлагаешь делать каждый раз
                                import f as fun
                                


                                Примеры накладок
                                def f(x):
                                    return x * x + 1
                                

                                f = open('lalala')
                                f.close()
                                


                                Да и python-pylint реагирует на такие имена, говорит «нужно прорефакторить имя, слишком короткое».

                                Написал бы программу чисто на ФП, чтобы можно было посмотреть на читаемость (на соответствие Zen'у).

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

                                  я этого не предлагал, где вы увидели?


                                  линтеры pylint и flake8 гибко настраиваются.


                                  мы уже похоронили второй питон

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

                                    0
                                    Я работал в компаниях, чей бизнес крутится исключительно на втором питоне.

                                    Да всем пофиг, где ты там работал. Тебе говорят про питон вообще. Ну, сдохнет твоя библиотека вместе со вторым питоном. Да она даже не оживёт, нечему подыхать будет.

                                    я этого не предлагал, где вы увидели?

                                    Ты занял имя, вместо того чтобы сделать, как все делают — уникальное имя, а дальше, кому надо, тот сократит до f.

                                    линтеры pylint и flake8 гибко настраиваются.

                                    Не, глупая идея. Предлагаешь из-за одного твоего модуля перенастраивать их и коснуться из-за этого всего остального. С какой стати? В том-то и дело, что ты выбрал имя неправильно.
                                      –1

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


                                      Библиотека работает на диапазоне версий 2.6 — 3.5. Вы, наверное, не заметили это в тексте. Комментировать остальное я не считаю нужным.

                                        +2
                                        Я ходил на PyPI, вот твоя ссылка https://pypi.python.org/pypi/f и там Python 2.7 выставлен. Не можешь заполнить информацию о пакете правильно? А зачем выкатил тогда?
                                  0
                                  Интересно посмотреть на альтернативное решение частых задач. Можно использовать как библиотеку «под рукой» — набросать скриптик в консоли, обработать пачку данных. Лаконичность конструкций в такой ситуации — большой плюс
                                    +1
                                    pcall, pcall_wraps
                                    Так принято в го, но не в питоне. В питоне принято пользоваться исключениями.
                                    Даже если вам нравится такой стиль, это не повод тащить его в питон: функции у вас в проекте будут вести себя неконсистентно — одни будут кидать исключения, а другие — возвращать ошибку.

                                    achain, ichain
                                    В питоне 3.6+ будет способ это делать нативно:
                                    None-aware operators — www.python.org/dev/peps/pep-0505

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

                                    Список и кортеж дополнительно реализуют .reversed, .sorted, .group, .distinct и .apply.
                                    Зачем? Это всё можно делать либо встроенными функциями, либо с помощью list/dict/set comprehensions, либо с помощью generator expressions.

                                      0
                                      pep-505 не факт, что примут, Гвидо не решил ничего)
                                      0
                                      Спасибо за библиотеку. Это набор функций, к которым всё равно так или иначе приходишь со временем, а у вас уже оформлено в пакет. Для себя утащил achain/ichain, comp и монады.

                                      Нельзя отвергать хорошие идеи и паттерны из других языков только потому, что они не соответствуют чувству прекрасного авторов Python.

                                      Only users with full accounts can post comments. Log in, please.