9. Appendix 1 - Example use of Functional Design Patterns for an IPD Simulation#

10. Outline#

This appendix provides an example of how functional design patterns might be used to implement a simulation of the Iterated Prisoners Dilemma in the python programming language.

It should be read as a set of annotated notes as to the design choices and process of composing core conceptual elements into larger designs.

10.1. Make some functional patterns available#

from pymonad.tools import curry
from functools import reduce
from itertools import product
from copy import copy
def compose(*funcs):
    return reduce(lambda f,g : lambda x : f(g(x)), funcs, lambda x : x)

10.2. The basic components#

These are the initial ideas for the core concepts that a simulation could be built from.

Note - these are is provisional.

10.2.1. Strategies#

\({\bf choices = \left\{1,0\right\}}\)

\({\bf strategy : (\left[ choices \right] \times \left[ choices \right]) \rightarrow choices}\)

Parameterise a tit4tat strategy

# tit4tat paramaterised by value of initial play 
@curry(2)
def tit4tat(initial_play,history) :
    _,opponent = history
    return initial_play if opponent == [] else opponent[-1]

Use parameterisation to create strategies

tit4tat_nice_at_first = tit4tat(1)
tit4tat_nasty_at_first = tit4tat(0)

10.2.2. Turn#

\({\bf history : \left[choices\right] \times \left[choices\right]}\)

\({\bf turn : strategy \rightarrow strategy \rightarrow history \rightarrow choices \times choices}\)

@curry(3)
def turn(s0,s1,history) :
    h0,h1 = history
    return h0 + [s0((h0,h1))],h1 + [s1((h1,h0))]

    

10.2.3. Round#

\({\bf round : N \rightarrow turn \rightarrow history}\)

@curry(2)
def round(nplays,t) : return reduce(compose,nplays*[t])(([],[]))
    

10.3. A Round Robin#

This is a first attempt to assemble the core concepts into a simple simulation

\({\bf round\_robin : \left[ strategies \right] \rightarrow N \rightarrow [history]}\)

10.3.1. By hand (check core concepts make sense)#

Note - start off by transforming the core concepts seperatley

strategies = [tit4tat_nice_at_first,tit4tat_nasty_at_first]
round_robin =  product(strategies,strategies)
turns = map(lambda args : turn(*args),round_robin) 
nplays = 10 # nplay turns in a round 

results = map(lambda x : round(nplays,x),turns)

list(results)
[([1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]),
 ([1, 0, 1, 0, 1, 0, 1, 0, 1, 0], [0, 1, 0, 1, 0, 1, 0, 1, 0, 1]),
 ([0, 1, 0, 1, 0, 1, 0, 1, 0, 1], [1, 0, 1, 0, 1, 0, 1, 0, 1, 0]),
 ([0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0])]

10.4. Package it up#

Seems ok. So, compose everything together into a single program

\({\bf round\_robin : \left[ strategies \right] \rightarrow N \rightarrow [history]}\)

10.4.1. A curried map is a very versatile tool !!#

cmap = curry(2,map)
round_robin_competition = lambda strategies,nplays : compose(cmap(lambda x : round(nplays,x)),
                                                             cmap(lambda args : turn(*args)),
                                                             lambda x : product(x,x))(strategies)

10.5. Use it !!#

strategies = [tit4tat_nice_at_first,tit4tat_nasty_at_first]
results = round_robin_competition(strategies,10)

list(results)
[([1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]),
 ([1, 0, 1, 0, 1, 0, 1, 0, 1, 0], [0, 1, 0, 1, 0, 1, 0, 1, 0, 1]),
 ([0, 1, 0, 1, 0, 1, 0, 1, 0, 1], [1, 0, 1, 0, 1, 0, 1, 0, 1, 0]),
 ([0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0])]

10.6. Extensions#

See if extra steps (prreprocessing/postprocessing can easily be added. NOte - this is testing reusability of the code and concepts.

10.6.1. Map the strategies to the results ?#

First attempt to be able to associate the strategies with a key so as to llok then up later.

round_robin_competition_v2 = lambda strategies,nplays : zip(product(strategies,strategies),round_robin_competition(strategies,nplays))
results = round_robin_competition_v2(strategies,10)

list(results)
[((<function pymonad.tools._curry_helper.<locals>._curry_internal(*arguments: List[Any])>,
   <function pymonad.tools._curry_helper.<locals>._curry_internal(*arguments: List[Any])>),
  ([1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1])),
 ((<function pymonad.tools._curry_helper.<locals>._curry_internal(*arguments: List[Any])>,
   <function pymonad.tools._curry_helper.<locals>._curry_internal(*arguments: List[Any])>),
  ([1, 0, 1, 0, 1, 0, 1, 0, 1, 0], [0, 1, 0, 1, 0, 1, 0, 1, 0, 1])),
 ((<function pymonad.tools._curry_helper.<locals>._curry_internal(*arguments: List[Any])>,
   <function pymonad.tools._curry_helper.<locals>._curry_internal(*arguments: List[Any])>),
  ([0, 1, 0, 1, 0, 1, 0, 1, 0, 1], [1, 0, 1, 0, 1, 0, 1, 0, 1, 0])),
 ((<function pymonad.tools._curry_helper.<locals>._curry_internal(*arguments: List[Any])>,
   <function pymonad.tools._curry_helper.<locals>._curry_internal(*arguments: List[Any])>),
  ([0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]))]

10.6.2. Name the strategies#

Mapping strategies to results seemed ok - but strings are more human readable.

named_strategies = {tit4tat_nice_at_first : "start nice",  tit4tat_nasty_at_first : "start nasty"}
round_robin_competition_v3 = lambda strategies,nplays : {(named_strategies[s0],named_strategies[s1]) : (h0,h1) 
                            for ((s0,s1),(h0,h1)) in round_robin_competition_v2(named_strategies.keys(),10)}
results = round_robin_competition_v3(named_strategies.keys(),10)

results
{('start nice', 'start nice'): ([1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]),
 ('start nice', 'start nasty'): ([1, 0, 1, 0, 1, 0, 1, 0, 1, 0],
  [0, 1, 0, 1, 0, 1, 0, 1, 0, 1]),
 ('start nasty', 'start nice'): ([0, 1, 0, 1, 0, 1, 0, 1, 0, 1],
  [1, 0, 1, 0, 1, 0, 1, 0, 1, 0]),
 ('start nasty', 'start nasty'): ([0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0])}
for key in named_strategies.keys() : print(key)
<function _curry_helper.<locals>._curry_internal at 0x7ca3f4552340>
<function _curry_helper.<locals>._curry_internal at 0x7ca3f4552520>

10.6.3. Score the results#

Now for some post processing. Want to get scores from histories. First need to conceptualise something to use for scoring. Whats this going to be ? This is functinal style programming - so surely it must be a function !!

This could come in handy !! Why ?

creduce = curry(2,reduce)

\({\bf score : choices \times choices \rightarrow R \times R} \)

Parameterise a score with the pay of matrix

@curry(2)
def score(pom,choices) : return pom[choices]
standard_pom = {(1,1) : (1,1),
                (1,0) : (5,0),
                (0,1) : (0,5),
                (0,0) : (3,3)}

standard_score = score(standard_pom)

10.7. Over to you !!#

The ultimate test of code reusability is to get others to reuse the code !!

Try using functional patterns to “compose in” extra steps that use the concept of score to map strategies to actual scores.Go0d luck, and have fun !!