Skip to main content

Functional Programming Utility

Project description

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]}]

# Imperative
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) # 3

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:

# FP
from fpu.fp import *
from functools import reduce, partial

# compose2(f, g) = f(g())
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) # Any operation on v1 will return new vectory to reflect the modification

In [4]: v1                                                                      
Out[4]: pvector([1, 2, 3])  # v1 stay immutable

In [5]: v2                                 
Out[5]: pvector([1, 2, 3, 4])  # v2 reflect the change for appending 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']
# Complete the gemstones function below.
def gemstones_imp(arr):
    set_list = []
    for s in arr:
        set_list.append(set(list(s)))

    # Imperative code
    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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Files for fpu, version 0.2.5
Filename, size File type Python version Upload date Hashes
Filename, size fpu-0.2.5-py3-none-any.whl (11.6 kB) File type Wheel Python version py3 Upload date Hashes View hashes
Filename, size fpu-0.2.5.tar.gz (12.9 kB) File type Source Python version None Upload date Hashes View hashes

Supported by

Elastic Elastic Search Pingdom Pingdom Monitoring Google Google BigQuery Sentry Sentry Error logging AWS AWS Cloud computing DataDog DataDog Monitoring Fastly Fastly CDN DigiCert DigiCert EV certificate StatusPage StatusPage Status page