We now know how to write "functions" that remember things.
Write a function named counter that gives us the next integer every time it is called.
> (counter) 1 > (counter) 2 > (counter) 3 > (for-each (lambda (n) (counter)) (range 10000)) > (counter) 10004
Challenge 1: Modify your function to enable us to create multiple counters at the same time.
Challenge 2: Modify your function so that its counters wrap around to 0 when they reach an upper limit.
Here are possible solutions. The function counter is a simple example of a closure, a function that maintains state, an idea we learned about last time. We can create a counter with memory simply by returning a lambda from inside a let. To enable the use of multiple counters at the same time, we return a lambda from within another lambda, creating a counter "factory". To create bounded counters, we add a little logic to to the add1 step so that the function knows when to wrap around.
[ Recall that (for-each proc lst) is a built-in Racket procedure that applies proc to every item in lst, in sequence, like map but done for side effect. It is similar to the homegrown for-each procedure you saw in your reading a couple of sessions back. ]
We are studying how programs can have state. Last time, we introduced the idea of mutable data and saw Racket's set! primitive for modifying the value of an existing object. You also read about the distinction between denoted and expressed values, which connects how programs treats names with the underlying machine model. Here is a simple glossary of terms:
With mutable data, a program can represent values that change over time. However, it needs a way to remember data that is not recreated every time the program is executed. This is the idea of a closure, a function that is created in a local context where an identifier has a binding. The function is able to access the variable even after it is seemingly out of scope, because the closure stores both the function and the bindings.
Now we can see why the region of an identifier is not the same as its scope. In a closure, the region of an identifier is the body of the procedure. However, a closure outlives the identifier, and the scope of the variable is the lifetime of the closure!
A closure creates a hole in space where interpretation is different from the space that surrounds it. We see this idea in the real world, too. For example:
The American embassy in Paris occupies a very nice building on the Place de la Concorde. Certainly, the embassy is physically within the boundaries of France. But when you step inside the embassy, what country are you in? You're no longer in France. You're in the United States, and US law applies.
In a similar way, a closure behaves like a sovereign state. Though the code travels to other locations in the program, the identifiers in that code retain the meaning they had in the code where they were created.
(let ((n 42)) (let ((minutes (make-bounded-int 60))) ... (minutes) ... ))When minutes set!s an n, it is the object inside its closure, not the one that exists when minutes is used.
Changing the value of an object becomes meaningful only now that the object can exist over the course of multiple invocations of the procedure. Procedures that change the value of an object are called mutators, an addition to our vocabulary that refers to any procedure or special form that treats a data objects as variables for the purpose of changing the values, which are stored in particular locations.
Today, we continue with our discussion of programs that have state, including procedures that share data. By the end of the session, you will see how we can use closures of shared variables to implement many of the familiar concepts from object-oriented programming.
At the end of last session, we created a procedure that acts like a constructor for withdrawal procedures:
(define make-withdraw (lambda (balance) (lambda (amount) (if (>= balance amount) ;; balance is bound, but to (begin ;; a new object on each call! (set! balance (- balance amount)) balance) (error "Insufficient funds" balance)))))
To model a real bank account, we need the ability to do more than just withdraw funds. At the very least, we will want to deposit money into the account, by increasing the value of the balance. This requires us to create not just one procedure that can access the value of balance over time, but two. The implementation idea, though, is the same:
create a closure in which a procedure refers to a free, local variableIn this case, though, our closure will contain two procedures!
One approach is to use message-passing style, creating a function that receives as its argument a symbol and uses it to choose which procedure to run. You may remember seeing this idea in Session 22, when we looked a bunch of ways to implement a pair.
Consider the following function, make-account, which receives an initial balance as its only argument. It returns a function that responds to withdraw and deposit messages:
(define make-account ;; Create a function that creates (lambda (balance) ;; a closure around balance. ;========================================================= (lambda (transaction) ;; The function uses its arg (case transaction ;; to select an operation to ('withdraw ;; perform on the balance ;-------------------------------------------------- (lambda (amount) ;; -- returning a procedure! (if (>= balance amount) (begin (set! balance (- balance amount)) balance) (error "Insufficient funds" balance)))) ;-------------------------------------------------- ('deposit ;-------------------------------------------------- (lambda (amount) (set! balance (+ balance amount)) balance)) ;-------------------------------------------------- (else (error "Unknown request -- ACCOUNT" transaction)))) ;========================================================= ))
We can now create different accounts with different balances, and make deposits and withdrawals on each.
> (define account-for-eugene (make-account 100)) > ((account-for-eugene 'withdraw) 10) 90 > ((account-for-eugene 'withdraw) 10) 80 > ((account-for-eugene 'deposit) 100) 180 > (define account-for-bill (make-account 10000000)) > ((account-for-bill 'withdraw) 10) 9999990 > ((account-for-eugene 'withdraw) 10) 170
Each call to make-account creates a procedure that has its own local binding for the variable balance. The resulting procedure is a "selector" that returns either the withdraw procedure or the deposit procedure, depending upon the argument it is given. Both of these procedures access the same balance variable. The deposit operation for Eugene's account refers to the balance in its closure, while the deposit operation for Bill's account refers to its own balance. This is a more general use of the closure, in which mutable data are local to a set of procedures.
When we introduced mutation, sequences, side effects, and closures last time, we began to move from the realm of functional programming to the realm of imperative programming. With the idea of a selector procedure, we have begun to move toward a particular form of imperative programming: object-oriented programming. While we can write OO programs that behave functionally, one of the powerful features of OOP is the ability to create objects that manage their own state.
The closure returned by make-account behaves like a simple object.
The syntax we use to send messages to an object implemented as a selector function is not as convenient as we might like:
> ((account-for-eugene 'withdraw) 10) 90
Message Passing Syntax
The syntax for sending messages to our objects looks very Racket-y. It reflects the fact that a bank account is a procedure and that, when we send it a withdraw or deposit message, the account returns a procedure that we must call. Can you think of a more convenient syntax, one that doesn't expose these implementation details? How might we implement it?
Here is a possible solution:
(define send ; or even "←" (lambda (object message . args) (apply (look-up-method object message) args))) (define look-up-method (lambda (object selector) (object selector)))
Now we can interact with our objects with a more convenient syntax, and with fewer parentheses:
> (send account-for-eugene 'withdraw 10) 160
Why do you think I defined a look-up-method function? Remember the apply-ff function we added to the finite function ADT... Without the look-up-method function, send must assume that objects are implemented as functions. In general, of course, there are an infinite variety of implementations for any data abstraction. We are usually better off building tools that do not assume a particular implementation.
[ Recall that the '.' in send's parameter list works just like the dot in dotted pair notation. The rest of the parameter list is bound to the parameter args, no matter how many arguments are passed. ]
This solution brings to mind an idea we encountered previously in our discussion of finite functions: the use of a function as the concrete implementation of a datatype. As with previous ADTs, we have many alternative implementations available for implementing our little objects. Perhaps a data-based solution would be simpler?
Write a version of make-account that returns a list of procedures that share the balance variable, rather than a message selector.
The code for the withdraw and deposit procedures will be the same, so you don't have to write again. Just write WITHDRAW and DEPOSIT in their places.
Here is a sample solution. We can define functions named withdraw and deposit that access the desired function in our list object, and then call them.
Unfortunately, the syntax for accessing the object and its methods changes with this implementation. That's a sign of that we have not designed a solid abstract interface for bank accounts. Different implementations of an ADT should never affect client code!
Fortunately, we created syntactic sugar to hide such details from our procedure-based implementation. Can we adapt that idea here? You bet we can.
I noted last week that I think this idea of data-as-function is very cool, and closures show another reason why. Keep in mind that data can be implemented as functions, and functions can be implemented as data. Such flexibility makes some problems easier to solve, including some the problems we face when writing a language interpreter.
Let's return to our bank account, implemented as a closure and extended with a balance method:
(define make-account ;; Create a procedure that creates (lambda (balance) ;; a closure around balance. (lambda (transaction) ;; The procedure uses its argument to (case transaction ;; select a particular operation on ('withdraw ;; balance -- returning a procedure! (lambda (amount) (if (>= balance amount) (begin (set! balance (- balance amount)) balance) (error "Insufficient funds" balance)))) ('deposit (lambda (amount) (set! balance (+ balance amount)) balance)) ('balance (lambda () balance)) (else (error "Unknown request -- ACCOUNT" transaction))))))
This one function implements many of object-oriented programming's basic principles:
What else do we need to do object-oriented programming? We might want a cleaner message-passing syntax. The send procedure you implemented earlier moves us in that direstion. But the cleaner syntax is really just sugar.
We can use our message-sending procedure even more conveniently if we make our object a message responder rather than a message selector:
> (define ellen (make-account 100)) > (send ellen 'withdraw 100) 0 > (send ellen 'deposit 50) 50 > (send ellen 'withdraw 10) 40 > (send ellen 'deposit 20) 60
Still, there are important semantic features of OOP that we might want:
Indeed, the idea of a class is missing entirely from our implementation of OOP. If your only exposure to objects is through Python or Java, you may be surprised to learn that not all object-oriented languages have classes! Consider Self, a language designed at Sun beginning in the mid-1980s. In Self, there are no classes, only objects. You create a new class by "cloning" another object, called a prototype. You can add new state and behavior to individual objects. Self is quite cool and has influenced many languages since. You can do some things must more simply and elegantly in Self than in a class-based OO language.
In a dynamically-typed language such as Racket, classes might play a less important role than in a language such as Java. But how might we implement them?
If we think about classes in a different way, we can implement something simple that captures the idea. What if we think of a class as an object that can create instances for us? In that sense, we already know how to implement a class: use the same sort of closure that we use to implement objects!
How about this function:
(define bank-account ;; class (let ((count 0)) ;; class variable (lambda (transaction) (case transaction ('new ;; constructor call = message (lambda (balance) ;; constructor method + inst var (set! count (+ count 1)) (lambda (transaction) (case transaction ('withdraw ;; instance message (lambda (amount) ;; instance method (if (>= balance amount) (begin (set! balance (- balance amount)) balance) (error "Insufficient funds" balance)))) ('deposit (lambda (amount) (set! balance (+ balance amount)) balance)) ('balance (lambda () balance)) (else (error "Unknown request -- ACCOUNT" transaction)))))) ('count ;; class message (lambda () ;; class method count)) (else (error "Unknown request to BANK ACCOUNT class" transaction))))))
> (define eugene ((bank-account 'new) 100)) 100 > ((eugene 'balance)) 100 > ((eugene 'deposit) 100) 200 > ((eugene 'balance)) 200 > ((bank-account 'count)) 1 > (define mary ((bank-account 'new) 100)) > ((bank-account 'count)) 2
Of course, now that bank accounts are objects as closures, too, we don't have to use the parenthesized syntax:
> (define eugene (send bank-account 'new 100)) > (send eugene 'balance) 100 > (send eugene 'deposit 100) 200 > (send eugene 'withdraw 50) 150 > (define mary (send bank-account 'new 1000)) > (define sarah (send bank-account 'new 10000)) > (define ellen (send bank-account 'new 100000)) > (send bank-account 'count) 4 > (send ellen 'balance) 100000
We now have a way to implement multiple constructors, as we see them in Java and C++. We can add another case to the selector procedure returned by bank-account!
In Java, classes aren't objects to which we send messages; class is a special construct. But I learned to program in Smalltalk, where everything is an object, and in Smalltalk a class is an object to which you send a message in order to create an instance of the class. Today, languages like Ruby provide the same feature. With one more level of closure wrapping our object closure, we are able to implement a class with nothing new. As David Wheeler, a programmer in the early 1950s (!) once said,
Any problem in computer science can be solved with another layer of indirection.
Those old programmers. men and women alike, were solving many of the problems that cause us to bang our heads against the wall, and they did so at a time when the tools available were a lot less powerful. Often, constraints make us more creative (optional).
What, if anything, do we have to do to implement dynamic polymorphism using our closure-based objects? Racket is dynamically typed, so we already have the ability to treat any object like some other. Any selector procedure that accepts withdraw and deposit messages can act as a bank account. Client code wouldn't know the difference. This means that dynamic polymorphism is free our implementation of OOP.
If we create a class-based implementation of OOP that checks types, we will need to implement a mechanism to allow polymorphic objects.
I took the example about the American embassy from an early
draft of Matthew Butterick's
The example did not survive into the final version of the book,
an old draft of the chapter
is still available online.