Fn.py: enjoy FP in Python
Despite the fact that Python is not pure-functional programming language, it's multi-paradigm PL and it gives you enough freedom to take credits from functional programming approach. There are theoretical and practical advantages to the functional style:
- Formal provability
- Ease of debugging and testing
Fn.py library provides you with missing "batteries" to get maximum
from functional approach even in mostly-imperative program.
More about functional approach from my Pycon UA 2012 talks: Functional Programming with Python.
Scala-style lambdas definition
from fn import _ from fn.op import zipwith from itertools import repeat assert list(map(_ * 2, range(5))) == [0,2,4,6,8] assert list(filter(_ < 10, [9,10,11])) ==  assert list(zipwith(_ + _)([0,1,2], repeat(10))) == [10,11,12]
More examples of using
_ you can find in test
declaration (attributes resolving, method calling, slicing).
Attention! If you work in interactive python shell, your should remember that
_ means "latest output" and you'll get unpredictable results. In this case, you can do something like
from fn import _ as X (and then write functions like
X * 2).
If you are not sure, what your function is going to do, you can print it:
from fn import _ print (_ + 2) # "(x1) => (x1 + 2)" print (_ + _ * _) # "(x1, x2, x3) => (x1 + (x2 * x3))"
_ will fail with
TypeError subclass) on inaccurate number of passed arguments. This is one more restrictions to ensure that you did everything right:
>>> from fn import _ >>> (_ + _)(1) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "fn/underscore.py", line 82, in __call__ raise ArityError(self, self._arity, len(args)) fn.underscore.ArityError: (_ + _) expected 2 arguments, got 1
Streams and infinite sequences declaration
Lazy-evaluated scala-style streams. Basic idea: evaluate each new
element "on demand" and share calculated elements between all created
Stream object supports
<< operator that means pushing
new elements when it's necessary.
from fn import Stream s = Stream() << [1,2,3,4,5] assert list(s) == [1,2,3,4,5] assert s == 2 assert list(s[0:2]) == [1,2] s = Stream() << range(6) << [6,7] assert list(s) == [0,1,2,3,4,5,6,7] def gen(): yield 1 yield 2 yield 3 s = Stream() << gen << (4,5) assert list(s) == [1,2,3,4,5]
Lazy-evaluated stream is useful for infinite sequences, i.e. fibonacci sequence can be calculated as:
from fn import Stream from fn.iters import take, drop, map from operator import add f = Stream() fib = f << [0, 1] << map(add, f, drop(1, f)) assert list(take(10, fib)) == [0,1,1,2,3,5,8,13,21,34] assert fib == 6765 assert list(fib[30:35]) == [832040,1346269,2178309,3524578,5702887]
fn.recur.tco is a workaround for dealing with TCO without heavy stack utilization. Let's start from simple example of recursive factorial calculation:
def fact(n): if n == 0: return 1 return n * fact(n-1)
This variant works, but it's really ugly. Why? It will utilize memory too heavy cause of recursive storing all previous values to calculate final result. If you will execute this function with big
n (more then
sys.getrecursionlimit()) CPython will fail with
>>> import sys >>> fact(sys.getrecursionlimit() * 2) ... many many lines of stacktrace ... RuntimeError: maximum recursion depth exceeded
Which is good, cause it prevents you from terrible mistakes in your code.
How can we optimize this solution? Answer is simple, lets transform function to use tail call:
def fact(n, acc=1): if n == 0: return acc return fact(n-1, acc*n)
Why this variant is better? Cause you don't need to remember previous values to calculate final result. More about tail call optimization on Wikipedia. But... Python interpreter will execute this function the same way as previous one, so you won't win nothing.
fn.recur.tco gives you mechanism to write "optimized a bit" tail call recursion (using "trampoline" approach):
from fn import recur @recur.tco def fact(n, acc=1): if n == 0: return False, acc return True, (n-1, acc*n)
@recur.tco is a decorator that execute your function in
while loop and check output:
(False, result)means that we finished
(True, args, kwargs)means that we need to call function again with other arguments
(func, args, kwargs)to switch function to be executed inside while loop
Attention: be careful with mutable/immutable data structures processing.
High-level operations with functions
fn.F is a useful function wrapper to provide easy-to-use partial
application and functions composition.
from fn import F, _ from operator import add, mul # F(f, *args) means partial application # same as functools.partial but returns fn.F instance assert F(add, 1)(10) == 11 # F << F means functions composition, # so (F(f) << g)(x) == f(g(x)) f = F(add, 1) << F(mul, 100) assert list(map(f, [0, 1, 2])) == [1, 101, 201] assert list(map(F() << str << (_ ** 2) << (_ + 1), range(3))) == ["1", "4", "9"]
It also give you move readable in many cases "pipe" notation to deal with functions composition:
from fn import F, _ from fn.iters import filter, range func = F() >> (filter, _ < 6) >> sum assert func(range(10)) == 15
You can find more examples for compositions usage in
fn.op.apply executes given function with given positional arguments
in list (or any other iterable).
fn.op.flip returns you function
that will reverse arguments order before apply.
from fn.op import apply, flip from operator import add, sub assert apply(add, [1, 2]) == 3 assert flip(sub)(20,10) == -10 assert list(map(apply, [add, mul], [(1,2), (10,20)])) == [3, 200]
fn.op.foldr are folding operators. Each accepts function with arity 2 and returns function that can be used to reduce iterable to scalar: from left-to-right and from right-to-left in case of
from fn import op, _ folder = op.foldr(_ * _, 1) assert 6 == op.foldl(_ + _)([1,2,3]) assert 6 == folder([1,2,3])
Use case specific for right-side folding is:
from fn.op import foldr, call assert 100 == foldr(call, 0 )([lambda s: s**2, lambda k: k+10]) assert 400 == foldr(call, 10)([lambda s: s**2, lambda k: k+10])
fn.uniform provides you with "unification"
of lazy functionality for few functions to work the same way in Python
itertools.imapin Python 2+)
itertools.ifilterin Python 2+)
functools.reducein Python 3+)
itertools.izipin Python 2+)
xrangein Python 2+)
itertools.ifilterfalsein Python 2+)
itertools.izip_longestin Python 2+)
accumulate(backported to Python < 3.3)
fn.iters is high-level recipes to work with iterators. Most
of them taken from Python
and adopted to work both with Python 2+/3+. Such recipes as
splitby I have already
submitted as docs patch which is
review status just now.
Functional style for error-handling
fn.monad.Option represents optional values, each instance of
Option can be either instance of
Empty. It provides you with simple way to write long computation sequences and get rid of many
if/else blocks. See usage examples below.
Assume that you have
Request class that gives you parameter value by its name. To get uppercase notation for non-empty striped value:
class Request(dict): def parameter(self, name): return self.get(name, None) r = Request(testing="Fixed", empty=" ") param = r.parameter("testing") if param is None: fixed = "" else: param = param.strip() if len(param) == 0: fixed = "" else: fixed = param.upper()
Hmm, looks ugly.. Update code with
from operator import methodcaller from fn.monad import optionable class Request(dict): @optionable def parameter(self, name): return self.get(name, None) r = Request(testing="Fixed", empty=" ") fixed = r.parameter("testing") .map(methodcaller("strip")) .filter(len) .map(methodcaller("upper")) .get_or("")
fn.monad.Option.or_call is good method for trying several variant to end computation. I.e. use have
Request class with optional attributes
url. You need to evaluate "request type" using at least one attribute:
from fn.monad import Option request = dict(url="face.png", mimetype="PNG") tp = Option \ .from_value(request.get("type", None)) \ # check "type" key first .or_call(from_mimetype, request) \ # or.. check "mimetype" key .or_call(from_extension, request) \ # or... get "url" and check extension .get_or("application/undefined")
$ pip install fn
Or, if you absolutely must:
$ easy_install fn
You can also build library from source
$ git clone https://github.com/kachayev/fn.py.git $ cd fn.py $ python setup.py install
Work in progress
fn.monad.Eitherto deal with error logging
- C-accelerator for most modules
Ideas to think about:
- Curried function builder to simplify
lambda arg1: lambda arg2: ...
- Scala-style for-yield loop to simplify long map/filter blocks
- Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug.
- Fork the repository on Github to start making your changes to the master branch (or branch off of it).
- Write a test which shows that the bug was fixed or that the feature works as expected.
How to find me
- Twitter: @kachayev
- Email: kachayev <at> gmail.com