Function composition and inverse with Python

·

9 min read

Yesterday I wanted to implement function composition and inverse.

Screenshot_2020-10-08 MathJax.png

The first approach with classes was this (check how to use it within the tests in classes Plus2 and Times5):

class ComposableInversible:
    def op(self, *a, **kw):
        pass
    def rop(self, *a, **kw):
        pass
    def __invert__(f):
        class inverse_f(ComposableInversible):
            def op(self, *a, **kw):
                return f.rop(*a, **kw)
            def rop(self, *a, **kw):
                return f.op(*a, **kw)
        return inverse_f()
    def __or__(outer, inner):
        class composed_f(ComposableInversible):
            def op(self, *a, **kw):
                return outer.op(inner.op(*a, **kw))
            def rop(self, *a, **kw):
                return inner.rop(outer.rop(*a, **kw))
        return composed_f()

# and a basic test I'd reused:

def InversibleTest(Klass):
    class _InversibleTest(unittest.TestCase):
        class Plus2(Klass):
            def op(self, a):
                return a + 2
            def rop(self, a):
                return a - 2
        class Times5(Klass):
            def op(self, a):
                return a * 5
            def rop(self, a):
                return a / 5
        plus2 = Plus2()
        times5 = Times5()

        def testBasicCase(self):
            self.assertEqual(1, (~self.plus2).op(3) )
            self.assertEqual(4, (~self.times5).op(20))

        def testAFewCases(self):
            for x in range(50):
                self.assertEqual(
                    (~(self.plus2 | self.times5)).op(x),
                    ((~self.times5) | (~self.plus2)).op(x))

        def testAFewCases2(self):
            f = self.plus2 | self.times5
            for x in range(0,50):
                self.assertEqual(x, (~f | f).op(x))

        def testAFewCases3(self):
            f = self.times5 | self.plus2
            for x in range(0,50):
                self.assertEqual(x, (~f | f).op(x))

    return _InversibleTest

ComposableInversibleTest = InversibleTest(ComposableInversible)

After that I wanted to eliminate the .op(..) method definition which makes this more obscure, so decided to replace it with call() :

class ComposableInversible2:
    __inverse__ = None
    __call__ = None

    def __invert__(f):
        class inverse_f(ComposableInversible2):
            def __call__(self, x):
                return f.__inverse__(x)
            def __inverse__(self, x):
                return f(x)
        return inverse_f()

    def __or__(outer, inner):
        class composed(ComposableInversible2):
            def __call__(self, x):
                return outer(inner(x))
            def __inverse__(self, x):
                return (~inner)( (~outer)(x))
        return composed()

and then:

class ComposableInversible3:
    __inverse__ = None
    __call__ = None

    def __invert__(f):
        class inverse_f(ComposableInversible3):
            __call__ = f.__inverse__
            __inverse__ = f.__call__
        return inverse_f()

    def __or__(outer, inner):
        class composed(ComposableInversible3):
            __call__ = lambda _, x: outer(inner(x))
            __inverse__ = lambda _, x: (~inner)((~outer)(x))
        return composed()

# and the tests (also adapted from the one above):

def makest(klass):
    class _thetestklass(unittest.TestCase):
        class Plus2(klass):
            __call__ = lambda self, x: x+2
            __inverse__ = lambda self, x: x-2
        class Times5(klass):
            __call__ = lambda self, x: x * 5
            __inverse__ = lambda self, x: x / 5
        plus2 = Plus2()
        times5 = Times5()

        def testBasicCase(self):
            self.assertEqual(1, (~self.plus2)(3) )
            self.assertEqual(4, (~self.times5)(20))

        def testAFewCases(self):
            for x in range(50):
                self.assertEqual(
                    (~(self.plus2 | self.times5))(x),
                    ((~self.times5) | (~self.plus2))(x))

        def testAFewCases2(self):
            f = self.plus2 | self.times5
            for x in range(0,50):
                self.assertEqual(x, (~f | f)(x))

        def testAFewCases3(self):
            f = self.times5 | self.plus2
            for x in range(0,50):
                self.assertEqual(x, (~f | f)(x))
    return _thetestklass

ComposableInversible3Test = makest(ComposableInversible3)

Finally, I started to think that the class-thing makes too much code for something simple and tried a more functional approach..

def F(f,i):
    class Klass:
        __inverse__ = lambda x: F(i, f)
        __call__ = staticmethod(f)
        def __invert__(f):
            return f.__inverse__()
        def __or__(outer, inner):
            class composed(Klass):
                __call__ = lambda _, x: outer(inner(x))
                class __inverse__(Klass):
                    __call__ = lambda _, x: ((~inner)|(~outer))(x)
            return composed()
    return Klass()

# a few tests:

class TestF(unittest.TestCase):
    plus2 = F(lambda x:x+2, lambda x:x-2)
    times5 = F(lambda x:x*5, lambda x:x/5)

    def testBasicCase(self):
        self.assertEqual(1, (~self.plus2)(3))
        self.assertEqual(4, (~self.times5)(20))

    def testAFewCases(self):
        for x in range(50):
            self.assertEqual(
                    (~(self.plus2 | self.times5))(x),
                    ((~self.times5) | (~self.plus2))(x))

    def testAFewCases2(self):
        f = self.plus2 | self.times5
        for x in range(0, 50):
            self.assertEqual(x, (~f | f)(x))

    def testAFewCases3(self):
        f = self.times5 | self.plus2
        for x in range(0, 50):
            self.assertEqual(x, (~f | f)(x))

Also I think it would be better something returning both function object and inverse object at once.. but shouldn't be difficult to get that from code above..