Functional programming
For Data Scientists
About this talk
Expectation
beginners
Reality
experienced
Why FP for Data Scientists?
Uniformity
Micro
Code
ETL
=
Functional Programming
The Theory of Everything
Define we must...
What is Functional Programming?
History first
Inspired by
Lambda Calculus
History first
First FP language
LISP
Designed by
John McCarthy
History first
In 1977, while introducing FP
John Backus
Defined…
Functional Programs
as...
History first
Being built up in a hierarchical way by means of "combining forms" that allow an "algebra of programs”.
History first
We even had a
Lisp Machine
Precursor of
Garbage collector, LAN
And about that definition...
Let’s give it a try...
FP Definition (our attempt)...
A style of programming based on writing declarative code (in contrast to imperative) that favors:
Breaking down the definition
Our view
Programming style
Breaking down the definition
Are you a functional programmer?
This is the best functional code I’ve seen…
Let’s keep it flexible...
Breaking down the definition
Important
Immutability
Immutability
Usually, a decision to make when solving a problem...
Immutability
l = ['a', 'b']��new_l = l + ['c']
l = ['a', 'b']��l.append('c')
Mutable
Immutable
Immutability
Benefits:
> s[0]�=> 'h'
> s�=> 'hello
> s = "hello"�> s + " world"�=> 'hello world'
Python favors immutability
Pandas favors immutability
df.replace(inplace=True)�df.drop(inplace=True)�df.dropna(inplace=True)�df.rename(inplace=True)�df.fillna(inplace=True)�df.set_index(inplace=True)
Numpy favors immutability
How can you append an element to a numpy array?
Numpy favors immutability
Breaking down the definition
This is bad
Side Effects
Really. Bad.
Side Effects involve
Side Effects example: the wrong way
db_conn = connect()��def compute_earnings():� year = input("What year? ")� df = db_conn.select(� from='earnings', year=year)� # do your computations� results = compute(df, year)� db_conn.write(results, year)� print("All done!")
Side Effects example: the wrong way
db_conn = connect()��def compute_earnings():� year = input("What year? ")� df = db_conn.select(� from='earnings', year=year)� # do your computations� results = compute(df, year)� db_conn.write(results, year)� print("All done!")
Side Effects example: the wrong way
db_conn = connect()��def compute_earnings():� year = input("What year? ")� df = db_conn.select(� from='earnings', year=year)� # do your computations� results = compute(df, year)� db_conn.write(results, year)� print("All done!")
Who to blame if something fails?
Side Effects example: the correct way
def compute_earnings(data, year):� # Only our computations� return compute(data, year)�
def main():� year = sys.argv[0]� db_conn = connect()� df = db_conn.select(� from='earnings', year=year)�� results = compute_earnings(df, year�� db_conn.write(results, year)
assert compute_earnings(data, 2016) == X�assert compute_earnings(data, 2015) == Y
Benefits: testability
assert compute_earnings(data, 2016) == X�assert compute_earnings(data, 2015) == Y
Benefits: testability
Known
Input and Output
def scheduled_job():� # ...� results = compute_earnings(df, year)
�def main():� # ...� results = compute_earnings(df, year)
�def task_queue_job():� # ...� results = compute_earnings(df, year)
Benefits: reusability & modularization
The main benefit: reduced complexity
The main benefit: reduced complexity
Therac-25, a radiation therapy machine used in the 80s, contained a bug that costed the lives of at least 3 patients.
The bug was caused by a deadly side effect. An incorrect combinations of keys could alter the global state of the machine, leading to race conditions.
Therac-25 biggest problem: it was impossible to test.
Side effects are bad...
How to avoid side effects?
By using...
Pure Functions
A pure function is...
A pure function...
Let us reason about our program better. They’re intuitive and predictable. We can combine them, test them, cache them, parallelize them without problems.
They can also be processed lazily (more on this later).
Pure functions are: testable (and reasonable).
assert add(2, 3) == 5�assert add(-1, 3) == 2
Pure functions are: composable
def subtract(x, y):� return add(x, y * -1)���assert subtract(5, 2) == 3
Pure functions are: cacheable.
from functools import lru_cache��@lru_cache�def add(x, y):� return x + y��add(2, 3) # computed�add(2, 3) # cached�add(2, 3) # cached
def square_list(a_list):� return parallel(� a_list[:int(len(l) / 2)]� ) + parallel(� a_list[int(len(l) / 2):]� )
Pure functions are: parallelizable.
And pure functions...
Lead us to
Referential Transparency
The ability of swapping a function invocation with its corresponding value, without affecting the program's behavior.
Referential transparency is just...
Referential transparency
msg = "The result is: %s" % add(2, 3)��
msg = "The result is: %s" % 5
Back to the definition
FP Definition (our attempt)...
A style of programming based on writing declarative code (in contrast to imperative) that favors:
Back to the definition
Favor
Function composition
The concept is simple, can be seen as nested function definitions:
Function composition
f(g(h(x)))
But thinking in this way it’ll make us:
Function composition
Other important concepts are:
They’re related, but there are differences.
Function composition
Functions are first class objects (in Python)
def add(x, y):� return x + y��my_sum = add��def compute(x, y, op):� return op(x, y)��compute(1, 2, my_sum)
Functions are first class objects (in Python)
def add(x, y):� return x + y��my_sum = add��def compute(x, y, op):� return op(x, y)��compute(1, 2, my_sum)
Functions are first class objects (in Python)
def add(x, y):� return x + y��my_sum = add��def compute(x, y, op):� return op(x, y)��compute(1, 2, my_sum)
add
add
Functions are first class objects (in Python)
def add(x, y):� return x + y��my_sum = add��def compute(x, y, op):� return op(x, y)��compute(1, 2, my_sum)
Finally: Lambdas
def add(x, y):� return x + y���add = lambda x, y: x + y
Finally: Lambdas
def add(x, y):� return x + y���add = lambda x, y: x + y
Back to the definition
FP Definition (our attempt)...
A style of programming based on writing declarative code (in contrast to imperative) that favors:
Back to the definition
Favor
Declarative style
Imperative: When you explicitly specify the steps required to achieve a given result.
Declarative: When you declare (or express) what you want to be achieved.
Declarative vs Imperative
Example: our imperative robot cook
Example: our imperative robot cook
fridge = open_thing(x, y)�eggs = get_eggs(fridge, 4)
�shelve = open_thing(x, y)�flour = get_flour(shelf, 200)
�while time < 5:� mixture = mix(flour, eggs)� time += 1
�bake(mixture)
Example: our imperative robot cook
fridge = open_thing(x, y)�eggs = get_eggs(fridge, 4)
�shelve = open_thing(x, y)�flour = get_flour(shelf, 200)
�while time < 5:� mixture = mix(flour, eggs)� time += 1
�bake(mixture)
What is the code doing?
Imperative code
Example: our declarative robot cook
Example: our declarative robot cook
bake(� prepare_mixture(� cake(� flavor='chocolate', eggs=4)))
Declarative code
This is IMPORTANT
A = np.array([� [1, -1, 2],� [0, -3, 1]�])��x = np.array([2, 1, 0])
Abstraction example: dot product
1
-1
2
0
-3
1
1
2
0
⋅
result = []�row_count = 0��for row in A:� partial_result = 0� for i in range(len(row)):� a_elem = row[i]� x_elem = x[i]� partial_result += (a_elem * x_elem)� result.append(partial_result)� row_count += 1
Abstraction example: imperative solution
A = np.array([� [1, -1, 2],� [0, -3, 1]�])��x = np.array([2, 1, 0])��A @ x # np.dot(A, x)�# np.array([1, -3])
Abstraction example: declarative solution
How is this implemented internally?
A = np.array([� [1, -1, 2],� [0, -3, 1]�])��x = np.array([2, 1, 0])��A @ x # np.dot(A, x)�# np.array([1, -3])
Abstraction example: declarative solution
But, doesn’t it feel like cheating?
result = []�row_count = 0��for row in A:� partial_result = 0� for i in range(len(row)):� a_elem = row[i]� x_elem = x[i]� partial_result += (a_elem * x_elem)� result.append(partial_result)� row_count += 1
A @ x
vs.
The key of declarative programming is
A higher level of
Abstraction
Our robot understands primitives like bake or prepare_mixture. Which are high level primitives.
The key is learning this language of high level abstractions.
High level of abstraction
Finding the balance between something too high level that can’t be interpreted.
And something too low level that becomes imperative.
High level of abstraction
Functional programming building blocks
The most popular
FP abstractions
Functional programming building blocks
map, filter & reduce
Functional programming building blocks
Take this
map
Give me that
map: let the code explain it
map(lambda x: x ** 2, [1, 2, 3, 4])��>>> [1, 4, 9, 16]
map: let the code explain it
2
1
3
f(x) => x²
4
4
1
9
16
map: let the code explain it
2
1
3
f(x) => x²
4
4
1
9
16
f(x) => x²
f(x) => x²
f(x) => x²
map: visually
Original elements
Transformation
Result elements
map: does it look familiar?
?
map: does it look familiar?
[x ** 2 for x in (1, 2, 3, 4)]��>>> [1, 4, 9, 16]
map: does it look familiar?
arr = np.array([1, 2, 3, 4])��arr ** 2��# > array([ 1, 4, 9, 16])
Functional programming building blocks
Take this
filter
if...
filter: let the code explain it
filter(lambda x: x > 2, [1, 2, 3, 4])��>>> [3, 4]
filter: let the code explain it
2
1
3
f(x) => x > 2
4
3
4
filter: let the code explain it
2
1
3
f(x) => x > 2
4
3
4
f(x) => x > 2
f(x) => x > 2
f(x) => x > 2
filter: visually
Original elements
Result elements
blue-ish
elements
filter: does it look familiar?
?
filter: does it look familiar?
[x for x in (1, 2, 3, 4) if x > 2]��>>> [3, 4]
filter: does it look familiar?
arr = np.array([1, 2, 3, 4])��arr > 2�# > array([False, False, True, True])��arr[arr > 2]�# > array([3, 4])
(also known as fold)
reduce
Squash these things...
Functional programming building blocks
reduce: let the code explain it
reduce(lambda acc, y: acc + y, [1, 2, 3, 4], 0)��>>> 10
combination
reduce: visually
reduce: let the code explain it
reduce(lambda acc, y: acc * y, [1, 2, 3, 4], 1)��>>> 24
reduce: let the code explain it
Important!
No longer part of stdlib
from functools import reduce�reduce(lambda acc, y: acc * y, [1, 2, 3, 4], 1)��>>> 24
Almost every time I see a reduce() call (...), I need to grab pen and paper to diagram what's [that function] supposed to do.
reduce: why was removed from stdilib
Guido van van Rossum
Creator of Python
what about fold “fold”?
# reduce: initializer is 1st element�reduce(lambda acc, y: acc * y, [1, 2, 3, 4])���# fold: initializer is passed as argument�reduce(lambda acc, y: acc * y, [1, 2, 3, 4], 1)
Does “map” and “reduce” sound familiar?
Does “map” and “reduce” sound familiar?
The theory of everything
The theory of everything
words = (� (w.lower(), 1)� for w in text.split() if w)��dict([� (word, len(list(group)))� for word, group in� groupby(� sorted(words), key=lambda d: d[0]� )�])
What’s the difference between
map, filter
And
List comprehensions
You might wonder...
The difference is: laziness
from operator import pow�square = lambda x: pow(x, 2)��map(square, [1, 2, 3, 4])�# => <map object at 0x7f2300fc7198>��[square(x) for x in [1, 2, 3, 4]]�# => [1, 4, 9, 16]
Want to learn more about Data Science?
https://ine.com/pages/data-science