Error Monade (oder wie man sein Hirn fritiert)

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Benutzeravatar
bwbg
User
Beiträge: 407
Registriert: Mittwoch 23. Januar 2008, 13:35

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
"Du bist der Messias! Und ich muss es wissen, denn ich bin schon einigen gefolgt!"
Benutzeravatar
pillmuncher
User
Beiträge: 1482
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

@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)

    [...]
In specifications, Murphy's Law supersedes Ohm's.
Benutzeravatar
pillmuncher
User
Beiträge: 1482
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

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)

    [...]
In specifications, Murphy's Law supersedes Ohm's.
Benutzeravatar
pillmuncher
User
Beiträge: 1482
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

Unter class ErrorM(...): fehlt ein __slots__ = ().
In specifications, Murphy's Law supersedes Ohm's.
Benutzeravatar
bwbg
User
Beiträge: 407
Registriert: Mittwoch 23. Januar 2008, 13:35

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.
"Du bist der Messias! Und ich muss es wissen, denn ich bin schon einigen gefolgt!"
Benutzeravatar
pillmuncher
User
Beiträge: 1482
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

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.
In specifications, Murphy's Law supersedes Ohm's.
Benutzeravatar
bwbg
User
Beiträge: 407
Registriert: Mittwoch 23. Januar 2008, 13:35

Und ich alter Diktator wollte sicher gehen, dass sie genau das bleiben .. eine win-win-Situation ;)
"Du bist der Messias! Und ich muss es wissen, denn ich bin schon einigen gefolgt!"
Antworten