Pull to refresh

Comments 18

Основное, что я понял из этой статьи — изучение Хаскеля нешуточно выворачивает мозг.
Надо брать!
Я бы сказал, что изучение Хаскеля ставит мозг. По крайней мере, мне поставило.
Мне кажется, что инфиксабели красивее будет изобразить в виде слегка-монад.
Тоесть — саму инфиксную функцию сделать типа-монадической, с автомагическим поглащением не типа-монадических значений в цепочке.
(префиксы слегка- и типа- означают, что я пока не дорос до осознания граинц применения этих терминов)

Получается весьма красиво:
add = Infixable(lambda x,y: x+y)
sum = 1 |add| 2 |add| 3 # превращается в Infixable(1) | add | Infixable(2) | add | Infixable(3)
sum.val = 6

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

class Infixable(object):
    
    def __init__(self, arg):
        if callable(arg):
            self.fn = arg
        else:            
            self.val = arg

    def __repr__(self):
        if hasattr(self,'val'):
            return '<Inf=%s>'%repr(self.val)
        else:
            return "<Inf(%d)>"%self.fn.func_code.co_argcount

    def __or__(self, other):
        # self | other
        if not isinstance(other,Infixable):
            # self | noninf
            other = Infixable(other)
            
        logging.debug("%s + %s",repr(self),repr(other))
        if hasattr(self,'val') and hasattr(other,'fn'):
            # val | fn -> fn=curry            
            return Infixable(lambda x: other.fn(self.val,x))
        elif hasattr(self,'fn') and hasattr(other,'val'):
            # fn | val -> val=fn(val)
            return Infixable(self.fn(other.val))
        else:            
            raise ValueError("bogus operands in chain")
        
    def __ror__(self,other):
        # noninf | self
        return Infixable(other) | self
Можно и так. Но перегрузка операторов имеет одно неприятное свойство: не всегда прогнозируемый порядок вызова, а это как раз минус: в монадном контексте порядок вычисления должен быть строго последовательным.
Я тоже сначала сделал перегрузку >>, |, *, и вот тут то меня и подвел приоритет операций. В вашем варианте перегружен только пайп, но даже он уже может быть как лево-, так и правосторонним.
Я тут подумал, что пока пишешь сам и для себя — это, вероятно, всё очень интересно. Правда на выходе получаем какого-то уродца с нешуточной магией внутри. Мне интересно какая была бы реакция у моих коллег, увидев бы они такое… вот покажу им эту статью на днях, обсудим.
По крайней мере поведение функтора можно реализовать и использовать — он вполне pythonic. Это просто метод контейнерного класса, позволяющий применить простую функцию к данным внутри контейнера. Например, аналог map, но для дерева.
Что-то мне подсказывает, в реальном мире не существует ни одной реальной задачи, где бы подобная магия была бы уместна. Разве что, как экзотическая зарядка для ума…
Это только одна монада из многих, а они очень разные.
Самые разные монады напрямую используются в функциональных языках. Некоторые реализованы и для императивных языков.

Если объект имеет метод, который возвращает сам объект, мы можем писать так:
some_obj.set_name("abc").inrease_age().update_items([1,2,3]).validate().save()

Это тоже частный случай монады, просто он более привычен людям, привыкшим к ООП.
> Если объект имеет метод, который возвращает сам объект, мы можем писать так:
> some_obj.set_name(«abc»).inrease_age().update_items([1,2,3]).validate().save()
>
> Это тоже частный случай монады, просто он более привычен людям, привыкшим к ООП.

Ох, ну классно! У нас, оказывается, повсюду монады :)

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

не как могу сообразить, что к чему в примере с конями. в строчке:
List(pos) +'>>='+ raw_jumps +'>>='+ if_valid
функция raw_jumps используется совместно с оператором '>==' из монады List, значит она должна быть монадической.

по определению, функция raw_jumps должна принимать обычное значение координат, и возвращать результат в контексте (т.е. List(result)). но если посмотреть код выше, то функция определена иначе:
raw_jumps  = lambda (x, y): [ ... ]
т.е. принимая координаты, функция возвращает объект встроенного типа list, который к монаде List не имеет отношения. или имеет? в чем магия?
В целом, Вы правы — нужно бы возвращать объект класса List. Но данная реализация монад несколько своеобразна. Настоящее монадное значение — immutable. И после каждого >>= или >> мы получаем новое значение. Но конкретная реализация монад — это последовательность изменений первого контейнера в цепочке, что можно увидеть по коду методов applicate/bind/then. Это волевое решение: мне, в данном случае, показалась более удобной именно такая реализация — всё равно от нового монадного значения нужно только лишь содержимое. Начальное же значение упаковывается в экземпляр List только для реализации поведения, если бы можно было дополнить класс списка, я, безусловно, бы так и сделал. Таким образом монадные функции возвращают простой список (так их проще писать), но думать о нем нужно, как о монадном контексте.
Maybe-значение, по своей сути, тоже может быть представлено в виде простого кортежа (Bool, x). Можно было бы переписать just(), чтобы он возвращал просто кортеж. Тогда Maybe-последовательность нужно было бы начинать вручную с Maybe(just=x).

Кстати, в Haskell, в котором я и черпал вдохновение, список уже имеет монадное поведение, поэтому там монадные функции всегда возвращают список.

Спасибо большое Вам за то, что обратили внимание на несоответствие описания и кода. Можно переписать методы класса List, чтобы они принимали объекты класса List, а монадные функции их возвращали.
Поправил. Теперь, вроде бы противоречий нет.
я как раз из тех, кто много слышал про монады, но так и ничего не понял. :) спасибо за ваши статьи, создаётся ощущение, что вот оно, прозрение, уже совсем близко. отметил, что теперь примеры, чисто внешне, стали напоминать код для jQuery, где функции должны всегда возвращать этот чудо-объект. :)

говорят, что достоинства или недостатки той или иной архитектуры определяются тем, насколько хорошо в ней локализуются изменения при изменении требований. как вы считаете, как много придётся менять в примере с птицами, чтобы сообщать причину и место падения пользователю явно (которые в примерах указаны комментариями)?

в классическом ООП я бы решал эту задачу при помощи специализации «управляющей коробки» (class TightropeWalkerProblem(Maybe): ... ). допустимо-ли подобное для монад? похоже, что изменение монады, каскадом, заставит изменять и все монадические функции, которые с ней связаны — возможно, лишь для того, чтобы они стали возвращать монадические значения нового типа. скорее всего, это не оптимальное решение, а правильное от меня ускользает.
Можно сделать атрибут nothing монады Maybe не булевым, а строковым и поменять шорткат с nothing() на nothing(text), тогда любая монадная функция сможет возвращать текст ошибки.
кажется это нам не даст возможности указать на конкретную инструкцию, как в примере
show(
    begin()
    +'>>='+ to_left(2)
    +'>>='+ to_right(5)
    +'>>='+ to_left(-2) # канатоходец упадёт тут
)
Обычно, если и требуется только указать причину ошибки, то её одной и достаточно.
Но можно сделать и так:
#coding:utf-8

class Functor(object):
    def fmap(self, func):
        raise NotImplementedError()

class Applicative(Functor):
    def applicate(self, monad_value):
        raise NotImplementedError()

class Monad(Applicative):

    def bind(self, monad_func):
        raise NotImplementedError()

    def then(self, monad_func):
        raise NotImplementedError()

    @property
    def result(self):
        raise NotImplementedError()

#------------------------------------------------------------------
class Maybe(Monad):

    @classmethod
    def just(cls, x):
        return cls(just=x)

    @classmethod
    def nothing(cls, msg=''):
        return cls(nothing=True, just=msg)

    @classmethod
    def _from_monad_value(cls, monad_value):
        assert isinstance(monad_value, Maybe)
        nothing, value = monad_value.result
        if nothing:
            return cls.nothing(value)
        return cls.just(value)


    def __init__(self, just=None, nothing=False):
        self._just = just
        self._nothing = nothing


    def fmap(self, func):
        if self._nothing:
            return self.nothing(msg=self._just)
        return self.just(func(self._just))


    def applicate(self, monad_value):
        if self._nothing:
            return self.nothing(msg=self._just)
        nothing, val = monad_value.result
        if nothing:
            return self.nothing(msg=val)
        return self.just(self._just(val))


    def bind(self, monad_func):
        if self._nothing:
            return self.nothing(msg=self._just)
        return self._from_monad_value(
            monad_func(self._just))


    def then(self, monad_func):
        return self._from_monad_value(monad_func())


    @property
    def result(self):
        return (self._nothing, self._just)


just = lambda x: Maybe.just(x)
nothing = lambda msg='': Maybe.nothing(msg)

liftMaybe = lambda fn: lambda x: just(fn(x))

justLoggable = lambda x: Maybe.just((x, []))

def loggable(fn):
    def inner(value):
        value, log = value
        log.append('%s\t%s' % (fn.__doc__, repr(value)))
        new_nothing, value = fn(value).result
        if new_nothing:
            if value:
                log.append(value)
            return nothing('\n'.join(log))
        return just((value, log))
    return inner

#------------------------------------------------------------------
if __name__ == '__main__':

    def _newPole( (l, r) ):
        diff = l - r
        if diff > 3:
            return nothing(u'Слишком много птиц слева!')
        elif diff < -3:
            return nothing(u'Слишком много птиц справа!')
        return just( (l, r) )

    def seatLeft(x):
        @loggable
        def inner((l, r)):
            u'''Налево'''
            return _newPole( (l + x, r) )
        return inner

    def seatRight(x):
        @loggable
        def inner((l, r)):
            u'''Направо'''
            return _newPole( (l, r + x) )
        return inner

    banana = lambda x: nothing(u'Ой, корка!')
    banana.__doc__ = u'''Корка под ноги'''
    banana = loggable(banana)

    nothing, val = reduce(
        lambda m, v: m.bind(v), # свёртка посредством bind
        [
            seatLeft(2),
            seatRight(4),
            seatLeft(-1),
            banana
        ],
        justLoggable( (0, 0) ) # начальное значение
    ).result

    if nothing:
        print val
    else:
        print 'OK!'

Для ведения лога нужно начинать последовательность с justLoggable и все монадные функции должны быть декарированы посредством @loggable (или сами обрабатывать лог).
Написал новую версию. Описание изменений и ссылка в топике есть. Она непротиворечива (надеюсь), более соответствует принципам ООП.
Sign up to leave a comment.

Articles