Many computations in real world applications of computing require that we need a concept of state.
Scheme provides a special form (set! var expr) to support this. It is equivalent to the assignment statement var := expr in Pascal, or var = expr in the C language. The interpretation of set! is that the expression expr is evaluated and the variable var is rebound to have the resulting value.
Note that when it occurs at the top level the (define var expr) special form creates a new variable var if it does not already exist, and then does in effect (set! var expr). The following example shows how assignment can be used to record the amount of money held in a single bank account - this is of course equivalent to a very elementary Pascal or C program.
(define balance 100)
(define withdraw!
(lambda (amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
(list 'balance balance)
)
"Insufficient funds"
)
)
)
Note that there is a convention that procedures which act imperatively have
names ending in a shriek, as in withdraw!.
(example '(withdraw! 34) '(balance 66))
(example '(withdraw! 60) '(balance 6))
(example '(withdraw! 8) "Insufficient funds")
When combined with the concept of higher order functions we get
some interesting behaviour. The following example shows how we can make the
balance be a variable associated with an arbitrary number of
individual accounts, essentially by wrapping the withdraw!
function above in an outer function.
(define (make_account balance)
(lambda (amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
(list 'balance balance)
)
"Insufficient funds"
)
)
)
(define jane (make_account 200))
(define andrew (make_account 100))
(example
'(jane 40)
'(balance 160))
(example
'(andrew 45)
'(balance 55))
(example
'(andrew 65)
"Insufficient funds")
(example
'(jane 5)
'(balance 155))
Note that in introducing set! we have destroyed our ability to explain Scheme purely in terms of substitution. Consider evaluating (make_account 100). Substituting 100 for balance in the body:
(lambda (amount) (if (>= balance amount) (begin (set! balance (- balance amount)) (list 'balance balance) ) "Insufficient funds" ) )we obtain
(lambda (amount) (if (>= 100 amount) (begin (set! 100 (- 100 amount)) (list 'balance 100) ) "Insufficient funds" ) )Which has a nonsensical assignment to a constant. When we construct a model evaluators for Scheme in subsequent lectures, we shall see how the conceptually simplest evaluator fails to give an account of set!.
(define (make_account balance)
(let ((n_trans 0))
(lambda (amount)
(if (>= balance amount)
(begin
(set! balance (- balance amount))
(set! n_trans (+ n_trans 1))
(list 'transaction n_trans 'balance balance)
)
"Insufficient funds"
) ;end if
) ;end lambda
) ;end let
) ;end define
(define jane (make_account 200))
(define andrew (make_account 100))
(example
'(jane 40)
'(transaction 1 balance 160))
(example
'(jane 20)
'(transaction 2 balance 140))
(set! expr1 expr2)is interpreted as follows. First, let us note that procedures in UMASS Scheme have an updater attribute, which is normally itself a procedure.
((updater p)This may, of course, result in an error if the updater is not a procedure. For exampleexpr2 expr12 expr13...expr1n)
(set! (+ 2 3) 34)will earn you a scolding from Madeleine Misconception.
The above explanation sounds complicated. However the use of this facility is usually quite simple, since the updaters of built-in functions are designed to do the intuitive thing. For example you can use set! to change the first element of a list.
(define a '(fred joe)) (set! (car a) 'anne) (example '(car a) 'anne)Also, the selector-functions produced by record-class are designed to be used in this way with set!
After and including Version 3.5 of UMASS Scheme, there is a function updater which returns the updater of its argument.
(example (->string (+ 3 4)) "7")
It is fairly easy to say what the functional and logic paradigms are about:
It is somewhat harder to characterise the imperative paradigm.
The object oriented paradigm is about (a) creating "objects" in which behaviour is associated with data and (b) ways in which these objects can be placed in a hierarchy in which one kind of object can inherit the behaviour of others and (c) ways in which the structure of objects can be encapsulated so as to allow only legitimate enquiries to be made about them.
Now (a) and (c) are both properties of closures - a closure is an association of behaviour (i.e. some code) with data, and the data can only be accessed only by passing appropriate arguments to the closure. So there is something of an overlap of capabilities between the function paradigm and the object-oriented paradigm.
We often speak of the piece of code that implements a particular behaviour for a class of objects as a method. In general the essential aspect of object orientation is that a named action applied to an object is mapped on to a piece of code for effecting that action by means of a flexible mapping which takes into account both the name of the action and the class of object to which it is applied.
There are two sub-paradigms within object orientation:
In this sub-paradigm an object is "sent" a "message" to cause it to exhibit behaviour. For example, if we were simulating a university, we might have person objects, each one of which could be told to print himself or herself. This could be accomplished, with a Scheme implementation of this sub-paradigm, by:
(send person 'print)
which might cause the output:
name: Frederika Forsythe age: 42 sex: female
Likewise
(send person 'age)
might return the result 42.
Sometimes arguments are embodied in the message, for example
(send person 'location 234 456)
might be used to reposition a person in a simulation that went into topographic detail.
The most significant development of this paradigm was associated with the Smalltalk language.
Within this paradigm, a general concept of inheritance corresponds to the possibility that one class of objects may share some of the attributes of another class. For example, in our university model we would have students who are a kind of person and professors who are a kind of person. Students will be able to handle all the messages that a person can handle - they have ages, sexes, addresses etc.. However they also have attributes peculiar to students, such as the courses that they take. Moreover they may respond differently to standard messages.
(send student 'print) name: Jeremiah Jolt age: 23 sex: male course: CMPSCI 287, STATS 101
and
(send student 'courses) ==> '( (CMPSCI 287) (STATS 101))
The "send a message" approach has some virtues. It is a quite natural way of talking about a distributed system in which the objects are not all resident on a single computer so that information has to be transferred via some kind of communication link, for example a local area network.
It also has some snags, not least among which is the fact that the nice higher-order functions we have written in this course are not so readily applicable to this paradigm. For example if we wanted to convert a list of students into a list of lists of the courses they were taking we could not use map in quite such a simple way. In the functional paradigm, if courses was a function mapping from students to their courses, we would write
(map courses students)
whereas in the message-sending sub-paradigm we have to write: (map (lambda (s) (send s 'courses)) students)
Moreover the fact that a message has to be addressed to one object makes it tricky to implement certain functions which naturally involve two objects, such as comparing them for equality.
One common term for this limitation is that this approach embodies single dispatch, that is methods are selected on the basis of a single object (and are usually associated fairly directly with that object).
One can regard the C++ language as supporting a single-dispatch approach to method invocation.
The Common Lisp Object System (CLOS) and the objectclass library of the POP-11 language support a different sub-paradigm, one in which object orientation is effected by extending the definition of functions in an incremental way. In Scheme syntax, one would say:
(print person)
and have the same information printed out
name: Frederika Forsythe age: 42 sex: female
However, instead of the print function being defined in one place
(define (print person) (print-basic-info person) (cond ((student? person) (print-student-info person)) ((professor? person) (print-prof-info person)) ) )
the definition of how to print a person is distributed among various method definitions, using a new special form. 3 An example using the POP-11 objectclass library.
By way of a change we will consider the objectclass library for POP-11, written by Steven Knight of Hewlett Packard Laboratories, Bristol. POP-11 is derived from POP-2 [Burstall & Popplestone 1968], and is in essence a form of LISP with ALGOL-60 derived syntax. For example the factorial function in POP-11 is defined as:
define fact(n); if n=0 then 1 else n*fact(n-1) endif enddefine;
To enter POP-11 from Scheme, execute the function call (pop11) in your base window. To get back, type "Scheme". There is no literate version of POP-11, so you'd have to comment out the English (with the /*...*/ comment brackets familiar to C users.
In POP-11, to print an expression expr you do
expr =>
For example
fact(5) =>
will print out
** 120
Finally, to declare a new global variable, and give it a value, one uses vars command, exemplified below:
vars x = fact(120);
creates (if required) a new variable x and makes it have fact(120) as its value.
To load the objectclass library we need to execute the POP-11 command: lib objectclass;
This will take some tens of seconds, during which the message: ;;; Objectclass, version 5.02 ;;; Loading source files. ;;; Done.
is printed out. We are now ready to define our basic person class of objects. This is done with the following syntax:
define :objectclass person name = 'John Doe'; age :int = 25; ;;; The type-constraint is optional. sex = 'male'; enddefine;
This has the effect that each member of the person class is a record with three fields for name, age and sex. These fields are accessible with three functions name, age and sex which are created (or extended) by the objectclass declaration.
The declaration also creates two functions (called newperson and consperson) for creating person objects and one function (destperson) for taking them apart.
We don't need to consider the effect of the destperson function.
Thus, we can create a new person object, and bind the variable p1 to have it as its value by:
vars p1 = newperson();
and print out the new person, using a default print-method that is created when the person objectclass is created:
p1=>
**
If we don't like the way the default print function works, we can define a function print for a person by defining the method:
define :method print(p:person); printf('name: %p \n', cons(name(p),[])); printf('age: %p \n', cons(age(p),[])); printf('sex: %p \n', cons(sex(p),[])); enddefine;
Here printf is a system function which loosely follows the convention of the printf function of the C language, that is to say, the first argument specifies a string with "slots" %p which will be filled in by values taken from the list which is the second argument. The empty list is denoted by [] in POP-11, and cons is a POP-11 function corresponding to the Scheme cons function.
Now if we say:
print(p1);
we obtain the output:
name: John Doe age: 25 sex: male
We can create another person:
vars p2 = consperson('Frederika Forsythe',42,'female' );
so that evaluating:
print(p2)produces the output:
name: Frederika Forsythe age: 42 sex: female
define :objectclass student isa person; classes = ['CMPSCI 287' 'MATH 183']; enddefine;Here the force of the isa construct is to say that the new student class of objects ________inherits all the attributes of a person (name, age and sex) and by default inherits the _______methods that apply to a person. To be precise, the records that implement the student object have fields for name, age, sex, with methods of those same names. However, a student object has a new attribute, namely the classes attribute. vars s1 = newstudent(); If we print s1 using the built-in generic printing function of POP-11, we see that students have inherited the attributes of people, but have classes as well: s1=> **
define:method print(s:student); printf('classes: %p \n', cons(classes(s),[])); enddefine;However if we call this, we simply get the information about classes:
print(s1);gives:
classes: [CMPSCI 287 MATH 183]We could of course print out the people-attributes of a student, by copying our earlier code for printing persons, but more conveniently there is a special form defined in lib objectclass, namely call_next_method, which will, in this case, call the method for the person objectclass.
define:method print(s:student); call_next_method(s); printf('classes: %p \n', cons(classes(s),[])); enddefine;Now we find that:
print(s1);will produce:
name: John Doe age: 25 sex: male classes: [CMPSCI 287 MATH 183]How does call_next_method work? Well, the isa construct defines an ______object _________hierarchy in which the student class comes immediately below the person class. The expression call_next_method(s), knowing that s belongs to the student class of objects, arranges for the method appropriate to the next higher class in the hierarchy, that is the person class, to be called.
This approach supports multiple dispatch, that is to say one can qualify some or all arguments of a method with class membership constraints. For example, one might define one person as being senior to another in terms of age:
define :method senior(p1:person, p2:person); age(p1) > age(p2) enddefine;Whereas one might define one student as being senior to another in terms of the number of classes taken:
define :method senior(s1:student, s2:student); length(classes(s1)) > length(classes(s2)) enddefine;What happens if we try to compare a student with a non-student. The system searches the class-hierarchy to find a method that applies to both. In this case the definition of senior for two people is used. Where a class-hierarchy has some complexity it is possible that more than one method may exist which is applicable to the arguments of a multiply-dispatched method call. What is to happen in this case? In fact lib objectclass uses a simple lexicographical rule to make the choice. The Common Lisp Object System does this by default, although this default is user-modifiable. The Java language, which does this kind of search at compile-time, insists on finding a unique _________maximally ________specific method-descriptor [Gosling, Joy & Steele 1996]. __________References Burstall, R.M. and Popplestone, R.J., [1968] The POP-2 Reference Manual, Machine Intelligence 2, pp. 205-46, eds Dale,E. and Michie,D. Oliver and Boyd, Edinburgh, Scotland. Gosling,J. Joy,B. and Steele,G.[1996] The Java Language Specification. Addison Wesley.