Seite 1 von 1

Error Monade (oder wie man sein Hirn fritiert)

Verfasst: Donnerstag 25. September 2014, 15:02
von bwbg
Ich versuche mich (im n-ten Anlauf) daran, Monaden zu verstehen. So habe ich nun versucht die Monad Laws aus dem Haskell-Wiki in unittests zu übersetzen und eine Error-Monade zu entwicklen, um so ein besseres Verständnis für dieses "Design-Pattern" zu bekommen.

Die Tests laufen durch und die Klasse tut, was soll. Das heißt aber nichts, diese könnten ja falsch sein bzw. mein Verständnis fehlgeleitet.

Es wäre schön, wenn ihr den folgenden Quelltext einfach mal auseinander nähmet.

Code: Alles auswählen

#!/usr/bin/env python3

import collections
import unittest

class Error(collections.namedtuple('_Error', ['value', 'error'])):
    
    @classmethod
    def success(cls, x):
        return cls.__new__(cls, x, None)
        
    @classmethod
    def fail(cls, why):
        return cls.__new__(cls, None, why)
        
    @classmethod
    def unit(cls, x):
        return cls.success(x)
    
    @classmethod
    def bind(cls, mx, mf):
        if mx.error is not None: return mx
        return mf(mx.value)

    def __rshift__(self, mf):
        """API-sugar for the bind-function."""
        return self.__class__.bind(self, mf)


def madd_four(x):
    return Error.unit(x + 4)
    
def mmul_three(x):
    return Error.unit(x * 3)

     
class TestErrorMonadLaws(unittest.TestCase):
    
    def test_left_identity_success(self):
        x = 5
        m = Error.success(x) # success == unit
        f = madd_four
        assert m >> f == f(x)
        
    def test_right_identity_success(self):
        m = Error.success(-2) # success == unit
        assert m >> Error.unit == m
        
    def test_associativity_success(self):
        m = Error.success(42) # success == unit
        f = madd_four; g = mmul_three
        assert (m >> f) >> g == m >> (lambda x: f(x) >> g)

    def test_left_identity_fail(self):
        m = Error.fail('Something went wrong!')
        f = madd_four
        assert m >> f == m # f(x) won't match as it won't fail.
        
    def test_right_identity_fail(self):
        m = Error.fail('Something went wrong!')
        assert m >> Error.unit == m
        
    def test_associativity_fail(self):
        m = Error.fail('Something went wrong!')
        f = madd_four; g = mmul_three
        assert (m >> f) >> g == m >> (lambda x: f(x) >> g)

if __name__ == '__main__':
    unittest.main()
Grüße ... bwbg

Re: Error Monade (oder wie man sein Hirn fritiert)

Verfasst: Donnerstag 25. September 2014, 17:40
von pillmuncher
@bwbg: Ja, das stimmt im Prinzip schon so. Man könnte es noch erweitern/vereinfachen, aber inwieweit man da gehen mag ist Geschmackssache:

Code: Alles auswählen

class Error(collections.namedtuple('_Error', ['value', 'error'])):

    @classmethod
    def success(cls, x):
        return cls(x, None)

    @classmethod
    def fail(cls, why):
        return cls(None, why)

    unit = success

    def bind(self, mf):
        if self.error is not None: return self
        return mf(self.value)

    __rshift__ = bind  # API-sugar for the bind-function

    @classmethod
    def lift(cls, func):
        @functools.wraps(func)
        def mfunc(value):
            try:
                result = func(value)
            except Exception as err:
                return cls.fail(str(err))
            else:
                return cls.success(result)
        return mfunc

[...]

@Error.lift
def madd_five(x):
    return x + 5

@Error.lift
def mdiv_10_by(x):
    return 10 / x


class TestErrorMonadLaws(unittest.TestCase):

    def test_lift(self):
        assert Error.unit(3) >> madd_five == Error.unit(8)
        assert Error.unit(2) >> mdiv_10_by == Error.unit(5)
        msg = 'integer division or modulo by zero'
        assert Error.unit(0) >> mdiv_10_by == Error.fail(msg)

    [...]
Man könnte es auch so machen:

Code: Alles auswählen

import collections
import functools
import unittest


class ErrorM(collections.namedtuple('_ErrorM', 'value')):

    def __eq__(self, other):
        return type(self) == type(other) and self.value == other.value

    def __ne__(self, other):
        return not self == other

    def __rshift__(self, mf):
        return self.bind(mf)

    @staticmethod
    def lift(func):
        @functools.wraps(func)
        def mfunc(value):
            try:
                result = func(value)
            except Exception as err:
                return Failure(str(err))
            else:
                return Success(result)
        return mfunc


class Success(ErrorM):

    __slots__ = ()

    def bind(self, mf):
        return mf(self.value)


class Failure(ErrorM):

    __slots__ = ()

    def bind(self, mf):
        return self


ErrorM.unit = Success
ErrorM.fail = Failure
ErrorM.success = Success

[...]

class TestErrorMonadLaws(unittest.TestCase):

    def test_lift(self):
        assert ErrorM.unit(3) >> madd_five == Success(8)
        assert ErrorM.unit(2) >> mdiv_10_by == Success(5)
        msg = 'integer division or modulo by zero'
        assert ErrorM.unit(0) >> mdiv_10_by == Failure(msg)

    def test_eq(self):
        assert Success(0) == Success(0)
        assert Failure(0) == Failure(0)
        assert Success(0) != Failure(0)

    [...]

Re: Error Monade (oder wie man sein Hirn fritiert)

Verfasst: Donnerstag 25. September 2014, 18:06
von pillmuncher
mcompose() fehlt auch noch:

Code: Alles auswählen

compose = lambda f, g: lambda *a, **k: g(f(*a, **k))

class ErrorM(collections.namedtuple('_ErrorM', 'value')):

    @staticmethod
    def mcompose(*mfuncs):
        return compose(
            ErrorM.unit,
            functools.partial(reduce, ErrorM.__rshift__, mfuncs))

    [...]

class TestErrorMonadLaws(unittest.TestCase):

    def test_mcompose(self):
        from itertools import product
        funcs = madd_four, mmul_three, madd_five, mdiv_10_by
        for fs in product(funcs, repeat=3):
            mf = ErrorM.mcompose(*fs)
            def mg(x):
                mx = ErrorM.unit(x)
                for f in fs:
                    mx >>= f
                return mx
            for n in range(5):
                assert mf(n) == mg(n)

    [...]

Re: Error Monade (oder wie man sein Hirn fritiert)

Verfasst: Freitag 26. September 2014, 12:51
von pillmuncher
Unter class ErrorM(...): fehlt ein __slots__ = ().

Re: Error Monade (oder wie man sein Hirn fritiert)

Verfasst: Freitag 26. September 2014, 22:32
von bwbg
Ich habe das ganze jetzt mal zusammen gefasst und in ein gist gepackt:

https://gist.github.com/bwbg/c844de3b8a19a75d763f

@pillmuncher: Auf die Kombination von partial und reduce wäre ich selbst wohl nie gekommen. mcompose hatte ich bereits mit einer Schleife gelöst. So ist es aber schöner.

Ich überlege jetzt noch an einem überschaubaren aber nicht zu simplen Beispiel für die Error-Monade.

Re: Error Monade (oder wie man sein Hirn fritiert)

Verfasst: Freitag 26. September 2014, 22:53
von pillmuncher
Ich hätte es dazuschreiben sollen, dass ich __slots__ nicht verwendet habe, damit keine Attribute hinzugefügt werden können. Mit __slots__ wird verhindert, dass pro Instanz ein __dict__ angelegt wird und damit braucht ein ErrorM Objekt (und Objekte davon abgeleiteter Klassen, sofern diese ebenfalls __slots__ definieren) nur genausoviel Speicher wie ein ein-elementiges Tupel, da sie ja - als namedtuple - genau das sind.

Re: Error Monade (oder wie man sein Hirn fritiert)

Verfasst: Freitag 26. September 2014, 23:24
von bwbg
Und ich alter Diktator wollte sicher gehen, dass sie genau das bleiben .. eine win-win-Situation ;)