Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Functional Programming in python for the uninitiated

GavinB
June 10, 2012

Functional Programming in python for the uninitiated

FP in python

GavinB

June 10, 2012
Tweet

More Decks by GavinB

Other Decks in Programming

Transcript

  1. 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
  2. “ ” Purely functional is the right default. Imperative constructs

    .. must be exposed through explicit effects-typing constructs. Tim Sweeney, EPIC games (2006)
  3. 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
  4. 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...
  5. 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
  6. 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
  7. 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.
  8. 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.
  9. 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
  10. 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.
  11. 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.
  12. 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
  13. 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)
  14. 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.
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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).
  20. 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.
  21. 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
  22. 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
  23. 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
  24. 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.
  25. 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
  26. 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.
  27. 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
  28. 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
  29. 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).
  30. 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.
  31. 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'
  32. 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).
  33. 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
  34. 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
  35. 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.
  36. 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)
  37. 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)
  38. 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
  39. 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