Pythonic pointfree programming.
Copyright 2011 Mark Shroyer
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
The general use case is to wrap functions in the pointfree wrapper / decorator class, granting them both automatic partial application support and a pair of function composition operators:
>>> from pointfree import *
>>> @pointfree
... def pfadd(a, b):
... return a + b
>>> @pointfree
... def pfexp(n, exp):
... return n ** exp
>>> fn = pfexp(exp=2) * pfadd(1)
>>> fn(3)
16
pointfree.pointfree inherits from the pointfree.partial class (not to be confused with functools.partial()), which provides automatic partial application but not the function composition operators. See partial's documentation for details of the partial application semantics, and pointfree's documentation for information about the function composition operators.
The module also includes a number of pre-defined helper functions which can be combined for various purposes:
>>> fn = pfmap(lambda x: x**3) >> pfprint_all
>>> fn(range(4))
0
1
8
27
Refer to the section Composable helper functions for information about the helpers provided by this module.
Wraps a regular Python function or method into a callable object supporting automatic partial application.
Parameters: |
|
---|
Example:
>>> @partial
... def foo(a,b,c):
... return a + b + c
>>> foo(1,2,3)
6
>>> foo(1)(2)(3)
6
Generally speaking, the evaluation strategy with regard to automatic partial application is to apply all given arguments to the underlying function as soon as possible.
When a partial instance is called, the positional and keyword arguments supplied are combined with the instance’s own cache of arguments for the wrapped function (which is empty to begin with, for instances directly wrapping – or applied as decorators to – pure Python functions or methods). If the combined set of arguments is sufficient to invoke the wrapped function, then the function is called and its result returned. If the combined arguments are not sufficient, then a new copy of the wrapper is returned instead, with the new combined argument set in its cache.
Calling a partial object never changes its state; instances are immutable for practical purposes, so they can be called and reused indefinitely:
>>> p = q = foo(1,2)
>>> p(3)
6
>>> q(4) # Using the same instance twice
7
Arguments with default values do not need to be explicitly specified in order for evaluation to occur. In the following example, foo2 can be evaluated as soon as we have specified the arguments a and b:
>>> @partial
... def foo2(a, b, c=3):
... return a + b + c
>>> foo2(1,2)
6
>>> foo2(1)(2)
6
However, if extra arguments are supplied prior to evaluation, and if the underlying function is capable of accepting those arguments, then those will be passed to the function as well. If we call foo2 as follows, the third argument will be passed to the wrapped function as c, overriding its default value:
>>> foo2(1,2,5)
8
>>> foo2(3)(4,5)
12
This works similarly with functions that accept variable positional argument lists:
>>> @partial
... def foo3(a, *args):
... return a + sum(args)
>>> foo3(1)
1
>>> foo3(1,2)
3
>>> foo3(1,2,3)
6
Or variable keyword argument lists:
>>> @partial
... def foo4(a, **kargs):
... kargs.update({'a': a})
... return kargs
>>> result = foo4(3, b=4, c=5)
>>> for key in sorted(result.keys()):
... print("%s: %s" % (key, result[key]))
a: 3
b: 4
c: 5
But if you try to supply an argument that the function cannot accept, a TypeError will be raised as soon as you attempt to do so – the wrapper doesn’t wait until the underlying function is called before raising the exception (unlike with functools.partial()):
>>> @partial
... def foo5(a, b, c):
... return a + b + c
>>> foo5(d=7)
Traceback (most recent call last):
...
TypeError: foo5() got an unexpected keyword argument 'd'
There are some sutble differences between how automatic partial application works in this module and the semantics of regular Python function application (or, again, of functools.partial()). First, keyword arguments to partially applied functions can override an argument specified in a previous call:
>>> @partial
... def foo6(a, b, c):
... return (a, b, c)
>>> foo6(1)(b=2)(b=3)(4) # overriding b given as keyword
(1, 3, 4)
>>> foo6(1,2)(b=3)(4) # overriding b given positionally
(1, 3, 4)
Also, the wrapper somewhat blurs the line between positional and keyword arguments for the sake of flexibilty. If an argument is specified with a keyword and then “reached” by a positional argument in a subsequent call, the remaining positional argument values “wrap around” the argument previously specified as a keyword.
This second difference is best illustrated by example. Again using the function foo6 from above, if we specify b as a keyword argument:
>>> p = foo6(b=2)
and then apply two positional arguments to the resulting partial instance, those arguments will be used to specify a and c, skipping over b because it has already been specified:
>>> p(1,3)
(1, 2, 3)
This approach was chosen because it allows us to compose partial applications of functions where a previous argument has been specified as a keyword argument.
As well as functions, partial can be applied to methods, including class and static methods:
>>> class Foo7(object):
... m = 2
...
... def __init__(self, n):
... self.n = n
...
... @partial
... def bar_inst(self, a, b, c):
... return self.m + self.n + a + b + c
...
... @partial
... @classmethod
... def bar_class(klass, a, b, c):
... return klass.m + a + b + c
...
... @partial
... @staticmethod
... def bar_static(a, b, c):
... return a + b + c
>>> f = Foo7(3)
>>> f.bar_inst(4)(5)(6)
20
>>> f.bar_class(3)(4)(5)
14
>>> f.bar_static(2)(3)(4)
9
The wrapper can also be instantiated from another partial instance:
>>> def foo8(a, b, c, *args):
... return a + b + c + sum(args)
>>> p = partial(foo8, 1)
>>> q = partial(p, 2)
>>> q(3)
6
Or even from a functools.partial() instance:
>>> p = functools.partial(foo8, 1)
>>> q = partial(p)
>>> q(2)(3)
6
However, it cannot currently wrap a Python builtin function (or a functools.partial() instance which wraps a builtin function), as Python does not currently provide sufficient reflection for its builtins.
While you will probably apply partial as a decorator when defining your own functions, you can also wrap existing functions by instantiating the class directly:
>>> partial(foo8)(1)(2)(3)
6
Or like with functools.partial(), you can specify arguments for the wrapped function when you instantiate a wrapper:
>>> p = partial(foo8, 1)
>>> p(2)(3)
6
But unlike calling an existing wrapper instance, the wrapped function will not be invoked during instantiation even if enough arguments are supplied in order to do so; invocation does not occur until the partial instance is called at least once, even with an empty argument list:
>>> p = partial(foo8, 1, 2, 3)
>>> type(p)
<class 'pointfree.partial'>
>>> p()
6
>>> p(4)
10
Wraps a regular Python function or method into a callable object supporting the >> and * function composition operators, as well as automatic partial application inherited from partial.
Parameters: |
|
---|
This class inherits its partial application behavior from partial; refer to its documentation for details.
On top of automatic partial application, the pointfree wrapper adds two function composition operators, >> and *, for “forward” and “reverse” function composition respectively. For example, given the following wrapped functions:
>>> @pointfree
... def pfadd(a, b):
... return a + b
>>> @pointfree
... def pfmul(a, b):
... return a * b
The following forward composition defines the function f() as one which takes a given number, adds one to it, and then multiplies the result of the addition by two:
>>> f = pfadd(1) >> pfmul(2)
>>> f(1)
4
Reverse composition simply works in the opposite direction. In this example, g() takes a number, multiplies it by three, and then adds four:
>>> g = pfadd(4) * pfmul(3)
>>> g(5)
19
The alias pf is provided for pointfree to conserve electrons when wrapping functions inline:
>>> def add(a, b):
... return a + b
>>> def mul(a, b):
... return a * b
>>> f = pf(add, 1) >> pf(mul, 2)
>>> f(2)
6
When using pointfree as a decorator on class or static methods, you must ensure that it is the “topmost” decorator, so that the resulting object is a pointfree instance in order for the composition operators to work.
A pointfree map function: Returns an iterator over the results of applying a function of one argument to the items of a given iterable. The function is provided “lazily” to the given iterable; each function application is performed on the fly as it is requested.
Parameters: |
|
---|---|
Return type: | Iterator of function application results |
Example:
>>> f = pfmap(lambda x: x+1) \
... >> pfmap(lambda x: x*2) \
... >> pfcollect
>>> f(range(5))
[2, 4, 6, 8, 10]
A pointfree reduce / left fold function: Applies a function of two arguments cumulatively to the items supplied by the given iterable, so as to reduce the iterable to a single value. If an initial value is supplied, it is placed before the items from the iterable in the calculation, and serves as the default when the iterable is empty.
Parameters: |
|
---|---|
Return type: | Single value |
Example:
>>> from operator import add
>>> sum_of_squares = pfreduce(add, initial=0) * pfmap(lambda n: n**2)
>>> sum_of_squares([3, 4, 5, 6])
86
Collects and returns a list of values from the given iterable. If the n parameter is not specified, collects all values from the iterable.
Parameters: |
|
---|---|
Return type: | List of values from the iterable |
Example:
>>> @pointfree
... def fibonaccis():
... a, b = 0, 1
... while True:
... a, b = b, a+b
... yield a
>>> (pfcollect(n=10) * fibonaccis)()
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
Prints an item.
Parameters: |
|
---|---|
Return type: | None |
Example:
>>> from operator import add
>>> fn = pfreduce(add, initial=0) >> pfprint
>>> fn([1, 2, 3, 4])
10
Prints each item from an iterable.
Parameters: |
|
---|---|
Return type: | None |
Example:
>>> @pointfree
... def prefix_all(prefix, iterable):
... for item in iterable:
... yield "%s%s" % (prefix, item)
>>> fn = prefix_all("An item: ") >> pfprint_all
>>> fn(["foo", "bar", "baz"])
An item: foo
An item: bar
An item: baz
Consumes all the items from an iterable, discarding their output. This may be useful if evaluating the iterable produces some desirable side-effect, but you have no need to collect its output.
Parameters: | iterable – An iterable |
---|---|
Return type: | None |
Example:
>>> result = []
>>> @pointfree
... def append_all(collector, iterable):
... for item in iterable:
... collector.append(item)
... yield item
>>> @pointfree
... def square_all(iterable):
... for item in iterable:
... yield item**2
>>> fn = square_all \
... >> append_all(result) \
... >> pfignore_all
>>> fn([1, 2, 3, 4])
>>> result
[1, 4, 9, 16]