Index: Modules/functionalmodule.c =================================================================== --- Modules/functionalmodule.c (revision 42219) +++ Modules/functionalmodule.c (working copy) @@ -2,10 +2,13 @@ #include "Python.h" #include "structmember.h" -/* Functional module written and maintained - by Hye-Shik Chang +/* partial type by Hye-Shik Chang with adaptations by Raymond Hettinger - Copyright (c) 2004, 2005 Python Software Foundation. + + foldr, foldl, scanr, scanl, id, the compose type, the flip type + by Collin Winter + + Copyright (c) 2004-2006 Python Software Foundation. All rights reserved. */ @@ -191,7 +194,7 @@ return 0; } -static PyGetSetDef partail_getsetlist[] = { +static PyGetSetDef partial_getsetlist[] = { {"__dict__", (getter)partial_get_dict, (setter)partial_set_dict}, {NULL} /* Sentinel */ }; @@ -229,7 +232,7 @@ 0, /* tp_iternext */ 0, /* tp_methods */ partial_memberlist, /* tp_members */ - partail_getsetlist, /* tp_getset */ + partial_getsetlist, /* tp_getset */ 0, /* tp_base */ 0, /* tp_dict */ 0, /* tp_descr_get */ @@ -241,13 +244,604 @@ PyObject_GC_Del, /* tp_free */ }; +/* foldl ********************************************************************/ +static PyObject * +functional_foldl(PyObject *self, PyObject *args) +{ + PyObject *func, *start, *seq, *iter, *result, *func_args, *item; + + if (!PyArg_UnpackTuple(args, "foldl", 3, 3, &func, &start, &seq)) { + return NULL; + } + Py_INCREF(start); + + iter = PyObject_GetIter(seq); + if (iter == NULL) { + PyErr_SetString(PyExc_TypeError, + "argument 3 to foldl() must support iteration"); + Py_DECREF(start); + return NULL; + } + + result = start; + + while ((item = PyIter_Next(iter))) { + func_args = PyTuple_New(2); + PyTuple_SET_ITEM(func_args, 0, result); + PyTuple_SET_ITEM(func_args, 1, item); + + result = PyObject_CallObject(func, func_args); + Py_DECREF(func_args); + if (NULL == result) { + goto Fail; + } + } + if (PyErr_Occurred()) { + Py_DECREF(start); + goto Fail; + } + goto Succeed; + +Fail: + Py_CLEAR(result); +Succeed: + Py_DECREF(iter); + return result; +} + +PyDoc_STRVAR(foldl_doc, +"foldl(function, start, iterable) -> object\n\ +\n\ +Takes a binary function, a starting value (usually some kind of 'zero'), and\n\ +an iterable. The function is applied to the starting value and the first\n\ +element of the list, then the result of that and the second element of the\n\ +list, then the result of that and the third element of the list, and so on.\n\ +\n\ +foldl(add, 0, [1, 2, 3]) is equivalent to add(add(add(0, 1), 2), 3)"); + +/* foldr ********************************************************************/ + +static PyObject * +foldr(PyObject *func, PyObject *base, PyObject *iter) +{ + PyObject *item, *func_args, *func_return, *recursive; + + /* new reference */ + item = PyIter_Next(iter); + if (NULL == item) { + if (PyErr_Occurred()) { + return NULL; + } + return base; + } + + recursive = foldr(func, base, iter); + if (NULL == recursive) { + Py_DECREF(item); + return NULL; + } + + /* new reference */ + func_args = PyTuple_New(2); + /* both steal references */ + PyTuple_SET_ITEM(func_args, 0, item); + PyTuple_SET_ITEM(func_args, 1, recursive); + + func_return = PyObject_CallObject(func, func_args); + Py_DECREF(func_args); + return func_return; +} + +static PyObject * +functional_foldr(PyObject *self, PyObject *args) +{ + PyObject *func, *base, *seq, *iter, *result; + + if (!PyArg_UnpackTuple(args, "foldr", 3, 3, &func, &base, &seq)) { + return NULL; + } + Py_INCREF(base); + + iter = PyObject_GetIter(seq); + if (!iter) { + PyErr_SetString(PyExc_TypeError, + "argument 3 to foldr() must support iteration"); + Py_DECREF(base); + return NULL; + } + + result = foldr(func, base, iter); + Py_DECREF(iter); + return result; +} + +PyDoc_STRVAR(foldr_doc, +"foldr(function, start, iterable) -> object\n\ +\n\ +Like foldl, but starts from the end of the iterable and works back toward the\n\ +beginning. For example, foldr(subtract, 0, [1, 2, 3]) == 2, but\n\ +foldl(subtract, 0, [1, 2, 3] == -6\n\ +\n\ +foldr(add, 0, [1, 2, 3]) is equivalent to add(1, add(2, add(3, 0)))"); + +/* scanl ********************************************************************/ + +static PyObject * +functional_scanl(PyObject *self, PyObject *args) +{ + PyObject *func, *start, *result = NULL, *seq, *iter, *result_list; + + if (!PyArg_UnpackTuple(args, "scanl", 3, 3, &func, &start, &seq)) { + return NULL; + } + Py_INCREF(start); + + iter = PyObject_GetIter(seq); + if (!iter) { + PyErr_SetString(PyExc_TypeError, + "argument 3 to scanl() must support iteration"); + Py_DECREF(start); + return NULL; + } + + if ((args = PyTuple_New(2)) == NULL) { + goto Fail; + } + + result_list = PyList_New(0); + PyList_Append(result_list, start); + result = start; + + for (;;) { + PyObject *item; + + item = PyIter_Next(iter); + if (!item) { + if (PyErr_Occurred()) { + goto Fail; + } + break; + } + + PyTuple_SetItem(args, 0, result); + PyTuple_SetItem(args, 1, item); + if ((result = PyObject_CallObject(func, args)) == NULL) { + Py_INCREF(start); + goto Fail; + } + + PyList_Append(result_list, result); + + if (args->ob_refcnt > 1) { + Py_DECREF(args); + if ((args = PyTuple_New(2)) == NULL) + goto Fail; + } + } + goto Succeed; + +Fail: + Py_CLEAR(result_list); +Succeed: + Py_DECREF(start); + Py_XDECREF(args); + Py_DECREF(iter); + return result_list; +} + +PyDoc_STRVAR(scanl_doc, +"scanl(func, start, iterable) -> list\n\ +\n\ +Like foldl, but produces a list of successively reduced values, starting\n\ +from the left.\n\ +scanr(f, 0, [1, 2, 3]) is equivalent to\n\ +[0, f(0, 1), f(f(0, 1), 2), f(f(f(0, 1), 2), 3)]"); + +/* scanr ********************************************************************/ + +/* +In Haskell: + +scanr :: (a -> b -> b) -> b -> [a] -> [b] +scanr f q0 [] = [q0] +scanr f q0 (x:xs) = f x q : qs + where qs@(q:_) = scanr f q0 xs +*/ + +static PyObject * +functional_scanr(PyObject *self, PyObject *args) +{ + PyObject *func, *start, *seq, *iter, *result_list, *item, *func_args; + PyObject *result, *arg_1, *arg_2; + int i; + + if (!PyArg_UnpackTuple(args, "scanr", 3, 3, &func, &start, &seq)) { + return NULL; + } + Py_INCREF(start); + + func = PyTuple_GET_ITEM(args, 0); + if (!PyCallable_Check(func)) { + PyErr_SetString(PyExc_TypeError, + "argument 1 to scanr() must be callable"); + Py_DECREF(start); + return NULL; + } + + iter = PyObject_GetIter(seq); + if (iter == NULL) { + PyErr_SetString(PyExc_TypeError, + "argument 3 to scanr() must support iteration"); + Py_DECREF(start); + return NULL; + } + + result_list = PyList_New(0); + if (NULL == result_list) { + Py_DECREF(iter); + Py_DECREF(start); + return NULL; + } + + while ((item = PyIter_Next(iter))) { + PyList_Append(result_list, item); + Py_DECREF(item); + } + Py_DECREF(iter); + if (PyErr_Occurred()) { + Py_DECREF(start); + goto Fail; + } + + PyList_Append(result_list, start); + Py_DECREF(start); + + for (i = PyList_GET_SIZE(result_list) - 1; i > 0; i--) { + func_args = PyTuple_New(2); + arg_1 = PyList_GET_ITEM(result_list, i - 1); + Py_INCREF(arg_1); + arg_2 = PyList_GET_ITEM(result_list, i); + Py_INCREF(arg_2); + + PyTuple_SetItem(func_args, 0, arg_1); + PyTuple_SetItem(func_args, 1, arg_2); + + result = PyObject_CallObject(func, func_args); + Py_DECREF(func_args); + if (NULL == result) { + goto Fail; + } + PyList_SetItem(result_list, i - 1, result); + } + goto Succeed; + +Fail: + Py_CLEAR(result_list); +Succeed: + return result_list; +} + +PyDoc_STRVAR(scanr_doc, +"scanr(func, start, iterable) -> list\n\ +\n\ +Like foldr, but produces a list of successively reduced values, starting\n\ +from the right.\n\ +scanr(f, 0, [1, 2, 3]) is equivalent to\n\ +[f(1, f(2, f(3, 0))), f(2, f(3, 0)), f(3, 0), 0]"); + +/* id ***********************************************************************/ + +static PyObject * +functional_id(PyObject *self, PyObject *arg) +{ + Py_INCREF(arg); + return arg; +} + +PyDoc_STRVAR(id_doc, +"id(obj) -> object\n\ +\n\ +The identity function. id(obj) returns obj unchanged.\n\ +\n\ +>>> obj = object()\n\ +>>> id(obj) is obj\n\ +True"); + +/* compose ******************************************************************/ + +typedef struct { + PyObject_HEAD + PyObject *inner_func; + PyObject *outer_func; +} composeobject; + +static PyTypeObject compose_type; + +static PyObject * +compose_new(PyTypeObject *type, PyObject *args, PyObject *kw) +{ + PyObject *inner_func, *outer_func; + composeobject *compo; + + if (!PyArg_UnpackTuple(args, "compose", 2, 2, &outer_func, &inner_func)) { + return NULL; + } + Py_INCREF(inner_func); + Py_INCREF(outer_func); + + if (!PyCallable_Check(outer_func)) { + PyErr_SetString(PyExc_TypeError, + "all arguments to compose must be callable"); + goto Fail; + } + + if (!PyCallable_Check(inner_func)) { + PyErr_SetString(PyExc_TypeError, + "all arguments to compose must be callable"); + goto Fail; + } + + /* create flipobject structure */ + compo = (composeobject *)type->tp_alloc(type, 0); + if (compo == NULL) { + goto Fail; + } + + compo->inner_func = inner_func; + compo->outer_func = outer_func; + return (PyObject *)compo; + +Fail: + Py_DECREF(inner_func); + Py_DECREF(outer_func); + return NULL; +} + +static PyObject * +compose_call(composeobject *compo, PyObject *args, PyObject *kw) +{ + PyObject *ret_val, *inner_ret_val, *inner_tuple; + + inner_ret_val = PyObject_Call(compo->inner_func, args, kw); + if (NULL == inner_ret_val) { + return NULL; + } + + inner_tuple = PyTuple_New(1); + /* steals the reference to inner_ret_val */ + PyTuple_SET_ITEM(inner_tuple, 0, inner_ret_val); + + ret_val = PyObject_CallObject(compo->outer_func, inner_tuple); + Py_DECREF(inner_tuple); + return ret_val; +} + +static int +compose_traverse(composeobject *compo, visitproc visit, void *arg) +{ + Py_VISIT(compo->inner_func); + Py_VISIT(compo->outer_func); + + return 0; +} + +PyDoc_STRVAR(compose_doc, +"compose(func_1, func_2) -> compose object\n\ +\n\ +The compose object returned by compose is a composition of func_1 and func_2.\n\ +That is, compose(func_1, func_2)(5) == func_1(func_2(5))"); + +static void +compose_dealloc(composeobject *compo) +{ + PyObject_GC_UnTrack(compo); + + Py_XDECREF(compo->inner_func); + Py_XDECREF(compo->outer_func); + + compo->ob_type->tp_free(compo); +} + +static PyTypeObject compose_type = { + PyObject_HEAD_INIT(NULL) + 0, /* ob_size */ + "functional.compose", /* tp_name */ + sizeof(composeobject), /* tp_basicsize */ + 0, /* tp_itemsize */ + /* methods */ + (destructor)compose_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + (ternaryfunc)compose_call, /* tp_call */ + 0, /* tp_str */ + PyObject_GenericGetAttr, /* tp_getattro */ + PyObject_GenericSetAttr, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | + Py_TPFLAGS_BASETYPE, /* tp_flags */ + compose_doc, /* tp_doc */ + (traverseproc)compose_traverse, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + 0, /* tp_methods */ + 0, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0, /* tp_alloc */ + compose_new, /* tp_new */ + PyObject_GC_Del, /* tp_free */ +}; + +/* flip *********************************************************************/ + +typedef struct { + PyObject_HEAD + PyObject *func; +} flipobject; + +static PyTypeObject flip_type; + +static PyObject * +flip_new(PyTypeObject *type, PyObject *args) +{ + PyObject *func; + flipobject *flipo; + + if (!PyArg_UnpackTuple(args, "flip", 1, 1, &func)) { + return NULL; + } + Py_INCREF(func); + + if (!PyCallable_Check(func)) { + PyErr_SetString(PyExc_TypeError, + "the first argument must be callable"); + goto Fail; + } + + /* create flipobject structure */ + flipo = (flipobject *)type->tp_alloc(type, 0); + if (flipo == NULL) { + goto Fail; + } + + flipo->func = func; + + return (PyObject *)flipo; + +Fail: + Py_DECREF(func); + return NULL; +} + +static PyObject * +flip_call(flipobject *flipo, PyObject *args, PyObject *kw) +{ + PyObject *return_val, *flipped_args, *item; + int args_size, i; + + assert (PyCallable_Check(flipo->func)); + + args_size = PyTuple_GET_SIZE(args); + if (args_size < 2) { + PyErr_SetString(PyExc_TypeError, + "flip object takes at least 2 non-keyword parameters"); + return NULL; + } + + flipped_args = PyTuple_New(args_size); + + item = PyTuple_GetItem(args, 1); + Py_INCREF(item); + PyTuple_SET_ITEM(flipped_args, 0, item); + item = PyTuple_GetItem(args, 0); + Py_INCREF(item); + PyTuple_SET_ITEM(flipped_args, 1, item); + + for(i = 2; i < args_size; i++) { + item = PyTuple_GetItem(args, i); + Py_INCREF(item); + PyTuple_SET_ITEM(flipped_args, i, item); + } + + return_val = PyObject_Call(flipo->func, flipped_args, kw); + Py_DECREF(flipped_args); + return return_val; +} + +static int +flip_traverse(flipobject *flipo, visitproc visit, void *arg) +{ + Py_VISIT(flipo->func); + return 0; +} + +PyDoc_STRVAR(flip_doc, +"flip(func) -> flip object\n\ +\n\ +flip causes `func` to take its first two non-keyword arguments in reverse\n\ +order. The returned flip object is a wrapper around `func` that makes this\n\ +happen."); + +static void +flip_dealloc(flipobject *flipo) +{ + PyObject_GC_UnTrack(flipo); + Py_XDECREF(flipo->func); + flipo->ob_type->tp_free(flipo); +} + +static PyTypeObject flip_type = { + PyObject_HEAD_INIT(NULL) + 0, /* ob_size */ + "functional.flip", /* tp_name */ + sizeof(flipobject), /* tp_basicsize */ + 0, /* tp_itemsize */ + /* methods */ + (destructor)flip_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + (ternaryfunc)flip_call, /* tp_call */ + 0, /* tp_str */ + PyObject_GenericGetAttr, /* tp_getattro */ + PyObject_GenericSetAttr, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | + Py_TPFLAGS_BASETYPE, /* tp_flags */ + flip_doc, /* tp_doc */ + (traverseproc)flip_traverse, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + 0, /* tp_methods */ + 0, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0, /* tp_alloc */ + flip_new, /* tp_new */ + PyObject_GC_Del, /* tp_free */ +}; + /* module level code ********************************************************/ PyDoc_STRVAR(module_doc, "Tools for functional programming."); static PyMethodDef module_methods[] = { + {"foldl", functional_foldl, METH_VARARGS, foldl_doc}, + {"foldr", functional_foldr, METH_VARARGS, foldr_doc}, + {"id", functional_id, METH_O, id_doc}, + {"scanl", functional_scanl, METH_VARARGS, scanl_doc}, + {"scanr", functional_scanr, METH_VARARGS, scanr_doc}, {NULL, NULL} /* sentinel */ }; @@ -259,6 +853,8 @@ char *name; PyTypeObject *typelist[] = { &partial_type, + &flip_type, + &compose_type, NULL }; Index: Lib/test/test_functional.py =================================================================== --- Lib/test/test_functional.py (revision 42219) +++ Lib/test/test_functional.py (working copy) @@ -152,14 +152,316 @@ thetype = PythonPartial +def add(a, b): + return a + b + +def sub(a, b): + return a - b + +def fail(a, b): + raise RuntimeError +from functional import foldl +class Test_foldl(unittest.TestCase): + fold_func = staticmethod(foldl) + minus_answer = -6 + def test_bad_arg_count(self): + try: + self.fold_func() + except TypeError, e: + pass + else: + self.fail("Failed to raise TypeError") + + def test_bad_first_arg(self): + try: + self.fold_func(5, 0, [1, 2, 3]) + except TypeError, e: + pass + else: + self.fail("Failed to raise TypeError") + + def test_bad_third_arg(self): + try: + self.fold_func(add, 0, 123) + except TypeError, e: + pass + else: + self.fail("Failed to raise TypeError") + + def test_functionality_1(self): + answer = self.fold_func(sub, 0, [1, 2, 3]) + self.assertEqual(answer, self.minus_answer) + + def test_functionality_2(self): + self.assertEqual(self.fold_func(add, 0, []), 0) + + def test_works_with_generators(self): + def gen(): + for i in [1, 2, 3]: + yield i + raise StopIteration + + answer = self.fold_func(sub, 0, gen()) + self.assertEqual(answer, self.minus_answer) + + def test_generators_raises_exc(self): + def gen(): + for i in [1, 2, 3]: + yield i + raise RuntimeError + + try: + list(self.fold_func(sub, 0, gen())) + except RuntimeError: + pass + else: + self.fail("Failed to raise RuntimeError") + + def test_errors_pass_through(self): + try: + self.fold_func(fail, 0, [1, 2, 3]) + except RuntimeError: + pass + else: + self.fail("Failed to raise RuntimeError") + + def test_func_not_called_for_empty_seq(self): + self.fold_func(fail, 0, []) + + def test_seq_not_modified(self): + seq = [1, 2, 3] + self.fold_func(add, 0, seq) + + self.assertEqual(seq, [1, 2, 3]) + +from functional import foldr +class Test_foldr(Test_foldl): + fold_func = staticmethod(foldr) + minus_answer = 2 + +from functional import scanl +class Test_scanl(unittest.TestCase): + scan_func = staticmethod(scanl) + minus_answer = [0, -1, -3, -6] + + def test_no_args(self): + try: + self.scan_func() + except TypeError: + pass + else: + self.fail("Failed to raise TypeError") + + def test_bad_first_arg(self): + try: + self.scan_func(5, 0, [1, 2, 3]) + except TypeError: + pass + else: + self.fail("Failed to raise TypeError") + + def test_bad_third_arg(self): + try: + self.scan_func(sub, 0, 5) + except TypeError: + pass + else: + self.fail("Failed to raise TypeError") + + def test_empty_list(self): + answer = self.scan_func(sub, 0, []) + + self.assertEqual(list(answer), [0]) + + def test_with_list(self): + answer = self.scan_func(sub, 0, [1, 2, 3]) + + self.assertEqual(list(answer), self.minus_answer) + + def test_with_generator(self): + def gen(): + for o in [1, 2, 3]: + yield o + raise StopIteration + + answer = self.scan_func(sub, 0, gen()) + + self.assertEqual(list(answer), self.minus_answer) + + def test_gen_raises_exc(self): + def gen(): + for o in [1, 2, 3]: + yield o + raise RuntimeError + + try: + list(self.scan_func(sub, 0, gen())) + except RuntimeError: + pass + else: + self.fail("Failed to raise RuntimeError") + + def test_with_exception(self): + try: + list(self.scan_func(fail, 0, [1, 2, 3])) + except RuntimeError: + pass + else: + self.fail("Failed to raise RuntimeError") + + def test_func_not_called_for_empty_seq(self): + self.scan_func(fail, 0, []) + +from functional import scanr +class Test_scanr(Test_scanl): + scan_func = staticmethod(scanr) + minus_answer = [2, -1, 3, 0] + +from functional import id +class Test_id(unittest.TestCase): + def test_no_args(self): + try: + id() + except TypeError: + pass + else: + self.fail("Failed to raise TypeError") + + def test(self): + obj = object() + + self.failUnless(id(obj) is obj) + +from functional import flip +class Test_flip(unittest.TestCase): + def test_no_args(self): + try: + flip() + except TypeError: + pass + else: + self.fail("Failed to raise TypeError") + + def test_bad_first_arg(self): + try: + flip("foo") + except TypeError: + pass + else: + self.fail("Failed to raise TypeError") + + def test(self): + flipped_sub = flip(sub) + + self.assertEqual(sub(4, 5), -1) + self.assertEqual(flipped_sub(4, 5), 1) + + def test_bad_call(self): + flipped_sub = flip(sub) + + try: + flipped_sub(4) + except TypeError: + pass + else: + self.fail("Failed to raise TypeError") + + def test_all_kw(self): + flipped_sub = flip(sub) + + try: + flipped_sub(f=4, g=3) + except TypeError: + pass + else: + self.fail("Failed to raise TypeError") + +from functional import compose +class Test_compose(unittest.TestCase): + def test_no_args(self): + try: + compose() + except TypeError: + pass + else: + self.fail("Failed to raise TypeError") + + def test_bad_first_arg(self): + try: + compose(5, compose) + except TypeError: + pass + else: + self.fail("Failed to raise TypeError") + + def test_bad_second_arg(self): + try: + compose(compose, 5) + except TypeError: + pass + else: + self.fail("Failed to raise TypeError") + + def test(self): + def minus_4(a): + return a - 4 + + def mul_2(a): + return a * 2 + + f = compose(minus_4, mul_2) + self.assertEqual(f(4), 4) + + f = compose(mul_2, minus_4) + self.assertEqual(f(4), 0) + + def test_errors_flow_through_1(self): + def inner(a): + raise RuntimeError + + def outer(a): + return a + + f = compose(outer, inner) + + try: + f(5) + except RuntimeError: + pass + else: + self.fail("Failed to raise RuntimeError") + + def test_errors_flow_through_2(self): + def outer(a): + raise RuntimeError + + def inner(a): + return a + + f = compose(outer, inner) + + try: + f(5) + except RuntimeError: + pass + else: + self.fail("Failed to raise RuntimError") + def test_main(verbose=None): import sys test_classes = ( TestPartial, TestPartialSubclass, TestPythonPartial, + Test_foldl, + Test_foldr, + Test_scanr, + Test_scanl, + Test_id, + Test_flip, + Test_compose, ) test_support.run_unittest(*test_classes) Index: Doc/lib/libfunctional.tex =================================================================== --- Doc/lib/libfunctional.tex (revision 42219) +++ Doc/lib/libfunctional.tex (working copy) @@ -5,7 +5,9 @@ \moduleauthor{Peter Harris}{scav@blueyonder.co.uk} \moduleauthor{Raymond Hettinger}{python@rcn.com} +\moduleauthor{Collin Winter}{collinw@gmail.com} \sectionauthor{Peter Harris}{scav@blueyonder.co.uk} +\sectionauthor{Collin Winter}{collinw@gmail.com} \modulesynopsis{Higher-order functions and operations on callable objects.} @@ -15,9 +17,175 @@ that act on or return other functions. In general, any callable object can be treated as a function for the purposes of this module. +To learn more about functional programming, please consult a dedicated +functional programming tutorial. A quick Google search for "python +functional programming" turns up many excellent resources. -The \module{functional} module defines the following function: +In addition to the contents of this module, Python includes a number of other +tools useful for functional programming, such as the built-in functions +\function{sum}, \function{max}, \function{min}, \function{map} and +\function{filter}. +Also of interest to functional programmers are the \refmodule{operator} +and \refmodule{itertools} modules. + +The \module{functional} module defines the following functions: + +\begin{funcdesc}{compose}{outer, inner} + +\function{compose} implements function composition. In other words, +it returns a wrapper around the \var{outer} and \var{inner} callables, such +that the return value from \var{inner} is fed directly to \var{outer}. That is + + \begin{verbatim} + >>> def add(a, b): + ... return a + b + ... + >>> def double(a): + ... return 2 * a + ... + >>> compose(double, add)(5, 6) + 22 + \end{verbatim} + +is equivalent to + + \begin{verbatim} + >>> double(add(5, 6)) + 22 + \end{verbatim} + +Note that while the \var{inner} callable may take multiple keyword and +non-keyword arguments, the \var{outer} callable may take only a single +non-keyword argument. + +\end{funcdesc} + + +\begin{funcdesc}{flip}{func} + +\function{flip} wraps the callable in \var{func}, causing it to receive its +first two non-keyword arguments in reverse order; all other keyword and +non-keyword arguments are passed normally. + +If \var{func} is not callable, a \exception{TypeError} exception will be +raised. + +The callable returned by \function{flip} takes at least 2 non-keyword +arguments; if fewer than 2 such arguments are passed, a \exception{TypeError} +will be raised. + +Example: + + \begin{verbatim} + >>> def pair(a, b): + ... return (a, b) + >>> pair(5, 6) + (5, 6) + >>> flipped_pair = flip(pair) + >>> flipped_pair(5, 6) + (6, 5) + \end{verbatim} + +\end{funcdesc} + + +\begin{funcdesc}{foldl}{func,start,iterable} + +\function{foldl} takes a binary function, a starting value (usually some kind +of 'zero'), and an iterable. The function is applied to the starting value and +the first element of the list, then the result of that and the second element +of the list, then the result of that and the third element of the list, and so +on. + +This means that a call such as + + \begin{verbatim} + foldl(f, 0, [1, 2, 3]) + \end{verbatim} + +is equivalent to + + \begin{verbatim} + f(f(f(0, 1), 2), 3) + \end{verbatim} + +\function{foldl} is roughly equivalent to the following recursive function: + + \begin{verbatim} + def foldl(func, start, seq): + if len(seq) == 0: + return start + + return foldl(func, func(start, seq[0]), seq[1:]) + \end{verbatim} + +Speaking of equivalence, the above \function{foldl} call can be expressed in +terms of the built-in \function{reduce} like so: + + \begin{verbatim} + reduce(f, [1, 2, 3], 0) + \end{verbatim} +\end{funcdesc} + + +\begin{funcdesc}{foldr}{func,start,iterable} + +\function{foldr} is much like \function{foldl}, except it starts with the last +element, rather than the first element like \function{foldl} does. This +means that a call such as + + \begin{verbatim} + foldr(f, 0, [1, 2, 3]) + \end{verbatim} + +is equivalent to + + \begin{verbatim} + f(1, f(2, f(3, 0))) + \end{verbatim} + +The difference can be seen in the following example: + + \begin{verbatim} + >>> def minus(a, b): + ... return a - b + ... + >>> foldl(minus, 0, [1, 2, 3]) + -6 + >>> foldr(minus, 0, [1, 2, 3]) + 2 + >>> + \end{verbatim} + +\function{foldr} is roughly equivalent to the following recursive function: + + \begin{verbatim} + def foldr(func, start, seq): + if len(seq) == 0: + return start + + return func(seq[0], foldr(func, start, seq[1:])) + \end{verbatim} + +Note that unlike \function{foldl}, \function{foldr} cannot be expressed in +terms of the built-in \function{reduce} function. +\end{funcdesc} + + +\begin{funcdesc}{id}{obj} +The identity function. \function{id} takes any object and simply returns that +object with an updated reference count. + +\function{id} is implemented in \C for speed. It is equivalent to this function: + + \begin{verbatim} + def id(obj): + return obj + \end{verbatim} +\end{funcdesc} + + \begin{funcdesc}{partial}{func\optional{,*args}\optional{, **keywords}} Return a new \class{partial} object which when called will behave like \var{func} called with the positional arguments \var{args} and keyword @@ -51,7 +219,56 @@ \end{funcdesc} +\begin{funcdesc}{scanl}{func, start, iterable} +Like \function{foldl}, but produces a list of successively reduced values, +starting from the left. + + \begin{verbatim} + scanl(f, 0, [1, 2, 3]) + \end{verbatim} + +is equivalent to + + \begin{verbatim} + [0, f(0, 1), f(f(0, 1), 2), f(f(f(0, 1), 2), 3)] + \end{verbatim} + +\function{scanl} returns a iterator over the result list. This is done so that the +list may be calculated lazily. + +The first argument to \function{scanl} must be callable or a +\exception{TypeError} will be raised. Likewise, a \exception{TypeError} will be +raised if the \var{iterable} argument is not capable of iteration. + +\end{funcdesc} + + +\begin{funcdesc}{scanr}{func, start, iterable} + +Like \function{foldr}, but produces a list of successively reduced values, +starting from the right. + + \begin{verbatim} + scanr(f, 0, [1, 2, 3]) + \end{verbatim} + +is equivalent to + + \begin{verbatim} + [f(1, f(2, f(3, 0))), f(2, f(3, 0)), f(3, 0), 0] + \end{verbatim} + +\function{scanr} returns a iterator over the result list. This is done so that the +list may be calculated lazily. + +The first argument to \function{scanr} must be callable or a +\exception{TypeError} will be raised. Likewise, a \exception{TypeError} will be +raised if the \var{iterable} argument is not capable of iteration. + +\end{funcdesc} + + \subsection{\class{partial} Objects \label{partial-objects}}