SlideShare a Scribd company logo
1 of 47
Download to read offline
Monads for normal people!




   Dustin Getz   https://github.com/dustingetz/monadic-interpreter
   @dustingetz   https://github.com/dustingetz/pymonads
intended audience
•   coders
•   who are comfortable with lambdas
•   who learn by example
goals
•   how do monads work
•   how do monads help
•   are monads useful IRL?
•   especially in enterprise?
•   where do they fall short and what's next
large codebases are complex
•   Spring, EJB, AspectJ, DI, AOP
•   Common goal: make code look like business
    logic
•   (to varying degrees of success)
Aspect Oriented Programming
From Wikipedia, the free encyclopedia


In computing, aspect-oriented programming (AOP) is a
    programming paradigm which aims to increase modularity by allowing the
    separation of cross-cutting concerns.
Typically, an aspect is scattered or tangled as code, making it harder to
   understand and maintain. It is scattered by virtue of the function (such as
   logging) being spread over a number of unrelated functions that might use
   its function, possibly in entirely unrelated systems, different source
   languages, etc. That means to change logging can require modifying all
   affected modules. Aspects become tangled not only with the mainline
   function of the systems in which they are expressed but also with each
   other. That means changing one concern entails understanding all the
   tangled concerns or having some means by which the effect of changes can
   be inferred.
Lots of code to follow
•   Pay attention to how the functions compose
a bank API
def get_account(person):
      if person.name == "Alice": return 1
      elif person.name == "Bob": return 2
      else: return None


def get_balance(account):
      if account.id == 1: return 1000000
      elif account.id == 2: return 75000
      else: return None


def get_qualified_amount(balance):
      if balance.cash > 200000: return balance.cash
      else: return None
(shoutout to Tumult on hackernews for the 'bank' example)
what we want to write
def get_loan(name):
   account = get_account(name)
   balance = get_balance(account)
   loan = get_qualified_amount(balance)
   return loan
My boss could write this code
get_qualified_amount( get_balance( get_account( alice )))


alice | get_account | get_balance | get_qualified_amount


(-> get_account get_balance get_qualified_amount)(alice)


alice.get_account().get_balance().get_qualified_amount()
i love NullPointerExceptions
def get_account(person):
    if person.name == "Alice": return 1
    elif person.name == "Bob": return 2
    else: return None


>>> get_account(None)
AttributeError: 'NoneType' object has no attribute 'id'
what the prod code looks like :(
def get_loan(person):
   account = get_account(person)
   if not account:
       return None
   balance = get_balance(account)
   if not balance:
       return None
   loan = get_qualified_amount(balance)
   return loan
factor! abstract! happy!
def bind(v, f):
   if (v):
       return f(v)
   else:
       return None
factor! abstract! happy!
def bind(v, f):
    if (v):                    # v == alice
       return f(v)         # get_account(alice)
    else:
       return None


>>> alice = Person(...)
>>> bind(alice, get_account)
1
factor! abstract! happy!
def bind(v, f):
       if (v):
          return f(v)
       else:                  # v == None
          return None         # don't call f


>>> bind(None, get_account)
None
the code we really want to write
def bind(v,f): return f(v) if v else None


def get_loan(name):
       m_account = get_account(name)
       m_balance = bind(m_account, get_balance)
       m_loan =   bind(m_balance, get_qualified_amount)
       return m_loan


>>> alice = Person(...)
>>> get_loan(alice)
100000
>>> get_loan(None)
None
or more succinctly
def bind(v,f): return f(v) if v else None


def m_pipe(val, fns):
       m_val = val
       for f in fns:
          m_val = bind(m_val, f)
       return m_val


>>> fns = [get_account, get_balance, get_qualified_amount]
>>> m_pipe(alice, fns)
1000000
>>> m_pipe(dustin, fns)
None
big picture goal
•   make the code look like the business logic
•   "good clojure programmers write a language to write
    their program in" -- DSLs
•   build a language to build your business logic
•   add features without changing your business logic
add a feature to our API
def get_account(person):
   if person.name == "Alice": return (1, None)
   elif person.name == "Bob": return (2, None)
   else: return (None, "No acct for '%s'" % person.name)



>>> get_account(alice)
(1, None)


>>> get_account(dustin)
(None, "No acct for 'Dustin'")
def get_balance(account):
   if account.id == 1: return (1000000, None)
   elif account.id == 2: return (75000, None)
   else: return (None, "No balance, acct %s" %
                        account.id)


def get_qualified_amount(balance):
   if balance.cash > 200000: return (balance, None)
   else: return (None, "Insufficient funds: %s" %
                        balance.cash)
def bind(mval, mf):
   value = mval[0]
   error = mval[1]
   if not error:
       return mf(value)
   else:
       return mval
def bind(mval, mf):           # mval == (1, None)
   value = mval[0]            # value == 1
   error = mval[1]            # error == None
   if not error:
       return mf(value)       # mf(1)
   else:
       return mval


>>> mval = (1, None)
>>> bind(mval, get_balance)
(100000, None)
def bind(mval, mf):       # mval == (None, "insuf funds")
   value = mval[0]        # value == None
   error = mval[1         # error == "insuf funds"
   if not error:
       return mf(value)
   else:
       return mval        # short circuit


>>> mval = (None, "insuf funds")
>>> bind(mval, get_balance)
(None, "insuf funds")
Business logic didn't change
# error monad
def bind(mv, mf): mf(mv[0]) if mv[0] else mv
def unit(v): return (v, None)


def m_pipe(val, *fns):
   m_val = unit(val)
   for f in fns:
       m_val = bind(m_val, f)
   return m_val


>>> m_pipe(alice, get_account, get_balance,
              get_qualified_amount)
(1000000, None)
Business logic didn't change
# error monad
def bind(mv, mf): mf(mv[0]) if mv[0] else mv
def unit(v): return (v, None)


def m_chain(*fns): ...


>>> get_loan = m_chain(get_account, get_balance,
                         get_qualified_amount)
<fn that composes the fns in order>


>>> get_loan(alice)
(1000000, None)
>>> get_loan(dustin)
(None, "insuf funds")
Business logic didn't change
get_loan = m_chain(get_account, get_balance,
                   get_qualified_amount)


>>> map(get_loan, [alice, dustin, bob])
[(1000000, None), (None, "insuf funds"), (75000, None)]



>>> mmap(get_loan, [alice, bob])
([1000000, 75000], None)


>>> mmap(get_loan, [alice, bob, dustin])
(None, "insuf funds")
here be monads
# maybe monad
def bind(mv, mf): return mf(mv) if mv else None
def unit(v): return v


# error monad
def bind(mv, mf): mf(mv[0]) if mv[0] else mv
def unit(v): return (v, None)
monad comprehensions
# error monad
def bind(mv, mf): mf(mv[0]) if mv[0] else mv
def unit(v): return (v, None)


>>> mv1 = (7, None)
>>> mv2 = bind( mv1,
                lambda x: unit(x))
(7, None)

>>> mv3 = bind( mv2,
                lambda x: unit(x+1))
(8, None)
monad comprehensions
# error monad
def bind(mv, mf): mf(mv[0]) if mv[0] else mv
def unit(v): return (v, None)

>>> mv1 = (7, None); mv2 = (3, None)
>>> mv3 = bind( mv1,
                  lambda x: bind( mv2,
                                 lambda y: unit(x + y)))
(10, None)

>>> mv4 = (None, “error”)
>>> bind( mv3,
         lambda x: bind( mv4,
                            lambda y: unit(x + y)))
(None, “error”)
the simplest monad
# identity monad
def bind(mv,mf): return mf(mv)
def unit(v): return v


>>> bind( 7,
         lambda x: bind( 3,
                         lambda y: unit(x + y)))
10
the simplest monad
# identity monad
def bind(mv,mf): return mf(mv)
def unit(v): return v


>>> bind( 7,       lambda x:
     bind( 3,      lambda y:
           unit(x + y)))
10


repl> (let [x 7
                y 3]
          x + y)
10
Lists are a monad!
>>> [(x,y) for x in ranks for y in files]


# list monad
def unit(v): return [v]
def bind(mv,mf): return flatten(map(mf, mv))


>>> ranks = list("abcdefgh")
>>> files = list("12345678")
>>> bind( ranks,   lambda rank:
   bind( files,    lambda file:
         unit((rank, file))))
repl> (for [rank (seq “abcdefgh”)
           file (seq “12345678”)]
        [rank, file])
monads are...
•   a design pattern for composing functions
    that have incompatible types, but can still
    logically define composition
•   by overriding function application
•   to increase modularity and manage
    accidental complexity
here comes the fun stuff
•   introduce a few harder monads
•   combine monads to build...
    o   a Clojure parsing DSL
    o   a Python lisp interpreter
writer monad
def unit(v): return (v, [])
def bind(mv, mf): …


def addOne(x):
   val = x+1
   logmsg = "x+1==%s" % val
   return (val, [logmsg])


>>> mv = addOne(7)
(8, ['x+1==8'])
>>> m_chain(addOne, addOne, addOne)(mv)
(11, ['x+1==9', 'x+1==10', 'x+1==11'])
writer monad
def unit(v): return (v, [])
def bind(mv, mf): …


def addOne(x):
    val = x+1
    logmsg = "x+1==%s" % val
    return (val, [logmsg])


>>> addThreeLogged = m_chain(addOne, addOne, addOne)
>>> map(addThreeLogged, [10,20,30])
[(13, ['x+1==11', 'x+1==12', 'x+1==13']),
(23, ['x+1==21', 'x+1==22', 'x+1==23']),
(33, ['x+1==31', 'x+1==32', 'x+1==33'])]
writer monad
def unit(v): return (v, [])
def bind(mv, mf): …


def addOne(x):
    val = x+1
    logmsg = "x+1==%s" % val
    return (val, [logmsg])


>>> mmap(addOne, [1,2,3])
   ([2, 3, 4], ['x+1==2', 'x+1==3', 'x+1==4'])
reader monad
def unit(v): return lambda env: v
def bind(mv, mf): …


def read(key):
     def _(env):
        return env[key]
     return _


>>> mv = read('a')
<a function of env>
>>> mv({'a': 7})
7
>>> mv({'a': 10, 'b': 3})
10
reader monad
def unit(v): return lambda env: v
def bind(mv, mf): …


def read(key):
     def _(env):
        return env[key]
     return _


>>> computation = bind( read('a'),    lambda a:
                   bind( read('b'),   lambda b:
                          unit( a+b   ))
<a function of env>
>>> computation({'a': 7, 'b': 3})
10
reader monad
def unit(v): return lambda env: v
def bind(mv, mf): …


def read(key):
     def _(env):
        return env[key]
     return _


>>> computation = bind( read('a'),    lambda a:
                   bind( read('b'),   lambda b:
                          unit( a+b   ))
<a function of env>
>>> computation({'a': 42, 'b': 1})
43
environment = reader + writer
def unit(v): …
def bind(mv, mf): …


def read(key): lambda env: (env[key], env)
def write(key, val): lambda env: … (None, newEnv)


>>> write('a', 2)
<a function of env>
>>> myenv = {'a': 999, 'b': 999}
>>> write('a', 2)(myenv)
(None, {'a': 2, 'b': 999})
>>> read('a')(myenv)
(999, {'a': 999, 'b': 999})
environment = reader + writer
>>> computation = bind( read('a'),        lambda a:
                 bind( read('b'),         lambda b:
                 bind( write('c', a+b),   lambda _:
                       unit(c+1)          )))
<a function of env>


>>> computation({'a': 7, 'b': 3})
(11, {'a': 7, 'b': 3, 'c': 10})
interpreter = environment + error
def read(key): lambda env: ...         # can fail
def write(key, val): lambda env: ...   # can fail


>>> computation = bind( read('a'),          lambda a:
                 bind( read('b'),           lambda b:
                 bind( write('c', a+b),     lambda _:
                       unit(c+1)           )))


>>> computation({'a': 7, 'b': 3})
( (11, {'a': 7, 'b': 3, 'c': 10}), None)


>>> computation({'b': 3})
( (None, {'b': 3}), "unbound symbol 'a'")
interpreter = environment + error
lis.py> 1
((1, {}), None)
lis.py> x
((None, {}), 'referenced unbound symbol x')
lis.py> (define x 1)
((None, {'x': 1}), None)
lis.py> x
((1, {'x': 1}), None)
lis.py> (+ x 2)
((3, {'x': 1}), None)
lis.py> (set! x (+ x 2))
((None, {'x': 5}), None)
lis.py> x
((5, {'x': 5}), None)
interpreter = environment + error
lis.py> +
((<function <lambda> at 0x10047f320>,
{
'+': <function <lambda> at 0x10047f320>
'*': <function <lambda> at 0x10047f398>,
'dumpenv': <function <lambda> at 0x10047ecf8>,
'assert': <function <lambda> at 0x10047ec80>,
'symbol?': <function <lambda> at 0x10047eed8>,
'eq?': <function <lambda> at 0x10047f1b8>,
'car': <function <lambda> at 0x10047f2a8>,
... snip ...
}), None)
interpreter = monad heaven
•   identity -> values
•   reader -> read-only globals (builtins)
•   environment -> def, set!, lambda (lexical
    scope)
•   error -> assert, throw
•   continuation -> call/cc, try/catch/finally

"monads are how you implement special forms"
parser = environment + maybe
•   env where you are in the input, not a map
•   `nil` indicates failed parse branch, discard
    current env, restore env to last successful
    parse point and try another branch
So who cares?
•   Monads are a tool to manage complexity
•   Write code with maximally separated
    concerns
•   trust that you won't break existing code,
    allow you to maintain with minimal changes
•   write a language to talk about business state
    transitions, write your business logic in that
    language

More Related Content

What's hot

Jython: Python para la plataforma Java (JRSL 09)
Jython: Python para la plataforma Java (JRSL 09)Jython: Python para la plataforma Java (JRSL 09)
Jython: Python para la plataforma Java (JRSL 09)
Leonardo Soto
 
Jython: Python para la plataforma Java (EL2009)
Jython: Python para la plataforma Java (EL2009)Jython: Python para la plataforma Java (EL2009)
Jython: Python para la plataforma Java (EL2009)
Leonardo Soto
 

What's hot (20)

Everything you always wanted to know about forms* *but were afraid to ask
Everything you always wanted to know about forms* *but were afraid to askEverything you always wanted to know about forms* *but were afraid to ask
Everything you always wanted to know about forms* *but were afraid to ask
 
Wp query
Wp queryWp query
Wp query
 
F# in the real world (CodeMotion Dubai)
F# in the real world (CodeMotion Dubai)F# in the real world (CodeMotion Dubai)
F# in the real world (CodeMotion Dubai)
 
Extending grimoirelab
Extending grimoirelabExtending grimoirelab
Extending grimoirelab
 
Swift 함수 커링 사용하기
Swift 함수 커링 사용하기Swift 함수 커링 사용하기
Swift 함수 커링 사용하기
 
Jython: Python para la plataforma Java (JRSL 09)
Jython: Python para la plataforma Java (JRSL 09)Jython: Python para la plataforma Java (JRSL 09)
Jython: Python para la plataforma Java (JRSL 09)
 
Error Management: Future vs ZIO
Error Management: Future vs ZIOError Management: Future vs ZIO
Error Management: Future vs ZIO
 
F# in the real world (NDC)
F# in the real world (NDC)F# in the real world (NDC)
F# in the real world (NDC)
 
Difference between mysql_fetch_array and mysql_fetch_assoc in PHP
Difference between mysql_fetch_array and mysql_fetch_assoc in PHPDifference between mysql_fetch_array and mysql_fetch_assoc in PHP
Difference between mysql_fetch_array and mysql_fetch_assoc in PHP
 
50 Laravel Tricks in 50 Minutes
50 Laravel Tricks in 50 Minutes50 Laravel Tricks in 50 Minutes
50 Laravel Tricks in 50 Minutes
 
Comparisons
ComparisonsComparisons
Comparisons
 
Jython: Python para la plataforma Java (EL2009)
Jython: Python para la plataforma Java (EL2009)Jython: Python para la plataforma Java (EL2009)
Jython: Python para la plataforma Java (EL2009)
 
Symfony2, creare bundle e valore per il cliente
Symfony2, creare bundle e valore per il clienteSymfony2, creare bundle e valore per il cliente
Symfony2, creare bundle e valore per il cliente
 
Swift internals
Swift internalsSwift internals
Swift internals
 
Opaque Pointers Are Coming
Opaque Pointers Are ComingOpaque Pointers Are Coming
Opaque Pointers Are Coming
 
Arrays in PHP
Arrays in PHPArrays in PHP
Arrays in PHP
 
Electrify your code with PHP Generators
Electrify your code with PHP GeneratorsElectrify your code with PHP Generators
Electrify your code with PHP Generators
 
Closure, Higher-order function in Swift
Closure, Higher-order function in SwiftClosure, Higher-order function in Swift
Closure, Higher-order function in Swift
 
Drinking the free kool-aid
Drinking the free kool-aidDrinking the free kool-aid
Drinking the free kool-aid
 
PHPUnit でよりよくテストを書くために
PHPUnit でよりよくテストを書くためにPHPUnit でよりよくテストを書くために
PHPUnit でよりよくテストを書くために
 

Similar to Monads in python

Single Page Web Applications with CoffeeScript, Backbone and Jasmine
Single Page Web Applications with CoffeeScript, Backbone and JasmineSingle Page Web Applications with CoffeeScript, Backbone and Jasmine
Single Page Web Applications with CoffeeScript, Backbone and Jasmine
Paulo Ragonha
 
The Art Of Readable Code
The Art Of Readable CodeThe Art Of Readable Code
The Art Of Readable Code
Baidu, Inc.
 
Refactoring to Macros with Clojure
Refactoring to Macros with ClojureRefactoring to Macros with Clojure
Refactoring to Macros with Clojure
Dmitry Buzdin
 
Phoenix for laravel developers
Phoenix for laravel developersPhoenix for laravel developers
Phoenix for laravel developers
Luiz Messias
 
Ruby Language - A quick tour
Ruby Language - A quick tourRuby Language - A quick tour
Ruby Language - A quick tour
aztack
 

Similar to Monads in python (20)

Indexing thousands of writes per second with redis
Indexing thousands of writes per second with redisIndexing thousands of writes per second with redis
Indexing thousands of writes per second with redis
 
Functional programming in ruby
Functional programming in rubyFunctional programming in ruby
Functional programming in ruby
 
Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine
Rails-like JavaScript Using CoffeeScript, Backbone.js and JasmineRails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine
Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine
 
Odoo from 7.0 to 8.0 API
Odoo from 7.0 to 8.0 APIOdoo from 7.0 to 8.0 API
Odoo from 7.0 to 8.0 API
 
Odoo - From v7 to v8: the new api
Odoo - From v7 to v8: the new apiOdoo - From v7 to v8: the new api
Odoo - From v7 to v8: the new api
 
Micropatterns
MicropatternsMicropatterns
Micropatterns
 
A Few of My Favorite (Python) Things
A Few of My Favorite (Python) ThingsA Few of My Favorite (Python) Things
A Few of My Favorite (Python) Things
 
Single Page Web Applications with CoffeeScript, Backbone and Jasmine
Single Page Web Applications with CoffeeScript, Backbone and JasmineSingle Page Web Applications with CoffeeScript, Backbone and Jasmine
Single Page Web Applications with CoffeeScript, Backbone and Jasmine
 
The Art Of Readable Code
The Art Of Readable CodeThe Art Of Readable Code
The Art Of Readable Code
 
The Power of Decorators in Python [Meetup]
The Power of Decorators in Python [Meetup]The Power of Decorators in Python [Meetup]
The Power of Decorators in Python [Meetup]
 
SOLID Ruby, SOLID Rails
SOLID Ruby, SOLID RailsSOLID Ruby, SOLID Rails
SOLID Ruby, SOLID Rails
 
Refactoring to Macros with Clojure
Refactoring to Macros with ClojureRefactoring to Macros with Clojure
Refactoring to Macros with Clojure
 
Phoenix for laravel developers
Phoenix for laravel developersPhoenix for laravel developers
Phoenix for laravel developers
 
Refactor like a boss
Refactor like a bossRefactor like a boss
Refactor like a boss
 
Type safe embedded domain-specific languages
Type safe embedded domain-specific languagesType safe embedded domain-specific languages
Type safe embedded domain-specific languages
 
Mining Functional Patterns
Mining Functional PatternsMining Functional Patterns
Mining Functional Patterns
 
Beware sharp tools
Beware sharp toolsBeware sharp tools
Beware sharp tools
 
Ruby Language - A quick tour
Ruby Language - A quick tourRuby Language - A quick tour
Ruby Language - A quick tour
 
Python speleology
Python speleologyPython speleology
Python speleology
 
2013 28-03-dak-why-fp
2013 28-03-dak-why-fp2013 28-03-dak-why-fp
2013 28-03-dak-why-fp
 

Monads in python

  • 1. Monads for normal people! Dustin Getz https://github.com/dustingetz/monadic-interpreter @dustingetz https://github.com/dustingetz/pymonads
  • 2. intended audience • coders • who are comfortable with lambdas • who learn by example
  • 3. goals • how do monads work • how do monads help • are monads useful IRL? • especially in enterprise? • where do they fall short and what's next
  • 4. large codebases are complex • Spring, EJB, AspectJ, DI, AOP • Common goal: make code look like business logic • (to varying degrees of success)
  • 5. Aspect Oriented Programming From Wikipedia, the free encyclopedia In computing, aspect-oriented programming (AOP) is a programming paradigm which aims to increase modularity by allowing the separation of cross-cutting concerns. Typically, an aspect is scattered or tangled as code, making it harder to understand and maintain. It is scattered by virtue of the function (such as logging) being spread over a number of unrelated functions that might use its function, possibly in entirely unrelated systems, different source languages, etc. That means to change logging can require modifying all affected modules. Aspects become tangled not only with the mainline function of the systems in which they are expressed but also with each other. That means changing one concern entails understanding all the tangled concerns or having some means by which the effect of changes can be inferred.
  • 6. Lots of code to follow • Pay attention to how the functions compose
  • 7. a bank API def get_account(person): if person.name == "Alice": return 1 elif person.name == "Bob": return 2 else: return None def get_balance(account): if account.id == 1: return 1000000 elif account.id == 2: return 75000 else: return None def get_qualified_amount(balance): if balance.cash > 200000: return balance.cash else: return None (shoutout to Tumult on hackernews for the 'bank' example)
  • 8. what we want to write def get_loan(name): account = get_account(name) balance = get_balance(account) loan = get_qualified_amount(balance) return loan
  • 9. My boss could write this code get_qualified_amount( get_balance( get_account( alice ))) alice | get_account | get_balance | get_qualified_amount (-> get_account get_balance get_qualified_amount)(alice) alice.get_account().get_balance().get_qualified_amount()
  • 10. i love NullPointerExceptions def get_account(person): if person.name == "Alice": return 1 elif person.name == "Bob": return 2 else: return None >>> get_account(None) AttributeError: 'NoneType' object has no attribute 'id'
  • 11. what the prod code looks like :( def get_loan(person): account = get_account(person) if not account: return None balance = get_balance(account) if not balance: return None loan = get_qualified_amount(balance) return loan
  • 12. factor! abstract! happy! def bind(v, f): if (v): return f(v) else: return None
  • 13. factor! abstract! happy! def bind(v, f): if (v): # v == alice return f(v) # get_account(alice) else: return None >>> alice = Person(...) >>> bind(alice, get_account) 1
  • 14. factor! abstract! happy! def bind(v, f): if (v): return f(v) else: # v == None return None # don't call f >>> bind(None, get_account) None
  • 15. the code we really want to write def bind(v,f): return f(v) if v else None def get_loan(name): m_account = get_account(name) m_balance = bind(m_account, get_balance) m_loan = bind(m_balance, get_qualified_amount) return m_loan >>> alice = Person(...) >>> get_loan(alice) 100000 >>> get_loan(None) None
  • 16. or more succinctly def bind(v,f): return f(v) if v else None def m_pipe(val, fns): m_val = val for f in fns: m_val = bind(m_val, f) return m_val >>> fns = [get_account, get_balance, get_qualified_amount] >>> m_pipe(alice, fns) 1000000 >>> m_pipe(dustin, fns) None
  • 17. big picture goal • make the code look like the business logic • "good clojure programmers write a language to write their program in" -- DSLs • build a language to build your business logic • add features without changing your business logic
  • 18. add a feature to our API def get_account(person): if person.name == "Alice": return (1, None) elif person.name == "Bob": return (2, None) else: return (None, "No acct for '%s'" % person.name) >>> get_account(alice) (1, None) >>> get_account(dustin) (None, "No acct for 'Dustin'")
  • 19. def get_balance(account): if account.id == 1: return (1000000, None) elif account.id == 2: return (75000, None) else: return (None, "No balance, acct %s" % account.id) def get_qualified_amount(balance): if balance.cash > 200000: return (balance, None) else: return (None, "Insufficient funds: %s" % balance.cash)
  • 20. def bind(mval, mf): value = mval[0] error = mval[1] if not error: return mf(value) else: return mval
  • 21. def bind(mval, mf): # mval == (1, None) value = mval[0] # value == 1 error = mval[1] # error == None if not error: return mf(value) # mf(1) else: return mval >>> mval = (1, None) >>> bind(mval, get_balance) (100000, None)
  • 22. def bind(mval, mf): # mval == (None, "insuf funds") value = mval[0] # value == None error = mval[1 # error == "insuf funds" if not error: return mf(value) else: return mval # short circuit >>> mval = (None, "insuf funds") >>> bind(mval, get_balance) (None, "insuf funds")
  • 23. Business logic didn't change # error monad def bind(mv, mf): mf(mv[0]) if mv[0] else mv def unit(v): return (v, None) def m_pipe(val, *fns): m_val = unit(val) for f in fns: m_val = bind(m_val, f) return m_val >>> m_pipe(alice, get_account, get_balance, get_qualified_amount) (1000000, None)
  • 24. Business logic didn't change # error monad def bind(mv, mf): mf(mv[0]) if mv[0] else mv def unit(v): return (v, None) def m_chain(*fns): ... >>> get_loan = m_chain(get_account, get_balance, get_qualified_amount) <fn that composes the fns in order> >>> get_loan(alice) (1000000, None) >>> get_loan(dustin) (None, "insuf funds")
  • 25. Business logic didn't change get_loan = m_chain(get_account, get_balance, get_qualified_amount) >>> map(get_loan, [alice, dustin, bob]) [(1000000, None), (None, "insuf funds"), (75000, None)] >>> mmap(get_loan, [alice, bob]) ([1000000, 75000], None) >>> mmap(get_loan, [alice, bob, dustin]) (None, "insuf funds")
  • 26. here be monads # maybe monad def bind(mv, mf): return mf(mv) if mv else None def unit(v): return v # error monad def bind(mv, mf): mf(mv[0]) if mv[0] else mv def unit(v): return (v, None)
  • 27. monad comprehensions # error monad def bind(mv, mf): mf(mv[0]) if mv[0] else mv def unit(v): return (v, None) >>> mv1 = (7, None) >>> mv2 = bind( mv1, lambda x: unit(x)) (7, None) >>> mv3 = bind( mv2, lambda x: unit(x+1)) (8, None)
  • 28. monad comprehensions # error monad def bind(mv, mf): mf(mv[0]) if mv[0] else mv def unit(v): return (v, None) >>> mv1 = (7, None); mv2 = (3, None) >>> mv3 = bind( mv1, lambda x: bind( mv2, lambda y: unit(x + y))) (10, None) >>> mv4 = (None, “error”) >>> bind( mv3, lambda x: bind( mv4, lambda y: unit(x + y))) (None, “error”)
  • 29. the simplest monad # identity monad def bind(mv,mf): return mf(mv) def unit(v): return v >>> bind( 7, lambda x: bind( 3, lambda y: unit(x + y))) 10
  • 30. the simplest monad # identity monad def bind(mv,mf): return mf(mv) def unit(v): return v >>> bind( 7, lambda x: bind( 3, lambda y: unit(x + y))) 10 repl> (let [x 7 y 3] x + y) 10
  • 31. Lists are a monad! >>> [(x,y) for x in ranks for y in files] # list monad def unit(v): return [v] def bind(mv,mf): return flatten(map(mf, mv)) >>> ranks = list("abcdefgh") >>> files = list("12345678") >>> bind( ranks, lambda rank: bind( files, lambda file: unit((rank, file)))) repl> (for [rank (seq “abcdefgh”) file (seq “12345678”)] [rank, file])
  • 32. monads are... • a design pattern for composing functions that have incompatible types, but can still logically define composition • by overriding function application • to increase modularity and manage accidental complexity
  • 33. here comes the fun stuff • introduce a few harder monads • combine monads to build... o a Clojure parsing DSL o a Python lisp interpreter
  • 34. writer monad def unit(v): return (v, []) def bind(mv, mf): … def addOne(x): val = x+1 logmsg = "x+1==%s" % val return (val, [logmsg]) >>> mv = addOne(7) (8, ['x+1==8']) >>> m_chain(addOne, addOne, addOne)(mv) (11, ['x+1==9', 'x+1==10', 'x+1==11'])
  • 35. writer monad def unit(v): return (v, []) def bind(mv, mf): … def addOne(x): val = x+1 logmsg = "x+1==%s" % val return (val, [logmsg]) >>> addThreeLogged = m_chain(addOne, addOne, addOne) >>> map(addThreeLogged, [10,20,30]) [(13, ['x+1==11', 'x+1==12', 'x+1==13']), (23, ['x+1==21', 'x+1==22', 'x+1==23']), (33, ['x+1==31', 'x+1==32', 'x+1==33'])]
  • 36. writer monad def unit(v): return (v, []) def bind(mv, mf): … def addOne(x): val = x+1 logmsg = "x+1==%s" % val return (val, [logmsg]) >>> mmap(addOne, [1,2,3]) ([2, 3, 4], ['x+1==2', 'x+1==3', 'x+1==4'])
  • 37. reader monad def unit(v): return lambda env: v def bind(mv, mf): … def read(key): def _(env): return env[key] return _ >>> mv = read('a') <a function of env> >>> mv({'a': 7}) 7 >>> mv({'a': 10, 'b': 3}) 10
  • 38. reader monad def unit(v): return lambda env: v def bind(mv, mf): … def read(key): def _(env): return env[key] return _ >>> computation = bind( read('a'), lambda a: bind( read('b'), lambda b: unit( a+b )) <a function of env> >>> computation({'a': 7, 'b': 3}) 10
  • 39. reader monad def unit(v): return lambda env: v def bind(mv, mf): … def read(key): def _(env): return env[key] return _ >>> computation = bind( read('a'), lambda a: bind( read('b'), lambda b: unit( a+b )) <a function of env> >>> computation({'a': 42, 'b': 1}) 43
  • 40. environment = reader + writer def unit(v): … def bind(mv, mf): … def read(key): lambda env: (env[key], env) def write(key, val): lambda env: … (None, newEnv) >>> write('a', 2) <a function of env> >>> myenv = {'a': 999, 'b': 999} >>> write('a', 2)(myenv) (None, {'a': 2, 'b': 999}) >>> read('a')(myenv) (999, {'a': 999, 'b': 999})
  • 41. environment = reader + writer >>> computation = bind( read('a'), lambda a: bind( read('b'), lambda b: bind( write('c', a+b), lambda _: unit(c+1) ))) <a function of env> >>> computation({'a': 7, 'b': 3}) (11, {'a': 7, 'b': 3, 'c': 10})
  • 42. interpreter = environment + error def read(key): lambda env: ... # can fail def write(key, val): lambda env: ... # can fail >>> computation = bind( read('a'), lambda a: bind( read('b'), lambda b: bind( write('c', a+b), lambda _: unit(c+1) ))) >>> computation({'a': 7, 'b': 3}) ( (11, {'a': 7, 'b': 3, 'c': 10}), None) >>> computation({'b': 3}) ( (None, {'b': 3}), "unbound symbol 'a'")
  • 43. interpreter = environment + error lis.py> 1 ((1, {}), None) lis.py> x ((None, {}), 'referenced unbound symbol x') lis.py> (define x 1) ((None, {'x': 1}), None) lis.py> x ((1, {'x': 1}), None) lis.py> (+ x 2) ((3, {'x': 1}), None) lis.py> (set! x (+ x 2)) ((None, {'x': 5}), None) lis.py> x ((5, {'x': 5}), None)
  • 44. interpreter = environment + error lis.py> + ((<function <lambda> at 0x10047f320>, { '+': <function <lambda> at 0x10047f320> '*': <function <lambda> at 0x10047f398>, 'dumpenv': <function <lambda> at 0x10047ecf8>, 'assert': <function <lambda> at 0x10047ec80>, 'symbol?': <function <lambda> at 0x10047eed8>, 'eq?': <function <lambda> at 0x10047f1b8>, 'car': <function <lambda> at 0x10047f2a8>, ... snip ... }), None)
  • 45. interpreter = monad heaven • identity -> values • reader -> read-only globals (builtins) • environment -> def, set!, lambda (lexical scope) • error -> assert, throw • continuation -> call/cc, try/catch/finally "monads are how you implement special forms"
  • 46. parser = environment + maybe • env where you are in the input, not a map • `nil` indicates failed parse branch, discard current env, restore env to last successful parse point and try another branch
  • 47. So who cares? • Monads are a tool to manage complexity • Write code with maximally separated concerns • trust that you won't break existing code, allow you to maintain with minimal changes • write a language to talk about business state transitions, write your business logic in that language