Welcome, guest | Sign In | My Account | Store | Cart

Decorator makes function currying as long as there are more correct arguments to take and fires it as soon as there is enough to call, also checks arguments up front for errors.

Python, 97 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
from inspect import getcallargs, getargspec, ismethod
from functools import wraps

class KeywordRepeatedTypeError(TypeError): pass

def insert_keyword_first(fun, a, k): # based on recipe: 577922
    a = list(a)
    for idx, arg in enumerate(getargspec(fun).args, -ismethod(fun)): # or [0] in 2.5
        if arg in k:
            if idx < len(a):
                a.insert(idx, k.pop(arg))
            else:
                break
    return (a, k)

def currying(f):
    def collector(*a, **k):
        @wraps(f)
        def caller(*args, **kwargs):
            try:
                for key in kwargs:
                    if key in k:
                        raise KeywordRepeatedTypeError(
                            "TypeError: %s() got multiple values "
                            "for keyword argument '%s'" % (f.func_name, key))
                kwargs.update(k)
                args, kwargs = insert_keyword_first(f, a + args, kwargs)
                getcallargs(f, *args, **kwargs)
            except KeywordRepeatedTypeError, e:
                raise TypeError(e)
            except TypeError:
                spec = getargspec(f)
                if not spec.keywords:
                    for key in kwargs:
                        if key not in spec.args:
                            raise
                if len(spec.args) > len(args) + len(set(spec.args) & set(kwargs)):
                    return collector(*args, **kwargs)
                raise
            else:
                return f(*args, **kwargs)
        return caller
    return collector()

if __name__ == "__main__":
    import unittest
    class Test(unittest.TestCase):
        def test_args(self):
            
            @currying
            def function(a, b, c, d, e, f, *ar, **kw): return (a, b, c, d, e, f, ar, kw)
            print function(2, e=5)(4)(x=100, y=1000)(c=3, a=1)(6, 7, 8, z=10000)
            #returns (1, 2, 3, 4, 5, 6, (7, 8), {'y': 1000, 'x': 100, 'z': 10000})

            @currying
            def args(a, b, c): return (a, b, c)
            self.assertEquals(args()()(a=1)()(2, 3), (1, 2, 3))
            with self.assertRaises(TypeError): args(d=4)
       
            f = args(1)
            for x in range(1, 6):
                ff = f(x)
                for y in range(10, 101, 30):
                    print ff(y)

            @currying
            def default_args(a=10, b=20, c=30): return (a, b, c) #cannot curry but works
            self.assertEquals(default_args(1, c=3), (1, 20, 3))
    
            @currying
            def varargs_keywords(*a, **k): 
                '''function like that canonot be curried
                because it always eats up all arguments on the first call, but will work anyway...'''
                return a, k
       
            @currying
            def args_varargs(a, b, c, *ar): return a, b, c, ar
            self.assertEquals(args_varargs(1, c=3)(2, 4), (1, 2, 3, (4,)))
            with self.assertRaises(TypeError): args_varargs(d=4)
    
            @currying
            def args_keywords(a, b, c, **k): return a, b, c, k
            self.assertEquals(args_keywords(d=4)(1, 3)(b=2), (1, 2, 3, {'d':4}))
            with self.assertRaises(TypeError): args_keywords(c=3)(4, b=2, a=1)

            @currying
            def args_varargs_keywords(a, b, c, *ar, **k): return a, b, c, ar, k
            self.assertEquals(args_varargs_keywords(1, 2)(3, d=4), (1, 2, 3, (), {'d':4}))
            self.assertEquals(args_varargs_keywords(d=6)(2,a=1)(4,5, c=3), (1,2,3,(4,5),{'d':6}))
            with self.assertRaises(TypeError): args_varargs_keywords(a=1)(a=2)(3, 4)
       
            class ObjectMethod(object):
                @currying
                def metdhod(self, a, b): return a, b
            self.assertEquals(ObjectMethod().metdhod(2)(a=1), (1,2))
            self.assertEquals(ObjectMethod().metdhod(a=1)(2), (1,2))   
    unittest.main()

   
   

It's a greedy call so it will fire function as soon as there are enough arguments to do that (unlike partial that always waits for second call no matter what arguments you pass; correct or wrong), because of that e.g. functions that takes f(a, *k) cannot be curried with this because they always called correctly and returns result right away. Other functions with more Haskellish parameters should behave as expected. Also there is pretty good arguments checking so e.g. if you pass same keyword argument twice in different calls or pass keyword at some point that doesn't exist it'll rise type error right away, not waiting for final function call.

Unfortunately it started to work with **kwargs with Python 2.7.2 as there was a bug in getcallargs when passing dictionary arguments and I cannot call function instead of getcallargs because I don't want to catch user defined TypeErrors. Och and it won't work on built-ins of course. :)

Not sure if there is any real, practical use for it, but it looked interesting, if you find any, let me know, I'm curious myself :D