Object-Oriented Programming
The heart of object-oriented programming is one simple idea: things know how to do stuff. In more technical terms, we say our data is represented by objects, which have both methods (things they can do) and local state (information about the object that is specific to the object). The latter is implemented by variables, which can be defined with a default value or set at instantiation time. (Of course, you can modify either kind of variable later with set!
.) The last piece of OOP is inheritance, which means using parent classes to share common behavior and allow common interactions for similar objects.
Here are the most important rules for determining the relationship between two things:
- If "every A is a B", then A is a child class of B (and B is a parent class of A).
- If "A is a specific B", then A is an instance of B. B is a class.
- If "every A has a B", then B is an instance or instantiation variable of A. You can tell which by seeing if "every A starts with the same B" (instance), or not (instantiation). A is a class.
- If "every A shares a B", then B is a class variable of A. A is a class. (As mentioned in section, class variables don't come up very often.)
There are already many great references for our object-oriented system and OOP in general in your reader. The best places to look are:
- The "Above the line view" reference at the beginning of the Volume II reader.
- The "Reference Manual" just after the explanation above. (In my Volume II reader it's on page 11, but I have an old copy.)
- Brian's lecture notes at the end of Volume II
- Other TAs' handouts and practice problems for Week 7.
If you're looking for some practice, here are several problems that test your understanding of object-oriented programming.
-
Given this class:
(define-class (ucb-person name id-number) (instance-vars (bus-pass #f)) (method (get-bus-pass) (set! bus-pass #t)) )
Make a subclass
student
that has a GPA, which starts at 4.0, and a methodupdate-gpa!
, which just sets the GPA to the value it's given.(define-class (student name id) (parent (ucb-person name id)) (instance-vars (gpa 4.0)) (method (update-gpa! new-gpa) (set! gpa new-gpa)) )
There are two things to note here. The first is that when we had the
parent
clause, we had to not just name the parent but pass along the right instantiation variables to instantiate the parent. That means we often end up having the same instantiation variables in the child. Yes, you do have to say it twice -- it's a feature, it's not required that everything stay the same. Notice how we didn't have to keep the variable names the same, either.The other thing to ask is "how do we know if something's an instance variable or an instantiation variable"? Well, if you know what the value of the variable's going to be ahead of time, as in this case for
gpa
, you probably want an instance variable. If you want it to be specified as part of making an object (instantiation), use an instantiation variable. -
Now instantiate a student named Stewart Dent, who has an SID of 31337.
(instantiate student '(stewart dent) 31337)
This will return a new object, but note that it doesn't attach it to any variable! Usually when testing things like this, you're going to want to give the object a Scheme name (i.e. use
define
). -
Students often have blocks on their record that preven them from getting a bus pass. Override
get-bus-pass
(make a new version in the subclass) so that if the student's GPA is less than 1.0, the method returns the sentence(no pass for _____)
, where the blank is the student's ID number. If the GPA is greater than or equal to 1.0, the method should work as usual.(method (get-bus-pass) (if (< gpa 1.0) (se '(no pass for) id) (usual 'get-bus-pass) ))
This is designed to get you to use
usual
, which is how you can call a parent's implementation of a method. It's kind of like(ask self ...)
, except the parent will answer.There's also an interesting point regarding
id
. Because we automatically get methods for any instance or instantiation variables we define,(ask self 'id)
or(ask self 'id-number)
would have worked fine. But just sayingid-number
would not. Why? Because our OOP system doesn't let you refer directly to a parent's variables; you have to go through the methods. Just something to keep in mind. -
We talked a lot about how data abstraction means you don't care how something is implemented. Let's prove it, by implementing Trees as objects in our object-oriented system! Make sure you have equivalents for
make-tree
,datum
,children
, andleaf?
.(define-class (tree datum children) (method (leaf?) (null? (ask self 'children)) ))
It really is that simple. Remember that in our system, methods are automatically created for all instantiation and instance variables.
Why use
(ask self 'children)
instead of just accessingchildren
directly? Well, if some subclass wants to change the way children are stored, they now effectively do so by overriding thechildren
method. (This was actually a suggestion from a past student, and something to keep in mind when designing large OO class hierarchies! It's a little more complexity now, but it gives you more flexibility later — the question is if you're going to need it.)If we want existing Tree code to work with our new implementation of Trees, we need to change the constructors and selectors as well.
(define (make-tree datum children) (instantiate tree datum children)) (define (datum tree) (ask tree 'datum)) (define (children tree) (ask tree 'children))
Notice that we don't strictly have to change the procedure
leaf?
, since our original implementation only used the above-the-abstraction view of trees. -
Now, how can we convert an existing Tree (created with the old
make-tree
) into this representation? (This question was originally asked by a student, and is a good example of simple tree recursion as well.)We assume that the
children
anddatum
procedures still work for non-OO trees.(define (convert-tree classic-tree) (instantiate tree (datum classic-tree) (map convert-tree (children classic-tree)) ))
Again, pretty straightforward. We use map to convert all of a node's children to OO trees, then instantiate an OO tree with the original datum and the list of OO trees.
If you're feeling ambitious, you might recognize this as a similar situation to one we've seen before. Multiple implementations of the same data type...perhaps a place for generic programming? As an extra challenge, try implementing
make-tree
,datum
, andchildren
(andleaf?
) in a data-directed system, where the type tags areclassic
andobject
. -
We saw in lecture that cons pairs can be implemented using
lambda
, in a style that we now see is basically message-passing. (The code is in ~cs61a/lectures/2.1/cons.scm on the inst machines; similar code is in SICP on page 91.)Now, try implementing pairs using our object-oriented system, and show how to write procedures
kons
,kar
, andkdr
that behave exactly likecons
,car
, andcdr
when used together.(define-class (pair car cdr)) (define (kons ar dr) (instantiate pair ar dr)) (define (kar pair) (ask pair 'car)) (define (kdr pair) (ask pair 'cdr))
That really is it. Like the example that solely used
lambda
, it's mostly a brain exercise. There are two things to take away from this:- In a language based around the object-oriented style (such as Java), this is basically how you'd implement cons pairs.
- In our object-oriented system, the OO solution is basically implemented as the
lambda
solution. More to come next week!
-
Now, using this implementation of pairs, add a method
print-pair-structure
which prints (using thedisplay
procedure) the pair representation just like Scheme would with regular pairs. (For an easier problem, ignore the ". (
" cancelling. But even with, it's not that hard.) Hint: you're going to want a helper method that does most of the work.You may assume you have a procedure
oo-pair?
which tests if something is a pair. (For a quick and dirty way to test your own code,(define oo-pair? procedure?)
, since as we've hinted objects are implemented using lambda. This doesn't allow any objects or procedures in your pair structure, though.)(Hint: think about this one first — given a box-and-pointer diagram, how do you print it out? Then write down pseudocode. Finally, translate that pseudocode into methods.)
(define-class (pair car cdr) (method (print-pair-structure) (display "(") (ask self 'print-pair-contents) (display ")") (newline) ) (method (print-pair-contents) (if (oo-pair? car) (begin (display "(") (ask car 'print-pair-contents) (display ")") ) (display car) ) (display " ") (cond ((oo-pair? cdr) (ask cdr 'print-pair-contents) ((not (null? cdr)) (display ". ") (display cdr) ))))
Run through your own by-hand algorithm for converting box-and-pointer diagrams to Scheme expressions and notice which parts of your own process correspond to pieces of the method above. Notice how the car and cdr are treated differently! Also notice how the final
cond
has noelse
clause, effectively ignoring the empty list as a cdr (as it should).
Mutation (and Doing Several Things)
In CS61A OOP, we're introduced to the special obj.scm
syntax for classes and objects. But we also need two more Scheme special forms: set!
and begin
.
As a convention in Scheme, procedures and special forms dealing with mutation (changing things) end with an exclamation point, to mark that they are non-functional and might affect another part of the program. This is like how we use question marks for predicates (procedures returning #t
or #f
. I strongly encourage you to follow these conventions!
The set!
special form (pronounced "set-bang") has the same syntax as define
, where the first argument is the name of the variable you're setting, and the second is the value you're setting it to. Note that, like define
, the first argument is not evaluated, though the second one is.
set!
is used to change the value of existing variables, while define
is used to make new variables. We'll see how this works next week, but for now, consider the following:
What will Scheme print?
-
> (define a 3) > (let ((b (+ a 1))) (define a (* b 2)) ; this is local to the LET (- a) ) _______ > a _______ > (define c 3) > (let ((d (+ c 1))) (set! c (* d 2)) (- c) ) _______ > c _______
-8 3 -8 8
Both
let
expressions have the value-8
, since the expressions used to produce the result are basically the same. However, thedefine
creates a new variablea
that hides the outsidea
, and once thelet
is done that insidea
goes away.set!
, on the other hand, changes the globalc
that's defined above.
Our other new special form is begin
. This allows you to do multiple things in a row; the value of a begin
expression is just the value of the last thing you do. So, (begin (display 3) 5)
will display a three and then has the value 5. (Note that the value is not okay
, or okay
and 5, or (okay 5)
!)
It turns out that cond
clauses and lambda
bodies have "implicit begin
s". So you can already say (lambda (x) (display x) (* x x))
, without a begin
. This also applies to anything implemented using lambda, such as procedure definitions, method definitions, and let
. This means that in practice, you end up only needing begin
when working with if
.
There is an important distinction between displaying a value and returning a value. Almost always, you want to return a value, so it can be used by the calling procedure. (For example, if you ask for a person
's name, having it printed doesn't help if you're trying to make a list of names.)
Stay tuned for next week, in which we learn what's underneath obj.scm
's OOP. (Spoiler: it's lambda!)
Back to Jordy's home page | Course home page