Overview and Table of Contents
This package is used as utility to support more thorough FP (functional programming) functionalities in Python. For more, you can refer to FPU.pptx
FP Introduction
(Source link) Functional programming has a long history. In a nutshell, its a style of programming where you focus on transforming data through the use of small expressions that ideally don’t contain side effects. In other words, when you call my_fun(1, 2), it will alway return the same result. This is achieved by immutable data typical of a functional language.
- Lisp 1958
- Renaissance: F#, Haskell, Erlang ...
- Used in industry such as Trading, Algorithmic, and Telecommunication etc
Features of FP
- First-class function/Higher order function
- Pure functions without side effects
- Immutable data structures
- Preserve state in functions (Closure, Cury)
- Recursion instead of loops/iteration
First-class function/Higher order function
With this feature, you can:
- Use function as argument(s)
- Function can return function
- Enables functional composition
Let's take a example to explain the usage of functional composition. Below is the imperative way to get the minimum value of all maximum values from values:
dataSet = [{'values':[1, 2, 3]}, {'values':[4, 5]}]
def min_max_imp(dataSet):
max_list = []
for d in dataSet:
max_list.append(max(d['values']))
return min(max_list)
min_max_imp(dataSet)
Above function min_max_imp
actually comprises two sub steps:
- Get the maximum of each values
- Get the minimum of list collected by previous step
This implies you can compose above two steps (function) into one by leveraging exist functions:
from fpu.fp import *
from functools import reduce, partial
min_max = compose2(
partial(reduce, min), \
partial(map, lambda d: max(d['values']))
)
min_max(dataSet)
With composing feature, you can write less dump code and make use of exist function to generate new function!
Pure functions without side effects
The side effects can refer to many things. I suggest you to read below materials to know more:
Immutable data structures
There are many python packages support you to carry out this requirement. One of them is pyrsistent. Below is a few usage of it to show immutable data
:
In [2]: v1 = v(1, 2, 3)
In [3]: v2 = v1.append(4)
In [4]: v1
Out[4]: pvector([1, 2, 3])
In [5]: v2
Out[5]: pvector([1, 2, 3, 4])
In [6]: v3 = v2.set(1, 5)
In [7]: v2
Out[7]: pvector([1, 2, 3, 4])
In [8]: v3
Out[8]: pvector([1, 5, 3, 4])
Above is a demonstration on data structure vector. There are more for PMap, PSet, PRecord and PClass.
Preserve state in functions (Closure, Cury)
A Closure which simply creates a scope that allows the function to access and manipulate the variables in enclosing scopes. Normally, you will follow below steps to create a Closure in Python:
- We have to create a nested function (a function inside another function).
- This nested function has to refer to a variable defined inside the enclosing function.
- The enclosing function has to return the nested function
Below is a simple example to create closure:
In [10]: def addN(n):
...: def _add(v):
...: return v + n
...: return _add
...:
In [11]: addOne = addN(1)
In [12]: addOne(2)
Out[12]: 3
In [13]: addOne(3)
Out[13]: 4
In [14]: addTwo = addN(2)
In [15]: addTwo(2)
Out[15]: 4
In [16]: addTwo(3)
Out[16]: 5
Currying is like a kind of incremental binding of function arguments. It is the technique of breaking down the evaluation of a function that takes multiple arguments into evaluating a sequence of single-argument functions:
- Concept by Haskell Curry
- Translating a function that takes multiple arguments into a sequence of functions which all take 1 argument. e.g.:
add(a, b)
AND add(a)(b)
- Improves reusability and composition
- In some languages (Haskell, F#) functions are curried by default
Unfortunately, Python doesn't support curring in default. Below is a workaround for you to do curring in Python3:
from inspect import signature
def curry(x, argc=None):
if argc is None:
argc = len(signature(x).parameters)
def p(*a):
if len(a) == argc:
return x(*a)
def q(*b):
return x(*(a + b))
return curry(q, argc - len(a))
return p
@curry
def myfun(a,b,c):
print("{}-{}-{}".format(a,b,c))
myfun(1, 2, 3)
myfun(1, 2)(3)
myfun(1)(2)(3)
Recursion instead of loops/iteration
FP favors recursion over for-loop. However, the recursion will use precious resource as stack. You can use below sample code to retrieve the recursive limit:
In [17]: import sys
In [18]: sys.getrecursionlimit()
Out[18]: 3000
This package use class TailCall to store the function call in heap instead of stack. Below is one usage example:
In [1]: def fibRec(n, x=0, y=1):
...: if n == 0:
...: return x
...: else:
...: return fibRec(n-1, y, x + y)
...:
In [2]: fibRec(3)
Out[2]: 2
In [3]: fibRec(3000)
---------------------------------------------------------------------------
RecursionError Traceback (most recent call last)
<ipython-input-3-035cf1755b78> in <module>
----> 1 fibRec(3000)
<ipython-input-1-f509e891ef84> in fibRec(n, x, y)
3 return x
4 else:
----> 5 return fibRec(n-1, y, x + y)
6
... last 1 frames repeated, from the frame below ...
<ipython-input-1-f509e891ef84> in fibRec(n, x, y)
3 return x
4 else:
----> 5 return fibRec(n-1, y, x + y)
6
RecursionError: maximum recursion depth exceeded in comparison
The exception is raised owing to recursion limitation. We can get by this limition by adopting TailCall:
In [5]: from fpu.fp import *
In [6]: ret = TailCall.ret; sus = TailCall.sus
In [22]: def fib(n, x=0, y=1):
...: return ret(x) if n == 0 else sus(Supplier(fib, n-1, y, x + y))
...:
In [23]: fib(3)
Out[23]: <fpu.fp.Suspend at 0x7f2be96be710>
In [24]: fib(3).eval()
Out[24]: 2
In [25]: fib(3000).eval()
Out[25]: 410615886307971260333568378719267105220125108637369252408885430926905584274113403731330491660850044560830036835706942274588569362145476502674373045446852160486606292497360503469773453733196887405847255290082049086907512622059054542195889758031109222670849274793859539133318371244795543147611073276240066737934085191731810993201706776838934766764778739502174470268627820918553842225858306408301661862900358266857238210235802504351951472997919676524004784236376453347268364152648346245840573214241419937917242918602639810097866942392015404620153818671425739835074851396421139982713640679581178458198658692285968043243656709796000
Pro & Con of FP
Here we will be going to review what advantage/disadvantage FP will bring to you.
Advantages of FP
- Absence of side effects can make your programs more robust
- Programs tend to be more modular come and typically in smaller building blocks
- Better testable - call with same parameters always returns same result
- Focus on algorithms
- Conceptional fit with parallel / concurrent programming
- Live upates - Install new release while running
Disadvantages of FP
- Solutions to the same problem can look very different than procedural/OO ones
- Finding good developers can be hard
- Not equally useful for all types of problems
- Input/Output are side effects and need special treatment
- Recursion is "an order of magnitude more complex" than loops/iterations
- Immutable data structures may increase run times
Python's Functional Features - Overview
- Pure functions (sort of)
- Closures - hold state in functions
- Functions as objects and decorators
- Immutable data types (tuple, freezeSet)
- Lazy evaluation - generators
- List (dictionary, set) comprehensions
- functools, itertools, lambda, map, filter
- Recursion - try to avoid, recursion limit has a reason
Example API Usage
Here we are going to look at few examples from HackerRank to know how FP can help you write code gracefully.
Gemstones
The problem simply ask you to extract element exist in every rock. The imperative approach will look like:
arr = ['abcdde', 'baccd', 'eeabg']
def gemstones_imp(arr):
set_list = []
for s in arr:
set_list.append(set(list(s)))
uset = None
for aset in set_list:
if uset is None:
uset = aset
else:
uset = uset & aset
return len(uset)
print("Output of gemstones_imp={}".format(gemstones_imp(arr)))
The FP (declarative approach) code will be neat and graceful:
from fpu.flist import *
def gemstones_dec(arr):
rlist = fl(arr)
return len(
rlist.map(lambda r: set(list(r))) \
.reduce(lambda a, b: a & b)
)
print("Output of gemstones_imp={}".format(gemstones_dec(arr)))
Supplement