├── README.md ├── mpython.py ├── test1.py ├── test2.py └── test3.py /README.md: -------------------------------------------------------------------------------- 1 | Monad-Python 2 | ============ 3 | A toy Python interpreter with monad comprehensions 4 | 5 | Status 6 | ====== 7 | This is a completed project 8 | 9 | Instructions 10 | ============ 11 | See http://blog.sigfpe.com/2012/03/overloading-python-list-comprehension.html for instructions. 12 | 13 | Something like `python mpython.py test3.py` works for me. 14 | -------------------------------------------------------------------------------- /mpython.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from ast import * 4 | import sys 5 | 6 | def name(x, ctx): 7 | return Name(id = x, ctx = ctx, lineno = 0, col_offset = 0) 8 | 9 | def call(f, *args): 10 | f = name(f, Load()) 11 | return Call(func = f, args = list(args), lineno = 0, 12 | col_offset = 0, keywords = [], vararg = None) 13 | 14 | def func(args, body): 15 | return Lambda(args = arguments(args = args, defaults = []), 16 | body = body, vararg = None, lineno = 0, col_offset = 0) 17 | 18 | unit = Tuple(elts = [], lineno=0, col_offset = 0, ctx = Load()) 19 | 20 | def transform(elt, generators): 21 | elt = call("__singleton__", elt) 22 | 23 | for generator in generators[-1::-1]: 24 | for i in generator.ifs[-1::-1]: 25 | elt = call("__concatMap__", 26 | func([name('_', Param())], elt), 27 | IfExp(i, 28 | call('__singleton__', unit), 29 | call('__fail__'), lineno=0, col_offset=0)) 30 | 31 | elt = call("__concatMap__", 32 | func([name(generator.target.id, Param())], elt), 33 | generator.iter) 34 | return elt 35 | 36 | class RewriteComp(NodeTransformer): 37 | def visit_ListComp(self, node): 38 | return transform(RewriteComp().visit(node.elt), 39 | [RewriteComp().visit(generator) for generator in node.generators]) 40 | 41 | def __concatMap__(f, x): 42 | return [z for y in x for z in f(y)] 43 | 44 | def __singleton__(x): 45 | return [x] 46 | 47 | def __fail__(): 48 | return [] 49 | 50 | source = open(sys.argv[1]).read() 51 | e = compile(source, "", "exec", PyCF_ONLY_AST) 52 | e = RewriteComp().visit(e) 53 | f = compile(e, sys.argv[1], "exec") 54 | print f 55 | exec f 56 | print "Done" -------------------------------------------------------------------------------- /test1.py: -------------------------------------------------------------------------------- 1 | # Some simple regression tests 2 | 3 | a = [(x, y, z) for x in [0, 1, 2] 4 | for y in [0, 1, 2] 5 | for z in [0, 1, 2]] 6 | 7 | print len(a) 8 | 9 | b = [(x, y) for x in [0,1] 10 | for y in [0,1] 11 | if x+y < 2] 12 | 13 | print b 14 | 15 | c = [(x, y, z) for x in range(1, 10) 16 | for y in range(1, 10) 17 | if x < y 18 | for z in range(1, 10) 19 | if x*x+y*y == z*z] 20 | 21 | print c -------------------------------------------------------------------------------- /test2.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | def __concatMap__(k, m): 4 | return lambda c:m(lambda a:k(a)(c)) 5 | 6 | def __singleton__(x): 7 | return lambda f:f(x) 8 | 9 | def callCC(f): 10 | return lambda c:f(lambda a:lambda _:c(a))(c) 11 | 12 | def __fail__(): 13 | raise "Failure is not an option for continuations" 14 | 15 | def ret(x): 16 | return __singleton__(x) 17 | 18 | def id(x): 19 | return x 20 | 21 | def solve(a, b, c): 22 | return callCC(lambda throw: [((-b-d)/(2*a), (-b+d)/(2*a)) 23 | for a0 in (ret(a) if a!=0 else throw("Not quadratic")) 24 | for d2 in ret(b*b-4*a*c) 25 | for d in (ret(math.sqrt(d2)) if d2>=0 else throw("No roots")) 26 | ]) 27 | 28 | print solve(1, 0, -9)(id) 29 | print solve(1, 1, 9)(id) 30 | print solve(0, 1, 9)(id) -------------------------------------------------------------------------------- /test3.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | # 4 | # This implements the probablity monad 5 | # This defines the semantics of [x for a in b 6 | # for c in d] 7 | # to mean something like "the probability of x supposing a is drawn from 8 | # distribution b and then c is drawn from d, conditioned on a 9 | # 10 | class PDF(dict): 11 | def __hash__(self): 12 | return hash(tuple(sorted(self.iteritems()))) 13 | 14 | def scale(alpha, ps): 15 | result = PDF() 16 | for k in ps: 17 | result[k] = alpha*ps[k] 18 | return result 19 | 20 | def combine(pdf1, pdf2): 21 | result = pdf1.copy() 22 | for k in pdf2.keys(): 23 | if result.has_key(k): 24 | result[k] += pdf2[k] 25 | else: 26 | result[k] = pdf2[k] 27 | return result 28 | 29 | def __concatMap__(f, pps): 30 | result = PDF() 31 | for k0 in pps.keys(): 32 | p = pps[k0] 33 | k = f(k0) 34 | result = combine(result, scale(pps[k0], f(k0))) 35 | return result 36 | 37 | def __singleton__(x): 38 | return PDF([(x, 1)]) 39 | 40 | def __fail__(x): 41 | return PDF() 42 | 43 | def certain(x): 44 | return __singleton__(x) 45 | 46 | def expectation(pdf): 47 | total = 0.0; 48 | for k in pdf.keys(): 49 | total += pdf[k]*k 50 | return total 51 | 52 | def probability(condition, pdf): 53 | return expectation([1 if condition(k) else 0 for k in pdf]) 54 | 55 | ################################################################################ 56 | # Test code starts here 57 | ################################################################################ 58 | 59 | def bernoulli(p, a = True, b = False): 60 | """ 61 | A sample from bernoulli(p, a, b) has probability p of taking the value 62 | a and probabilty 1-p of taking the value b. 63 | """ 64 | return PDF([(a, p), (b, 1-p)]) 65 | 66 | # Straightforward coin toss. 67 | test1 = [x for x in bernoulli(0.5)] 68 | 69 | print "test1 =", test1 70 | 71 | # A pair of coin tosses. 72 | test2 = [(x, y) for x in bernoulli(0.5) 73 | for y in bernoulli(0.5)] 74 | 75 | print "test2 =", test2 76 | 77 | # The sum of the result of three coin tosses. 78 | test3 = [a+b+c for a in bernoulli(0.5, 1, 0) 79 | for b in bernoulli(0.5, 1, 0) 80 | for c in bernoulli(0.5, 1, 0)] 81 | 82 | print "test3 =", test3 83 | 84 | ################################################################################ 85 | # Chinese restaurant code 86 | # http://en.wikipedia.org/wiki/Chinese_restaurant_process 87 | ################################################################################ 88 | 89 | def addGuest(i, tables): 90 | if i==len(tables): 91 | return tables+(1,) 92 | else: 93 | tableList = list(tables) 94 | tableList[i] += 1 95 | return tuple(tableList) 96 | 97 | def selectTable(tables): 98 | newTables = list(tables)+[1] 99 | n = sum(newTables) 100 | return PDF(zip(range(len(newTables)), map(lambda x: float(x)/n, newTables))) 101 | 102 | def chineseRestaurantProcess(n): 103 | """ 104 | Simulates the Chinese restaurant process to n steps 105 | """ 106 | 107 | if n==0: 108 | return certain(()) 109 | else: 110 | return [addGuest(guestTable, tables) for tables in chineseRestaurantProcess(n-1) 111 | for guestTable in selectTable(tables)] 112 | 113 | # Expected table size (occupancy) given by theory. 114 | def theoreticalTableSize(n): 115 | total = 0.0 116 | for k in range(0, n): 117 | total += 1.0/(k+1) 118 | return total 119 | 120 | # Compare theoretical and simulated expected table size. 121 | # Should be equal to machine precision. 122 | test4a = expectation([len(table) for table in chineseRestaurantProcess(10)]) 123 | test4b = theoreticalTableSize(10) 124 | print "test4 =", math.fabs(test4a-test4b) < 1e-9 125 | 126 | # Estimate the probability that at least one person is sitting on their own. 127 | # Simulates 16 steps. Note that this is a lot of computation, even though it 128 | # is probably much more accurate than a Monte-Carlo simulation using the same 129 | # amount of CPU time. 130 | for i in range(1, 16): 131 | test5a = probability(lambda table: min(table)==1, chineseRestaurantProcess(i)) 132 | print i, test5a 133 | test5b = 1-math.exp(-1) 134 | print "test5 =", math.fabs(test5a-test5b) < 1e-9 135 | 136 | # Maybe you recognise the 1-1/e above from this problem: 137 | # n people visit a restaurant (another one) and hang up their hats. When they leave 138 | # they pick a hat at random. What's the probability of anyone getting their original 139 | # hat back? The connection between that problem and the CRP is a beautiful bit 140 | # of mathematics about the permutation group. I'll leave it as a puzzle to figure 141 | # out why problems have the same solution. 142 | 143 | # PS I only realised this fact because I'd written code to compute this stuff to machine 144 | # precision and recognised 1-1/e. It's fortuitous that I chose this problem completely at 145 | # random and was led to see the connection. (I admit I got some help from the web...) 146 | --------------------------------------------------------------------------------