Adventures in Currying

Nick Mathewson nickm at mit.edu
Sun May 14 15:47:09 EDT 2000


I've noticed that about 2/3 of the times I use lambda in Python, I'm
really just asking for curried functions.[*] With this in mind, I
started writing up currying operators of various degrees of
complexity.  I give them below, in their approximate evolutionary
order.  They are all classes designed to turn python functions into
curried functions.

They're all tested with 1.5.2, but rely on undocumented attributes of
function and code objects.  Therefore, they may break on 1.6, and are
certain to break in JPython.

[*] For more information on currying, look on deja.com for a thread
    called 'In defence of the two-namespace rule'.

=================================================================
from types import *

if __name__ == '__main__':
    # This is our usual example.
    def parrot(person, thing, act, stuff, place):
	print "Listen, %s! This %s has ceased to %s!" % (person, thing, act)
	print "Bereft of %s, it rests in %s!" % (stuff,place)
	
    def fermat(thing1,thing2,act='contain it'):
	print "I have discovered the most wonderful %s for this," %thing1
	print "but the %s is not large enough to %s." %(thing2,act)

class Curry1:
    """Simplest version. 

       Doesn't understand about keyword or default arguments.  
       Forces you to fill in all the arguments at once."""
    def __init__(self, fn, *args):
	self.fn = fn
	self.args = args

    def __call__(self, *args):
	return apply(self.fn,self.args + args)

if __name__  == '__main__':
    print "===1"
    Curry1(parrot, "matey", "parrot", "be")("life", "peace")
    print
    Curry1(Curry1(parrot, "matey"), "parrot", "be")("life", "peace")
    print

class Curry2:
    """Slightly more complex, and closer to real currying. 
       Still doesn't understand about keyword arguments."""
    def __init__(self, fn, *args):
	if isinstance(fn, Curry2):
	    self.fn = fn.fn
	    self.n_args = fn.n_args
	    self.args = fn.args + args[0]
	    return

	if type(fn) == FunctionType:
	    code = fn.func_code
	    self.n_args = code.co_argcount
	elif type(fn) ==  MethodType:
	    code = fn.im_func.func_code
	    self.n_args = code.co_argcount
	else:
	    assert None
	
	self.fn = fn
	self.args = args

    def __call__(self, *args):
	if len(args) + len(self.args) >= self.n_args:
	    return apply(self.fn, self.args + args)
	else:
	    return Curry2(self, args)

if __name__  == '__main__':
    print "===2"
    Curry2(parrot, "buddy")("python", "slither")("scales")("Amsterdam")
    print

class Curry3:
    """Like Curry2, but more complex. Understands keyword arguments. Baffled
       by default arguments.  Doesn't work on builtins."""
    def __init__(self, fn, *args, **kwargs):
	if isinstance(fn, Curry3):
	    self.fn = fn.fn
	    self.argnames = fn.argnames
	    self.kwargs = fn.kwargs.copy()
	else:
	    self.fn = fn
	    self.kwargs = {}
	    self.argnames = _argNames(fn)

	if kwargs:
	    self.kwargs.update(kwargs)

	argnames = self.argnames
	for i in range(len(args)):
	    self.kwargs[argnames[i]] = args[i]
	
	argnames2 = []
	for name in argnames[len(args):]:
	    if not self.kwargs.has_key(name):
		argnames2.append(name)

	self.argnames = argnames2

    def __call__(self, *args, **kwargs):
	more = apply(Curry3, (self,)+ args, kwargs)
	
	if len(more.argnames) == 0:
	    return apply(more.fn, (), more.kwargs)
	else:
	    return more

def _argNames(fn):
    if getattr(fn,'argnames',None) is not None:
	return fn.argnames
    elif type(fn) == FunctionType:
	code = fn.func_code
	return code.co_varnames[:code.co_argcount]
    elif type(fn) == MethodType:
	code = fn.im_func.func_code
	return code.co_varnames[1:code.co_argcount]
    else: 
	assert None

if __name__ == '__main__':
    print "===3"
    p = Curry3(parrot, 'matey', act='be')
    p('parrot','life')('peace')
    print
    p('python','scales')(place='peace')
    print
    p('fish',act='flop')('water',person='Jacques')

class Curry4:
    """Like Curry3, but more complex. Understands default arguments.
       Doesn't work on builtins.

       Can anybody make this less inefficient?"""

    def __init__(self, fn, *args, **kwargs):
	if isinstance(fn, Curry4):
	    self.fn = fn.fn
	    self.argnames = fn.argnames
	    self.kwargs = fn.kwargs.copy()
	else:
	    self.fn = fn
	    self.argnames = _argNames(fn)
	    self.kwargs = _defaultArgDict(fn,self.argnames)

	if kwargs:
	    self.kwargs.update(kwargs)

	argnames = self.argnames
	for i in range(len(args)):
	    self.kwargs[argnames[i]] = args[i]
	
	argnames2 = []
	for name in argnames[len(args):]:
	    if not self.kwargs.has_key(name):
		argnames2.append(name)

	self.argnames = argnames2

    def __call__(self, *args, **kwargs):
	more = apply(Curry4, (self,)+ args, kwargs)
	
	if len(more.argnames) == 0:
	    return apply(more.fn, (), more.kwargs)
	else:
	    return more

def _defaultArgDict(fn,argnames):
    if type(fn) == MethodType:
	fn = fn.im_func
    d = {}
    defs = fn.func_defaults
    for i in range(-1,-len(defs)-1,-1):
	d[argnames[i]] = defs[i]
	
    return d

if __name__ == '__main__':
    print "===4"
    Curry4(fermat)('proof')('margin')
    print
    Curry4(fermat)(thing2='available memory')('syntax',act='parse it')

=================================================================

Yes-I-Know-It-Should-Be-Called-Schoenfinkeling'ly yours,

-- 
Nick Mathewson     <nickm at mit.edu>     http://www.mit.edu/~nickm/



More information about the Python-list mailing list