Purely Functional Programming in Python: Pure Fun

Purely Functional Programming in Python: Pure Fun

This is an introduction to the concepts of purely functional programming in Python. I go over the concepts and techniques of FP, and also talk about specific libraries you can use to help you out with FP.

I gave this talk at PyTennessee 2015.

F408a7e7bbf1d41c66bfda80747ea4c5?s=128

Christopher Armstrong (radix)

February 07, 2015
Tweet

Transcript

  1. pure fun in python yeah Christopher Armstrong https://github.com/radix/effect @radix -

    Introduction - Twisted, if you know it - Effect (talk about later) - Rackspace, Auto Scale, we do FP! - Go!
  2. i am not going to say it monad I will

    not say the word on this slide at any point during this talk.
  3. hundreds of years ago http://yellow5.com/pokey/archive/index264.html — @stevehavelka heard from someone

    that every great talk starts with a lengthy section on history. - Pascal, the first programming language, had two features: functions and procedures. - Functions return, procedures had side-effects. - we made a distinction then, most modern languages don’t.
  4. “pure” Function, n. (plural functions) A relation between a set

    of inputs and a set of permissible outputs with the property that each input is related to exactly one output. -Wikipedia - title of this talk is “PURELY functional programming” - shouldn’t have to say that. - a distinction was lost: “functions” changed meaning. - <click> - I’ve also heard that every great talk has definition slides. - <read> - No room for side-effects. Can’t delete money from your bank account. - Most don’t know/care when they write funcs or procedures. So, what is pure FP? just programming with functions. :-)
  5. not for fun >:( - not here to tell you

    this for fun - not just encouraging you to expand your mind - people just go back to familiarity - YOLO state manipulation and OO design are a big source of problems - pure FP seriously improves our code
  6. j/k it’s super fun :D - ok, ok, it’s actually

    really fun to do functional programming! - check it out! - learn some stuff!
  7. definition #2 YOLO, v, t. Modifying state or performing IO

    in a function. For the rest of this talk, I’ll refer to side-effects as “YOLO”.
  8. why 1. reason 2. express 3. test 4. perform Let

    me tell you about why FP is good for you. - <click> Reason about code in a way impossible with YOLO. - <click> express code concisely/meaningfully. abstract mechanisms for composing funcs. - <click> WAY easier to test. Call func with args, check result. - <click> Opportunity for speed for PFP, parallelism
  9. 1. reason "See, I told you they'd listen to Reason.”

    -Fisheye (Snow Crash reference) - substantial effect on understanding of code - greater confidence - “equational reasoning”, function call = return value. - <click> for snow crash reference - an example…
  10. reasoning f(a(‘foo’), b(c(‘bar’))) - code - If these functions YOLO,

    we need to carefully search for side-effects. - If we know they’re pure, it’s much easier. Let me demonstrate.
  11. reasoning >>> a(‘foo’)
 1
 >>> c(‘bar’)
 2
 >>> b(2)
 3


    >>> f(1, 3)
 “Everything is awesome!” - This is what it’s like to experiment and trace execution. - It’s just like when you learned Python. Try stuff out. - May seem trite. Surely, anyone can do this kind of stuff with their Python code?
  12. sad REPL syndrome foo.bar(3)? Foo(dbconn, auth) INSERT INTO txn_log (event,

    owner, value mock.return_value = MockAuthenticationResponse() def foo(val): … print stuff return stuff Q: ever worked on a big project where it’s hard use the REPL? <click> “understand foo.bar(3)”. Easy? <click> but class needs DB connobj, authobj -> HTTP req, <click> and method YOLOs txn logs into DB. <click> Have mocks in tests, -> 5 minutes to find & put together <click> End up adding prints to func, try reproduce behavior in testing env. Happens to me! All the time! It’s a tragedy! slows dev cycle.
  13. happy REPL pure functions better factored dependencies :-) - If

    most code PF, you CAN use the REPL. - Functional code naturally minimizes deps between comps - end up w/ wonderfully factored little funcs, easy to evaluate in REPL.
  14. 2. express result = map(comp(f, g), inputs) not result =

    []
 for input in inputs:
 x = f(g(input))
 result.append(x) - FP lets us specify things in a way that exposes *meaning* more clearly - lots of FP advocates say that FP style is “say what you want, not how to get it” - it’s rhetoric, but has a grain of truth - more on this later when I talk about “style”
  15. 3. test call a function. pass it some arguments. check

    its return value. - Call func w/ args, check return. That’s it. - Not how most Python code is tested these days. - Mocking has become absolute *nightmare*. mocks -> mocks -> mocks - need track complex YOLOs - Tests too coupled to implementation. very hard to change your code. - care about this a lot. Really into unit testing. I like 100% test coverage. - I still mock (or parameterize deps), but the mocks are much more tasteful in FP
  16. 4. perform PyPy. STM. (go fast) - PyPy. Obviously. -

    Watch PyPy’s STM - STM optimistically runs your code concurrently - PFP = trivially parallel. - STM needs more work, but it’s the future. So much that it hurts. And so is FP! - Basically, STM and PFP will kill the GIL.
  17. purity FP in the large - Let’s talk about HOW

    we do pure functional programming. - Most people focus on stylistic parts of FP - <click> FP in the large
  18. don’t change anything • list.append(“foo”) • self.foo = bar •

    global • x = 1; x = 2 purity - don’t change anything - seems crazy? like many paradigms, not so hard once you get used to it
  19. lies def foo(coolstuff):
 coolstuff = list(coolstuff.items())
 coolstuff.append(('extrathing', 'value'))
 return coolstuff

    purity - but those are lies - only thing that matters is you don’t *expose* side-effects.
  20. python def assoc(d, k, v):
 new = d.copy()
 new[k] =

    v
 return new purity - local YOLO often necessary when doing pure FP in Python. - This function replaces a value in a dictionary by copying and mutating. - Pure FP, but local mutation. Easiest way to do it in this case.
  21. functional data purity/data toolz warehouse.python.org/project/toolz - Python has poor built-in

    data structures for FP. - Get stuff done by explicit copy/modify, but it’s inefficient and error-prone. - (e.g. nested mutable objects) - <click> toolz is a nice lib that supplies useful functions.
  22. toolz purity/data >>> from toolz.dicttoolz import assoc
 >>> assoc({'a': 1},

    'b', 2)
 {'a': 1, 'b': 2} - here’s the assoc function from earlier. pretty much does what it looks like. - this is nice, for small data….
  23. big data (structures) • Zone.actors : [Actor] • Actor.health :

    int • (Actor.mana, Actor.inventory) • Actor.attack(target) -> reduce target’s health purity/data - e.g.: game simulation with big world. - thousands of actors, with health/mana/inventory nested inside - inventory may contain hundreds of items too - Copying the whole zone is insanity! How do we do this efficiently without mutation?
  24. persistent data purity/data pyrsistent warehouse.python.org/project/pyrsistent/ - wonderful implementation of “persistent”

    data structures - (not on-disk; it means immutable) - incredibly, surprisingly efficient - shared structure!
  25. linked list a = (1, (2, ())) b = (0,

    a) purity/data - before I explain pyrsistent, an example of the idea of shared structure - pushing/popping a linked list is very efficient. Structure is shared.
  26. slow • push/pop: O(1) • anything else: O(n) purity/data -

    linked lists are limited. - push/pop is fast, but insert/index is slow. - Much more clever data structures exist now, like…
  27. (H)AMT purity/data (Hash) Array Mapped Tries - (HASH) Array Mapped

    Tries - becoming very common - used in the Clojure core data types - in the pyrsistent library for Python.
  28. (H)AMT purity/data - array of integers 0-8 - array is

    chunked up into the leaves of the trie. - Indexing this structure is very essentially O(1), unlike linked lists, which require traversal
  29. (H)AMT purity/data - let’s replace ‘5’ with “beef” - mutation

    is also essentially O(1) (O(log32 N))! - only the nodes on the way to that chunk need to be allocated - all shared structure just gets referenced in the new vector. - brown is old, blue is new.
  30. zone -> zone purity/data from pyrsistent import m, v
 zone

    = m(actors=v(m(health=100, mana=100,
 inventory=v())))
 updated_zone = zone.set_in(
 (‘actors’, 0, ‘health’), 90) - Example of pyrsistent. Let’s see how to change someone’s health. - What do you do with a new world, though? - Well, honestly, to be practical you could just overwrite the old world. - Or you can treat event-handlers as functions of world -> world and continually run them in a purely functional way. - Magic word here would be FRP, but I’m not going to talk too much about that.
  31. IO purity/effects - You’ve been biting your tongue. - “This

    is just a bunch of faffing about. Can’t do _real_ programming with pure FP!” - Useful programs have to communicate with the outside world. - You’re right. But maybe we can still do something useful
  32. functional core,
 imperative shell purity/effects — Gary Bernhardt - Start

    simple. - the majority of your logic should be written as pure functions. - wrap the imperative logic that talks to DB or makes HTTP requests
  33. typical IO def foo():
 name = input("Name?")
 print(“Oh, %s is

    a nice name" % (name,)) purity/effects - Here’s a typical IO-using function. - It looks tiny, right? - Let’s reduce it to its essence. Which parts can be factored out to be pure?
  34. refactoring def compliment(name):
 return "Oh, %s is a nice name"

    % (name,) def foo():
 print(compliment(input(“Name?”))) purity/effects - extract the part that takes the result of first IO and does the pure calculation based on it. - This is a trivial example of functional core/imperative shell, but you can extrapolate this idea to a lot of code. - In practice, a lot of your functions are already pure, there are a lot that aren’t, and could be. - Thinking in these terms will help you factor your apps much better. - I encourage you to go back to your code bases, find the side effects, and try to factor them out.
  35. down the rabbit hole def foo():
 print(compliment(input(“Name?”))) - But can

    we go deeper!???? - Here are our side-effecting functions. - Could we represent the ideas in this code in a way that maintains purity?
  36. down the rabbit hole def foo():
 eff = Input("Name?") purity/effects

    @do
 def foo():
 name = yield Input(“Name?”)
 return Print(compliment(n)) or return eff.on(lambda n: Print(compliment(n))) - Here’s an idea. - represent our side-effects as objects describing *intent* to perform an effect. - input became an Input object, print became a Print object. - `on` attaches a callback to deal with the eventual result of that IO. - This is a liiiiittle but of a tedious syntax, so… <click> - Maybe we can take advantage of generator syntax to make it look nicer. - This is just like asyncio’s coroutines. - twisted also has inlineCallbacks
  37. results purity/effects @do
 def foo():
 name = yield Input(“Name?”)
 return

    Print(compliment(n)) def main():
 perform(foo()) - So what’s the point? - When we call it, we get back an introspectable object. - Test it. - In the end, actually perform the side-effects with a “perform” function.
  38. wink, nudge • Effect, warehouse.python.org/project/effect purity/effects - With a few

    minor syntactic differences, this is exactly how Effect works. - It’s rather extreme compared to the typical Python program - but similar to asyncio, Twisted, etc. - Not just for IO-based side-effects. also useful for RNG, mutable data writes, etc.
  39. style In the beginning was the function.
 All things issue

    from it; all things return to it. 
 —Tao Te FP - That’s all I’m going to say about purity and side-effects. - Could say a lot more, but TIME. - Now I want to talk about the *style* of functional programming. - Lisp, Erlang, OCaml, are all about stylistic FP. They don’t isolate side-effects, but they do love everything-as-functions.
  40. style • higher-order functions • use functions for control flow

    • don’t reassign • be an abstraction astronaut style - “higher-order” DEFINE - Use functions for control flow. Ex, recursion == looping. - HO functions are used for control flow and abstraction - Don’t reassign variables - already talked about this - combine above = abstraction astronaut.
  41. higher-order result = map(comp(f, g), inputs) not result = []


    for input in inputs:
 x = f(g(input))
 result.append(x) style - I showed this earlier when talking about expressiveness. - Seems like a win! 2 HO funcs: map&compose. - map: apply a func to all elements in a list. - compose: double-whammy. accepts and returns functions.
  42. compose h = comp(f, g) same as def h(*args, **kwargs):


    return f(g(*args, **kwargs)) style - FPers love these tiny abstractions, use them to great effect. - not just shorter lexically: avoids mention “arg” to funcs,*conceptually* simpler (“number of referenced names”).
  43. compose def comp(f, g):
 return lambda *a, **kw: f(g(*a, **kw))

    or from toolz import comp style We can define compose, or use toolz.
  44. filter/reduce >>> filter(lambda x: x % 2 == 0,
 ...

    [0,1,2])
 [0, 2] >>> reduce(operator.add, [1,2,3])
 6 style more HO funcs
  45. list comprehensions >>> [x + 1 for x in [0,1,2,3,4]


    ... if x % 2 == 0]
 [1, 3, 5] >>> map(lambda x: x + 1,
 ... filter(lambda x: x % 2 == 0,
 … [0,1,2,3,4]))
 [1, 3, 5] style - syntax sugar for map/filter (not reduce) - often nicer > map/filter+lambdas,
  46. map/filter >>> map(add1, filter(is_even, [0,1,2]))
 [1, 3] style map/filter shine

    w/ named funcs
  47. recursion def factorial(n):
 if n == 0:
 return 1
 else:


    return n * factorial(n-1) style - Not a lot of recursion in Python. Uses up stack space, no TCE.
  48. abstract it away def factorial(n):
 if n == 0:
 return

    1
 else:
 return n * factorial(n-1) vs def factorial(n):
 reduce(operator.mul, range(1, n)) style But that implementation is actually pretty gross conceptually as well as performance-wise. Reduce is powerful. Reduce usually recursive in FPL, Py = iterative. large inputs = no p.
  49. astronaut add1all (first:rest) = first + 1 : add1all rest


    add1all [] = [] or, def add1all(inputs):
 if not inputs: return []
 return [inputs[0] + 1] + add1all(inputs[1:]) style - Functional programmers love abstractions. - Many patterns of recursion have been identified. - In ancient history, people often wrote things like this, and realized a pattern
  50. astronaut map f (head:rest) = f head : map f

    rest
 map f [] = [] or def map(f, inputs):
 if not inputs: return []
 return [f(inputs[0])] + map(f, inputs[1:]) style - Parameterize the per-element calculation and create `map`. - map, reduce, filter are all simple recursion recipes, extracted into HO funcs. - though recursion is an important part of FP, people prefer not to do it explicitly - Using the recipes makes it easier and quicker to understand what the code is doing - Even though Python hates recursion, these abstractions are implemented iteratively so we can use the same style.
  51. compose functional purity + functional style - Coming up on

    the end! - functional purity: idea of not exposing side-effects - functional style: use functions for everything! - doing pure functional programming leads to - easier reading - easier adding features/fixing bugs - easier refactoring - easier testing - Go forth and invoke a function!
  52. questions/refs • Pyrsistent, warehouse.python.org/project/pyrsistent • Toolz, warehouse.python.org/project/toolz • Effect, warehouse.python.org/project/effect

    • HAMT, http://hypirion.com/musings/understanding- persistent-vector-pt-1 • Pokey the Penguin, http://yellow5.com/pokey/