Skip to main content

Functional Programming Utility

Project description

Build Status
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.

Source Distribution

fpu-0.2.51.tar.gz (17.2 kB view details)

Uploaded Source

Built Distribution

fpu-0.2.51-py3-none-any.whl (13.0 kB view details)

Uploaded Python 3

File details

Details for the file fpu-0.2.51.tar.gz.

File metadata

  • Download URL: fpu-0.2.51.tar.gz
  • Upload date:
  • Size: 17.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/1.13.0 pkginfo/1.5.0.1 requests/2.31.0 setuptools/44.0.0 requests-toolbelt/0.9.1 tqdm/4.31.1 CPython/3.8.10

File hashes

Hashes for fpu-0.2.51.tar.gz
Algorithm Hash digest
SHA256 5b72af0c1b76a023ba20ebe25e4ef8be9c63af3a3d52029253f62afdb1aaf3ec
MD5 c4924f4f9c1c450f19ac251a7889bb83
BLAKE2b-256 635331befe3d6a61fc371ae2d7be7da02f0bf9bdd1a78e0134ed72e0bb14b0b3

See more details on using hashes here.

File details

Details for the file fpu-0.2.51-py3-none-any.whl.

File metadata

  • Download URL: fpu-0.2.51-py3-none-any.whl
  • Upload date:
  • Size: 13.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/1.13.0 pkginfo/1.5.0.1 requests/2.31.0 setuptools/44.0.0 requests-toolbelt/0.9.1 tqdm/4.31.1 CPython/3.8.10

File hashes

Hashes for fpu-0.2.51-py3-none-any.whl
Algorithm Hash digest
SHA256 65bccb864e2cae9d03e755642681e3fb4027dc6722f92b83eea5d227c7fe3726
MD5 b14663f3db420f384d6f70cb5e5265c5
BLAKE2b-256 0fbb65bb8fb57da28375395c8ae24a916c4f1703fa5a4db24253931bea39f32e

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page