""" CLOSURES PROGRAMMING ASSIGNMENT, PART I Python has full support for untyped lambda terms. It also has support for creating closures without using the "lambda" syntax. """ def K(x): def inner(y): return x return inner #K print(K(1)(3)) # prints 1 print(K(lambda x:x)(1)(3)) # prints 3 """ The K combinator returns a closure equivalent to lambda y:x. A proper closure starts with a lambda term that contains FREE variables. A closure is a pair consisting of that lambda term and a set of bindings (the "environment") for the free variables of that term. We know that pure lambda calculus does not allow mutation: indeed there's no mention of memory in lambda calculus. But we need RAM in real computers to compute just about anything. We can hide the memory operations using recursion and other abstractions, but what are the consequences if we allowed a program to directly change the contents of memory? What does allowing `x=x+1` do to our programming language, besides allowing us to write for/while loops? It turns out that changing the value of a local (bound) variable is usually harmless. The fun starts when we change the value of FREE variables inside a closure. We can further distinguish between two types of closures: those that never change their free variables, and those that do. We can call them "mutable" or "immutable". For immutable closures, we don't really need to keep the free variables stored in memory - just copy over them as constants. For example, (lambda x.lambda y.x)3 just becomes lambda y.3. However, if we're to change x inside the closure, then we need to associate a memory location with x, and that memory location must be "alive" as long as the lambda term that refers to it. Fortunately languages like Python has a magical memory fairy that handles all this memory allocation stuff for you. For the first part of this assignment, we are going to explore closures in Python, which is representative of a large number of languages. In the next part of the assignment, we will leave fairyland ... Some languages (Java) only allow immutable closures in lambda terms, but most modern languages also support mutable closures. Python3 supports mutable closures with the `nonlocal` keyword (as opposed to just `global`). A function that declares a variable to be nonlocal has mutable access to a free variable that's declared in an outer scope. We can use this feature to write functions that create closures (return closures). """ def make_accumulator(): x = 0 # variable local to make_accumulator def inner(dx): nonlocal x # this keyword signals the formation of a mutable closure x = x + dx return x # end of inner function return inner # body of outer function (returns inner closure) #make_accumulator a1 = make_accumulator() a2 = make_accumulator() print(a1(2)) # prints 2 print(a1(2)) # prints 4 print(a1(3)) # prints 7 print(a2(2)) # prints 2 - a2 is a different closure. """Both a1 and a2 are "instances" of the inner function. They point to the same source code (lambda term) but not the same environment: they're different closures. Each carries with it a different "instance" of the variable x. Please note that x is not a global variable: it's locally created each time make_accumulator is called. A closure behaves like an "object": calling a "method" on one object is not the same as calling it on another object. We could've written the same program in a variety of languages including javascript, perl, ruby, etc. These languages were all influenced, directly or indirectly, by Scheme, which is the language used in your reading assignment: "Modularity, Objects and State", part of a classic textbook. YOUR ASSIGNMENT, PART I: ******************************** Do the exercises from the book exert "Modularity, Objects, and State", numbers 3.1-3.4 (between pages 297-305), and exercise 3.7 on page 319. Do the exercises in Python3 instead of Scheme. ***For this assignment you are not allowed to use classes or any data structures (lists, association arrays/hashmaps), either built-in or user defined. YOU MUST USE CLOSURES EXCLUSIVELY as demonstrated in the sample code here. The "bank account" example used in the reading is reproduced in Python3 below, with the balance_transfer "method" my own addition. """ #### 3.2 (make_monitored) def make_monitored(func): count = 0 def monitored_func(x=""): nonlocal count if x=="reset-count": count=0 elif x=="how-many-calls?": return count else: count += 1 return func # note, func is not applied here: alternative func(x) #monitored_func return monitored_func #make_monitored # Note: I chose not to `return func(x)`, which would work because Python is # untyped (in the sense of no type syntax). But in any typed language func # could not take both a string and an int. So my version must be applied # in a Curried way. I'm using python's ability to give arguments a default # value to make the syntax look nicer. def sqrt(x): return x**0.5 msqrt = make_monitored(sqrt) print( msqrt()(100) ) # note curried application: print( msqrt()(25) ) # msqrt() returns the original function sqrt print( msqrt("how-many-calls?") ) # prints 2 msqrt("reset-count") print( msqrt("how-many-calls?") ) # prints 0 # If I just `return func(x)` inside the monitored_fun, I won't need the # curried application: msqrt(100) would return 10. # Combined solution to 3.3-3.4 def newaccount(name, password): balance = 0 pwd_fail_count = 0 def inquiry(): return balance # `nonlocal` not needed for immutable closure def deposit(n): nonlocal balance if n>0: balance += n def withdraw(n): nonlocal balance if n>0 and balance-n>=0: balance -= n def balance_transfer(otheraccount): nonlocal balance otherbalance = otheraccount("inquiry") otheraccount("withdraw")(otherbalance) balance += otherbalance def public_interface(request, pwd): nonlocal pwd_fail_count if pwd!=password: pwd_fail_count += 1 if pwd_fail_count>3: print("CALLING THE COPS!") # 7 is too many else: print("Incorrect Password") return lambda x:None # return something to accept curried argument else: pwd_fail_count = 0 if request=="inquiry": return inquiry() # function called directly elif request=="deposit": return deposit # function itself returned elif request=="withdraw": return withdraw elif request=="balance transfer": return balance_transfer else: print("Invalid request",request) return lambda x:None #public_interface return public_interface # body of newaccount #newaccount # 3.7 def make_joint(account, originalpwd, newpwd): def new_interface(request, pwd): if pwd==originalpwd or pwd==newpwd: return account(request,originalpwd) else: return account(request,pwd) # new_interface return new_interface # update account's public interface #make_joint # Note: if originalpwd is not the right password then the new interface provided # will always try to access the account with the wrong password anyway. # My solution does not require modifications to 3.3, 3.4 youraccount = newaccount("student","xyzzy") # xyzzy is THE password youraccount("deposit","xyzzy")(1000) print("your balance is ",youraccount("inquiry","xyzzy")) youraccount("withdraw","open_sesame")(100) youraccount("withdraw","open_sesame")(100) youraccount("withdraw","open_sesame")(100) youraccount("withdraw","open_sesame")(100) # call the cops jointaccount = make_joint(youraccount,"xyzzy","open_sesame") jointaccount("withdraw","open_sesame")(100) jointaccount("withdraw","xyzzy")(100) jointaccount("withdraw","passwordismypassword")(100) print("your balance is ",jointaccount("inquiry","open_sesame")) """ output: your balance is 1000 Incorrect Password Incorrect Password Incorrect Password CALLING THE COPS! Incorrect Password your balance is 800 """