Object-Oriented Programming in Common Lisp

Laboratory Exercise 8


810:161

Artificial Intelligence Laboratory


[ Goals | Background | Pre-Lab | In-Lab | Post-Lab | More Info ]

Goals for the Laboratory Exercise

The goals of this lab are two-fold. First, it introduces you to CLOS, Common Lisp's way of doing object-oriented programming. Second, it gives more experience with using local variables: as a way to simulate some of the features of OOP in Common Lisp.


Background

Early in the semester, I claimed that writing new languages was easy to do in Common Lisp, at least as compared to other programming languages. You have already encountered data-driven programming, a technique useful for implementing small language extensions, that is well-supported in Lisp. You have also learned a bit about macros, which let you create your own syntax on top of Common Lisp's syntax.

When some folks decided that they would like to do object-oriented programming but that they didn't want to give up programming in Lisp, they did the natural thing: they implemented ways to do OO in Lisp. For a few years, several competing sets of OO macros and functions were in use. A couple of these even added to the theory of object-oriented programming. Most were based on the idea of a closure, a local data environment in which one can create functions with global scope.

Around 1990, one of the later proposals -- the Common Lisp Object System, or CLOS -- was accepted as the future OO standard for Lisp. It differs from traditional OOP languages in that methods are created independent of classes, but methods can specialize on instances of a particular class. Nowadays, you can implement your own OO system if your task is small, but all large OO programming in Common Lisp is done in CLOS. Graham shows how to implement a different OO language, based on the more traditional message-passing model, in Chapter 17.

By the way, the development of CLOS as a standard OO implementation within Common Lisp is a good example of why Lisp will never die, despite being among the oldest of programming languages. Lisp's incredible flexibility allows us to add new features, new languages, and even new programming styles to Lisp whenever we desire. In this way, Lisp is like a living organism; it can absorb new ideas as they prove themselves useful to programmers.

CMU/CL implements a standard version of CLOS.


Pre-Lab Activities

Prior to doing the in-lab activity, be sure that you have done the following:

  1. Read Chapter 11 in Graham. Feel free to skip Section 11.8; it discusses a topic beyond our scope for this semester.

  2. Read this document in its entirety.

  3. Enter Graham's example code from Sections 11.3, 11.4, and 11.6, as a way to familiarize yourself with the CLOS primitives and with his examples.

Deliverables

Submit by the end of the day Monday, October 22, an e-mail message that contains a transcript of your load and evaluations. Use the built-in function dribble or a Lisp/Emacs buffer to capture your session as a text file.


In-Lab Activities

  1. Let's improve the code defined in Figure 11.2 of Graham, given what you know at the end of the chapter:

  2. Define a bank-account class that keeps track of the name of its owner and its balance. Define two CLOS methods on bank-accounts:

    For example:

         > (setf a1 (make-instance 'bank-account
                        :balance 5000.00 :name "Fred"))
         > (account-name a1)
         "Fred"
         > (account-balance a1)
         5000.0
         > (deposit a1 100)
         5100.0
         > (account-balance a1)
         5100.0
         > (withdraw a1 1000)
         4100.0
         > (account-balance a1)
         4100.0
    

  3. Define limited-account as a subclass of bank-account. A limited-account places an upper limit on how much money can be withdrawn at any one time. The limit is passed as an argument at the time the object is created. You will need to create a specialized withdraw method for limited-accounts. For example:
         > (setf a2 (make-instance 'limited-account
                        :balance 5000.00 :name "Fred" :limit 100.00))
         > (account-name a2)
         "Fred"
         > (withdraw a2 20.00)
         4980.0
         > (withdraw a2 200.00)
         OVER-LIMIT
    

    In order to solve this exercise, you will need a way to invoke the withdraw method inherited from limited-account's superclass. You do this by invoking call-next-method.

  4. Define a macro named send that lets me treat my CLOS objects more like Java objects. The format of send expressions is:

    (send object message( arg* ))

    The message corresponds to a method whose first argument is the object and the remainder of whose arguments are the message arguments.

    For example, following up on the previous example:

         > (send a2 withdraw( 20.00 ))
         4960.0
         > (send a2 withdraw( 200.00 ))
         OVER-LIMIT
    

  5. In Section 6.5, we learned about how closures can bundle functions with variables that are both local and global. The variables in a closure are local to the closure, so other functions cannot access them. But they are global to the functions defined in the closure, which means that those functions can share access to the variables.

    Show how we can use a closure to simulate an object by implementing the bank account class as a closure over a balance variable.

Deliverables

Submit an e-mail message containing only a single, gcl-loadable file containing your solutions to the above exercises by Wednesday, October 24, at 4:00 PM. If you forget what "gcl-loadable" means, please review the instructions in Laboratory Exercise 2..

By that same time, submit a print-out of the same file, stapled, to me in person or to the department office.


Post-Lab Exercise

Describe how to use the CLOS features presented in Section 11.7 to add an audited-account class to our hierarchy of bank accounts. An audited-account maintains an audit trail for all operations executed on an account and their results. The features discussed in Section 11.7 allow you to define this class without replicating any of the code defined in our existing classes!

Deliverables

Send me your description via e-mail by 4:00 PM on Wednesday. I'll give extra credit for working code!


Further Information

Graham is clearly a functional programmer at heart. As a result, his view of other programming styles is sometimes a little biased. For instance, he claims that OOP is prone to creating "spaghetti code". Of course, I can write spaghetti code in OOP, but I can also do so using the combination of macros, higher-order functions, and functions that use special variables. (Isn't that just a fancy name for a global variable??) Perhaps Graham is influenced by the complexity of OOP supported by CLOS. Multiple inheritance and the combination rules discussed in Sections 11.7-11.8 are indeed prone to overly complex code. Used properly, though, they are more powerful than anything Java or C++ or Ada95 give us!

That said, though, I hope now that you understand what he meant when he said earlier in the book that Lisp "transcends" object-oriented programming. Lisp contains features that allow you to implement any sort of OOP that you want to do. For that matter, Lisp transcends every style of programming, since it contains features that allow you to implement any sort of programming style that you want to use -- including procedural programming, because Lisp supports side effects with expressions such as setf and do.


Eugene Wallingford ==== wallingf@cs.uni.edu ==== October 12, 2001