CS 1321 Supplemental Notes
Guide to Object Oriented Programming in Scheme: Part I
------------------------------
Topics this week:
1. Some basic OOP Design
2. Encapsulation in Scheme
3. Objects in Scheme:
(a) Constructors
(b) Accessors/Modifiers
(c) The Service Manager
-----------------------------
Here we go:
We've learned all about functional and procedural programming - now it's
time to learn about Object-Oriented programming! (collective groan from
students...)
NOTE:
Objects take up MANY pages of code (collective groan from TAs...) so some
of these "simple" examples will be lengthy-ish. You'll have to write a
lot this time... :( sorry everyone.
;;;;;;;;;;;;;;;;;;;;
;; 1. OOP DESIGN ;;
;;;;;;;;;;;;;;;;;;;;
An Object is NOT a structure. It is a complex type of function, with a
local copy of both data *and* its own local functions. Every time you
construct an Object of a certain type (or "class"), you get a fresh
new copy of all the data and functions - a new Object.
Some vocab terms to know:
- An Object has its own *personal copy* of:
(a) Individual Data - also known as "fields" or "state"
(b) Common Behaviors - functions, or "methods"
- Each Object's data is independent of other Objects' data
- Data and behaviors often completely hidden from the user
- Useful when modeling several instances of things that are *similar*
in many attributes, yet need to be *independent* of one another
- Almost any real life object could become a Scheme Object - it's similar
to defining structures, but more versatile: with built-in functions!
We define:
(a) Classes - "blueprints" of objects with data and behavior
- every class outlines a "type" of Object.
- similar to define-struct for structures
(b) Instances - actual copies of those objects
- similar to "making" structure instances
- each has its own local data/state
- each has its own copy of common functions
How do we do this? (hint: encapsulation)
;;;;;;;;;;;;;;;;;;;;;;;
;; 2. ENCAPSULATION ;;
;;;;;;;;;;;;;;;;;;;;;;;
- use a (local [ ... ] ...) to enclose data and functions into an object
called a "lexical closure"
- we must *return a function* when creating the object, always!!!
- Never mind why that works - just return *a function!*
For goodness' sake - !return a function! - or be toast on hw 12.
Research the lexical closure theory if you like, but in the end, the
bare-minimum template for encapsulating some data & behavior is here:
------------------------------
(define (make-object ...) ;; <-- NOT a struct command!!!
(local [
;; Some data:
(define data1 ...) ;; <-- NOT functions, just variables
(define data2 ...)
(define data3 ...)
;; Some behavior:
(define (function ...) ...)
] ;; end of object locals
;; after the locals, you *always* return a function:
;;
function ))
-------------------------------
Then, to construct several of these objects, we make instances and give
each a name:
--------------------------------------
> (define instance1 (make-object))
> (define instance2 (make-object))
--------------------------------------
Okay maybe some more creative names than that... but each instance
will have its own copy of the data and the function - all of which we
can then interact with - and each instance is an "individual".
Each instance is completely independent of the other
instances. Each has its own state, and its own copy of all the Object
locals.
;;;;;;;;;;;;;;;;;;;;;;;;
;; SOME EXAMPLE CODE: ;;
;;;;;;;;;;;;;;;;;;;;;;;;
Here's a good, short example to go over: a household light switch.
We have many light switches for different rooms, but they all do the
exact same thing - turn the lights on, and turn the lights off, right?
Okay so that's not very profound... but consider: they are each entirely
independent of one another's current state. Unless we impose serious
energy restrictions or something, the bathroom could care less if the
lights in the living room are on or off.
All of this means - light switches are ideal examples for objects. They
each have their own independent states, yet exhibit common behavior.
We need a "class" to blueprint what the light-switch keeps track of, and
how the light-switch behaves. We then need to make "instances" of these
switches - one for the living room, one for the bathroom, etc. etc..
Note the Contract & Purpose here, for returning a *function*:
------------------------------ CLASS -----------------------------
;; Contract: make-switch: (void) --> (lambda (...) ...)*
;;
;; * Also you could write (void) --> ( --> (void))
;; Dr. Scheme and the text disagree with the lecture slides... :(
;; Just note that you are returning a function of some sort.
;;
;; Purpose: Makes a light-switch & returns its "switch" function.
;;
(define (make-switch)
(local [
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HERE ARE SOME LOCAL THINGS ;;
;; ALL SWITCHES KEEP A COPY OF: ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; the STATE of the light switch:
;;
(define state 'off) ;; <-- initially off
;; the BEHAVIOR of the light switch:
;;
(define (switch)
(begin
(cond [(symbol=? state 'off)
(set! state 'on)] ;; <-- turning on
[else
(set! state 'off) ]) ;; <-- turning off
;; Then let's not forget displaying:
(display state) ))
] ;; end of locals
;; now returning the switch FUNCTION as a value:
;;
switch ))
-----------------------------------------------
This doesn't have an "initializer" or a "service manager" yet, but
it still runs! It's 1337 h4x0r stuff: pure encapsulation, no tinsel!
Java will not show you how this all works on the inside... ;)
You can make multiple instances of switches and give them some
descriptive names:
---- INSTANCES -------------------
> (define bathroom (make-switch))
> (define livroom (make-switch))
> (define basement (make-switch))
----------------------------------
Those are all independent objects - you can see that each of these
definitions returns a lambda from the contract:
---------------------------------------------------
> bathroom ;; <-- just writing the name...
(lambda () ...) ;; <-- ...will return the "switch" function
;; Which means we can execute it by wrapping parens around it:
;;
> (bathroom)
on ;; <-- the current state is displayed
> (bathroom)
off ;; <-- the state changes
> (bathroom)
on ;; <-- and changes back
;; And so forth.
;; The other three are completely independent:
;;
> (livroom)
on
> (basement)
on
> (livroom)
off
> (bathroom)
off
> (basement)
off
----------------------------------------------------
This conserves energy (we turned all the lights off at the end...)
plus it shows each encapsulation having its own state.
Never mind why this works either - this is just how you do it!
Again, to summarize ENCAPSULATION (or LEXICAL CLOSURES) in a nutshell:
1. Write a LOCAL
2. Enclose DATA
3. Enclose BEHAVIOR
4. Return a FUNCTION from the locals
Each time the object is executed, it makes a copy of all the locals, and
returns the function that runs them - you can save that, and give it a
name. THen you can interact with it, like you would with a real object
in the Real World (wow).
;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 3. OBJECTS IN SCHEME ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;
Most objects are not so simple as a light-switch. They may have:
(a) several different ways we would like to initially create the object
(b) several different pieces of data we may wish to retrieve/change
(c) several different functions that we may need to call on the object
Here are some ways to deal with the data complexity: (and some vocab)
(a) Constructors: functions that set data to initial values as the
object is being created
(b) Accessors: functions that return copies of the stored data
for the user to see the object's current state
(c) Modifiers: functions that simply set! data to new values
in order to change the object's current state
Now to deal with the functional complexity inside the object, including
all those accessors & modifiers, we need a "service manager" that will
take requests and literally "serve" functions back to the user:
(d) Service Manager: - takes in a symbol from user
- returns associated local *function* from object
Each of these utilities will go inside the local along with the data
and other functions. Some templates here:
(a) ;; CONSTRUCTORS:
;;
(define (init ...) ;; <-- take some arguments
(begin (set! data1 ...) ;; <-- set all initial values
(set! data2 ...) ... ))
(b) ;; ACCESSORS:
;;
(define (get-<data>) <data>) ;; just return the data
(c) ;; MODIFIERS:
;;
(define (set-<variable> <new-value>)
(set! <variable> <new-value>)) ;; change the state
(d) ;; THE SERVICE MANAGER:
;;
(define (service-manager sym)
(cond [(symbol=? sym 'func1) func1] ;; symbol --> *function*
[(symbol=? sym 'func2) func2]
[(symbol=? sym 'func3) func3]
[ ... ]
[else <*function* to handle invalid input> ] ))
;;;;;;;;;;;;;;;;;;;;;;;
;; SOME EXAMPLE CODE ;;
;;;;;;;;;;;;;;;;;;;;;;;
You should most likely try to finish Hw 11 before looking
through this code. The homework will have sufficient instructions.
This example will take a LOT of time - but it will be *extremely*
worth it to go through an entire object step by step in excruciating
detail. There is no way to absorb all this code in lecture! So it's
up to recitations and examples to clear all this stuff up, step by
grueling step...
Let's create a "toaster" object. It will have the following data:
(a) Slot 1 - empty or toasting
(b) Slot 2 - empty or toasting
(c) Toaster setting - hi/med/low
It will have the following behavior:
(a) Check slot 1 or slot 2
(b) Insert or Remove toast for slots 1 or 2
(c) Change toaster settings
(d) Rattle and make scary "broken toaster" noises
----------------------------- CLASS -----------------------------
;; Contract: make-toaster: symbol --> (symbol --> (lambda (...) ...))
;; Purpose: To initialize a toaster and return its service manager
;;
(define (make-toaster initial-setting)
(local [
;; HERE IS THE BLANK DATA:
;;
(define slot1 0) ;; <-- all zeros for now...
(define slot2 0)
(define setting 0)
;; HERE IS THE CONSTRUCTOR:
;;
;; Contract: init: symbol --> (void)
;; Purpose: initializes toaster data
;;
(define (init factory-setting) ;; <-- given initial setting!
(begin
(set! slot1 empty) ;; <-- the slots should be empty
(set! slot2 empty)
(set! setting factory-setting) ))
;; HERE ARE THE ACCESSORS:
;;
;; All Contracts: check-whatever: (void) --> whatever
;; All Purposes: to retrieve toaster data
;;
(define (check-slot1) slot1) ;; <-- just return the data
(define (check-slot2) slot2)
(define (check-setting) setting)
;; HERE ARE THE MODIFIERS:
;;
;; Contract: change-setting: symbol --> (void)
;; Purpose: attempts to change the toaster's setting
;;
(define (change-setting new)
(cond [(or (symbol=? new 'hi)
(symbol=? new 'med)
(symbol=? new 'low)) (set! setting new)]
[else ;; invalid setting:
(make-rattling-noises)] ))
;;
;;
;;
;; Contract: add-toast: number --> (void)
;; Purpose: adds toast to the specified slot number
;;
(define (add-toast num)
(cond [(and (= num 1)
(empty? slot1)) (set! slot1 'Toasting!)]
[(and (= num 2)
(empty? slot2)) (set! slot2 'Toasting!)]
[else ;; not valid:
(make-rattling-noises)] ))
;;
;;
;;
;; Contract: remove-toast: number --> (void)
;; Purpose: removes toast from specified slot number,
;; also displaying its level of toastiness
;;
(define (remove-toast)
(begin
;; Displaying the ready toast:
(cond [(and (empty? slot1)
(empty? slot2)) (make-rattling-noises)]
[(symbol=? setting 'hi)
(display "Here is some very burnt toast!" )]
[(symbol=? setting 'med)
(display "Here is some burnt toast!")]
[(symbol=? setting 'low)
(display "Here is your fresh bread back!")] )
;; Emptying both slots:
;;
(set! slot1 empty)
(set! slot2 empty) ))
;; HERE IS SOME OTHER BEHAVIOR:
;;
;; Contract: make-rattling-noises: (void) --> (void)
;; Purpose: to scare the daylights out of toaster-users
;;
(define (make-rattling-noises)
(display "KHHTTTRTRTTZZZTTTGZFZRTRTKTTKKKFZ!!!"))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HERE IS THE SERVICE MANAGER: ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Contract: toast-manager: symbol --> (lambda (...) ...)
;; Purpose: retrieves a function from the toaster object
;;
(define (toast-manager what)
(cond
;; NOTE: take a 'symbol --> return a function
;;
[(symbol=? what 'check-slot1) check-slot1]
[(symbol=? what 'check-slot2) check-slot2]
[(symbol=? what 'check-setting) check-setting]
[(symbol=? what 'change-setting) change-setting]
[(symbol=? what 'add-toast) add-toast]
[(symbol=? what 'remove-toast) remove-toast]
[else ;; if nothing matches:
(lambda () (make-rattling-noises))] ))
] ;; end of HUGACIOUS toaster-locals
(begin
;; INITIALIZING TOASTER:
(init initial-setting)
;; RETURNING THE SERVICE MANAGER function name:
toast-manager )))
--------------------------------------------------------
At this point there are THREE layers of complexity:
We have, on the outside, the "make-toaster" function which constructs
a toaster object and initializes it:
----- INSTANCE ------------------------
> (define mytoaster (make-toaster 'med))
---------------------------------------
Remember how Objects work - every time we run the make-toaster
function, it makes a copy of ALL the locals, and returns the service
manager that runs them. So:
The second layer of our (make-toaster ...) is returning the service
manager - the defined "mytoaster" is now really a service manager
function. This means I can feed it symbols, according to the contract:
> (mytoaster 'check-setting)
(lambda () ...)
But feeding the service manager a symbol merely returns yet another
function - this time a function from the innards of the toaster object.
This is the third layer of complexity.
Remember - all that the service manager does is "serve" you the local
function that you cannot get to by yourself. It serves it, but it
doesn't actually *run* it!
By asking for 'check setting, I retrieve the "check-setting" function.
To run that, I need yet another set of parens (joy...) around the whole
thing:
> ((mytoaster 'check-setting)) ;; <-- double parens!
'med ;; <-- now I get the setting
Feel free to investigate more interactions with the toaster object and
ask any questions before things get complicated (inheritance is
on the menu next week...). Important stuff:
(a) Different ways to initialize - possibly from service manager (?)
(b) How to handle invalid symbols in the service manager
(c) How to pass arguments into the service manager, such as:
> ((mytoaster 'change-setting) 'low) ;; <-- note parens
Object stuff summarized:
Inside the LOCAL:
0. Create some DATA
1. Create a CONSTRUCTOR/INITIALIZER for any appropriate data
2. Create ACCESSORS & MODIFIERS for any appropriate data
4. Create BEHAVIOR functions
5. Create a SERVICE MANAGER
Then:
1. Use a (begin ...)
2. Run the "initializer" to initialize the data
3. *RETURN* THE SERVICE MANAGER - i.e. literally return its name