Slide 1

Slide 1 text

Functional Programming for the Uninitiated gavin bong pycon asia 2012, Singapore le 7 juin 2012 2229 jeudi

Slide 2

Slide 2 text

2 roadmap functional way high order functions recursion folds curry / partial app. parser combinators closing 30 mins mins 02 02 08 08 mins 06 mins 03 mins mins mins min 01

Slide 3

Slide 3 text

“ ” Purely functional is the right default. Imperative constructs .. must be exposed through explicit effects-typing constructs. Tim Sweeney, EPIC games (2006)

Slide 4

Slide 4 text

No matter what language you work in, programming in a functional style provides benefits. You should do it whenever it is convenient, and you should think hard about the decision when it isn’t convenient. “ ” John Carmack, ID Software 26th april 2012

Slide 5

Slide 5 text

The functional way

Slide 6

Slide 6 text

Side effect free: Use immutable containers. Recursion over iteration First-class functions & closures. Higher-order functions everywhere Lazy evaluation Functional way: a cook's tour Pure functions (referential transparency) sin(n), log2(n) Composable Execution order insignificant Data transformations e.g. XSLT etc...

Slide 7

Slide 7 text

Is python a functional language? anonymous functions (a.k.a lambdas) map, filter, reduce itertools functools list comprehensions, generators It is an imperative language that has acquired some functional features. No

Slide 8

Slide 8 text

Functional aspects #1 Immutable containers in python tuple frozenset lambdas in python are limited to a single expression. Types (haskell as a reference): Pattern matching does not exist in python. Unlike python, haskell has static type checking with type inference. sum :: (Num a) => [a] -> a

Slide 9

Slide 9 text

High order functions

Slide 10

Slide 10 text

In python 3.x, map returns an iterator. Thus itertools.imap is removed. High order functions Accept functions as arguments and/or return functions. map map(f, [a1, a2])  [f(a1), f(a2)] filter filter(predicate, [a1, a2]) reduce Detail treatment in upcoming slides.

Slide 11

Slide 11 text

Functional aspects #2 [(x, y) for x in xrange(1, 6) for y in xrange(6, 11) if y-x == 3] List comprehension in python [(x, y) | x <- [1..5], y <- [6..10], y-x == 3] is inspired by haskell's syntax: List comprehension has largely superceded the utility of the filter HOF.

Slide 12

Slide 12 text

Recursion

Slide 13

Slide 13 text

Recursion Functional languages do NOT have looping constructs. e.g. for, while Execution of recursive functions typically uses a stack. e.g. naïve sum function. sum :: [Integer] -> Integer sum [] = 0 sum (x:xs) = x + sum xs # haskell

Slide 14

Slide 14 text

def sum_(seq): if not seq: return 0 else: return seq[0] + sum_(seq[1:]) # python naïve sum function This does not work if you pass in an iterator (e.g. generator expression) as the argument. We will fix this in upcoming slides.

Slide 15

Slide 15 text

NOT tail recursive sum_([1, 2, 3]) 1 + sum_([2, 3]) 1 + (2 + sum_([3])) 1 + (2 + (3 + sum_([]))) 1 + (2 + (3 + 0)) 1 + (2 + 3) 1 + 5 6 Stack frames have to be kept around to maintain intermediate values. >>> import sys; sys.getrecursionlimit() 1000 The “stack” is a scarce resource.

Slide 16

Slide 16 text

Old technique: use an accumulator argument to make it tail-recursive. A recursive function is tail recursive if the return value of the recursive call is the final result of the function. tail recursive conversion >>> sum_([i for i in xrange(1000)]) RuntimeError: maximum recursion depth exceeded fibonacci(n-1) * fibonacci(n-2) seq[0] + sum_(seq[1:]) Example of non tail-recursive calls. sum :: [Integer] -> Integer -> Integer sum [] acc = acc sum (x:xs) acc = sum xs (x + acc) # haskell

Slide 17

Slide 17 text

Many functional languages (like Scheme) can support an unbounded number of active tail calls (recursive or otherwise). Haskell being a lazy language has a different approach. tail recursion elimination def sum_(seq): def tail_sum(seq, acc): if not seq: return acc else: return tail_sum(seq[1:], seq[0]+acc) return tail_sum(seq, 0) Python does not perform Tail Call Optimization (a.k.a Tail Recursion Elimination). The BDFL objects to this. O(n) => O(1)

Slide 18

Slide 18 text

Recursion in Python? In CPython, Prefer iteration over recursion. Avoid the python stack. Trampoline technique Use core python features. e.g. __builtins__.sum A trampoline is a function executor. Works for tail-recursive functions (and also coroutines with tail-calls). Avoids stack overflow.

Slide 19

Slide 19 text

Trampolines

Slide 20

Slide 20 text

trampoline #1 thunk = lambda fn: lambda *args: lambda: fn(*args) def tail_sum(seq, acc): it = iter(seq) try: first, rest = (next(it), list(it)) except StopIteration: return acc else: return thunk(tail_sum)(rest, first + acc) Delayed computation Trampoline code is based on example by James Tauber @jtauber

Slide 21

Slide 21 text

trampoline #2 In python 3.x (see PEP3132), we can simplify the code that extracts the head & tail of the iterator. def tail_sum(seq, acc): it = iter(seq) try: first, *rest = it except ValueError: return acc else: return thunk(tail_sum)(rest, first + acc) #python 3

Slide 22

Slide 22 text

trampoline #3 def trampoline(bouncer): while callable(bouncer): # should we land yet? bouncer = bouncer() return bouncer def sum_(seq): return trampoline(thunk(tail_sum)(seq, 0)) Trampoline code is based on example by @jtauber bounce land >>> sum_(i for i in range(2000)) 1999000

Slide 23

Slide 23 text

trampoline #4 Trampoline version highly inefficient. 784 μsecs >>> sum( i for i in xrange(10000) ) 49995000 Comparisons of benchmarks using timeit : >>> sum_( i for i in xrange(10000) ) 49995000 1.66 secs >>> sum_( i for i in xrange(100000)) 4999950000L 150 secs 7.79 msecs >>> sum( i for i in xrange(100000)) 4999950000L

Slide 24

Slide 24 text

Catamorphisms

Slide 25

Slide 25 text

It reduces a list to a single value. fold sum [] = 0 sum (x:xs) = x + sum xs # haskell It encapsulates the recursive pattern of processing data structures (lists for simplicity).

Slide 26

Slide 26 text

fold sum [] = 0 sum (x:xs) = (+) x (sum xs) # haskell It encapsulates the recursive pattern of processing data structures (lists for simplicity). [2] Graham Hutton – A tutorial on the universality & expressiveness of fold, 1999 Universal property [2] of the fold operator: g [] = z g (x:xs) = f x (g xs)  fold f z Thus, sum simplifies to: Prelude> foldl (+) 0 [1..3] 6 arity 2 finite lists It reduces a list to a single value.

Slide 27

Slide 27 text

left vs right fold In haskell, use foldl for folding from the left (the start) and foldr for folding from the right (the end). Prelude> let f = (\acc x -> 1 + acc) Prelude> foldl f 0 [0..3] 4 Example: finding the length 0 0 f f 1 f 2 f 3 left fold

Slide 28

Slide 28 text

left vs right fold #2 Prelude> let g = (\x acc -> 1 + acc) Prelude> foldr g 0 [0..3] 4 Example: finding the length 3 0 g g 2 g 1 g 0 right fold

Slide 29

Slide 29 text

fold in python In python 2.x, __builtins__.reduce is the python version of the fold operator. Left & right folds are handled by same function. def reverse(seq): def r(acc, x): acc.insert(0, x) return acc return reduce(r, seq, []) Example: reverse a list Let's implement haskell's elem using right fold. Prelude> elem 2 [1..3] True Prelude> elem 0 [1..3] False

Slide 30

Slide 30 text

def elem(item, seq): _seq = reverse(seq) def match(x, acc): if x == item: return True else: return acc return reduce(lambda a, b: match(b, a), _seq, False) Example: haskell's elem using right fold. The initial value is set to False. The accumulator remains False until a match is found.

Slide 31

Slide 31 text

784 μsecs >>> sum(i for i in xrange(10000) ) Comparisons of benchmarks using timeit : >>> reduce(operator.add, i for i in xrange(10000), 0) 1.38 msecs >>> reduce(operator.add, i for i in xrange(100000), 0) 13.6 msecs 7.79 msecs >>> sum(i for i in xrange(100000)) Simple benchmark of reduce >>> import operator

Slide 32

Slide 32 text

In python 3.x, __builtins__.reduce has been moved to functools.reduce Several properties of reduce reduce( lambda a, b: a | b , [True, False, False], False ) # any reduce does not short-circuit; which would explain why it is slower than the builtin any or all for use cases below: reduce( lambda a, b: a & b , [False, False, True], False ) # all Homework: Implement map & filter using reduce.

Slide 33

Slide 33 text

Currying & partial application

Slide 34

Slide 34 text

Curried functions Currying is the technique of transforming a function that takes multiple arguments into .. a chain of unary functions. Example: currying functions of arity 2 >>> import operator >>> assert operator.sub(1, 2) == -1operator >>> def curry2(f): return lambda a: lambda b: f(a, b) >>> curried_sub = curry2(operator.sub) >>> assert curried_sub(1)(2) == -1

Slide 35

Slide 35 text

Example: uncurrying functions of arity 2 uncurry >>> def uncurry2(f): return lambda a, b: f(a)(b) >>> original_sub = uncurry2(curried_sub) >>> assert original_sub(1, 2) == -1 All functions are curried in Haskell. [1] subtract :: (Num a) => a -> a -> a as oppose to the uncurried form: subtract :: (Num a) => (a, a) -> a

Slide 36

Slide 36 text

Benefits of curried functions What is the point of this ? Theoretical – able to treat functions uniformly. Practical – aids in the creation of partially applied functions. Bind some values to some (but not all) of the arguments of a curried function. Partial application # haskell ghci> map (2*) [0, 1, 2] [0, 2, 4] ghci> map (subtract 1) [0, 1, 2] [-1, 0, 1] PEP 309 standardizes a partial object (since python 2.5).

Slide 37

Slide 37 text

functools.partial in action >>> operator.sub.__doc__ 'sub(a, b) – same as a - b.' >>> subtract = partial(operator.sub, 1) >>> subtract(2) # evaluating 1 - 2 -1 >>> subtract.keywords {} >>> subtract.args (1,) Caveat ! Since operator is a C module, you cannot bind values to specific positional arguments.

Slide 38

Slide 38 text

functools.partial in action #2 The limitation described in the last slide does not apply to user defined functions. Thus, >>> def minus(a, b): 'clone of operator.sub' return a - b >>> subtract2 = partial(minus, b=2) >>> subtract2.keywords {'b': 2} >>> subtract2.__doc__ 'partial(func, *args, **keywords) ...' >>> from functools import update_wrapper >>> update_wrapper(subtract2, minus) >>> subtract2.__doc__ 'clone of operator.sub'

Slide 39

Slide 39 text

misc partial can be applied to classes, class methods and instance methods. partial can be used to create thunks (as opposed to hand-crafted lambdas) It is ok to imagine operator.itemgetter & operator.attrgetter as curried functions (although their implementation does not match that mental model).

Slide 40

Slide 40 text

functools.wraps Proxying to callables for creating decorators. In conjunction with functools.update_wrapper & functools.partial, the wrapped function's __name__, __module__ and __doc__ are preserved. e.g. Django view decorator @csrf_exempt Example: decorator to measure running time def queue(fn, duration): # send timings asynchronously pass

Slide 41

Slide 41 text

import time from functools import wraps def timer(f): @wraps(f) def wrapper(*args, **kwargs): start = time.time() f(*args, **kwargs) end = time.time() queue(f.__name__, end – start) return wrapper @timer def user_function(a, b, c): pass

Slide 42

Slide 42 text

functools in python 3.x functools.lru_cache Memoization decorator that caches results of the wrapper function. Improves performance of tree recursive functions e.g. fibonacci.

Slide 43

Slide 43 text

Parser combinators: a case study

Slide 44

Slide 44 text

Parser combinators A real world application of higher order functions. A parser is built up from smaller primitive parsers. Parser combinators are just higher-order functions. Parser :: String AST → Combinator :: Parser Parser Parser → → Thus, grammar construction for things like repetition , sequencing or choice is modelled using combinators. Example: trivial marathon running time parser using funcparselib. parse(“02:15:25”)==(2,15,25)

Slide 45

Slide 45 text

Parser combinators #2 tokenize: String [Token] → parse: [Token] tuple → >>> tokenize('2:15:25') [Token('PositiveInteger', '2'), Token('Colon', ':'), Token('PositiveInteger', '15'), Token('Colon', ':'), Token('PositiveInteger', '25')] >>> parse(tokenize('2:15:25')) (2, 15, 25)

Slide 46

Slide 46 text

Parser combinators #3 grammar = (hour + colon + minute + colon + second + skip(finished)) grammar.parse(seq) #seq is [Token] Complete code at http://pastebin.com/k1MdUHSi make_num = lambda n: int(n) tokval = lambda x: x.value hour = some(lambda x: pos_int(x) and within(x.value,7)) >> tokval >> make_num

Slide 47

Slide 47 text

The end Learning FP will make you a better (python) programmer. Theory Study Category theory & Type theory. Play with other FP languages Haskell, OCaml, Javascript Thank you