Page Numbers: Yes X: 306 Y: 1.0" First Page: 1
Margins: Top: 1.0" Bottom: 1.3"
Heading:
LECTURE NOTES #8 LISP: LANGUAGE AND LITERATURE May 8, 1984
————————————————————————————————————————————
Lecture Notes #8 State, Polymorphism, and Message-Passing
Filed as:[phylum]<3-lisp>course>notes>Lecture-08.notes
User.cm:
[phylum]<BrianSmith>system>user.classic
Last edited:
May 8, 1984 1:15 PM
————————————————————————————————————————————
A. Introductory Notes
Problem Set #2; not ready yet. Will try to have it ready on Thursday.
As today’s lecture will make clear, it is probably time to read chapter 3 of A&S (hope people have read 1 and 2 by now).
B. Data Representation with State
Talked a while ago about simple data types (points, etc.). As suggested in the syllabus, will come back to more complex data representation, dealing in particular with the question of state.
Last week talked about semantic questions, about languages, information, constants, etc.
I said that there was a problem, but I think I didn’t perhaps get across as well as I might just what that problem is.
In particular, got into more complexity than was perhaps clear. Will come back to it presently.
What we will do today is some natural programming work, exploring more issues of abstraction, modularity, and something we will call encapsulation, and will come back at the end to these semantics questions, which I think are still very complex.
So, take a simple problem (from A&S): represent a simple bank account.
Want to add money, take money out, and check the current balance:
1(let [[balance 0]]
(define WITHDRAW
(lambda [amount]
(if (< balance amount)
"Insufficient funds"
(begin (set balance (- balance amount))
balance))))
(define DEPOSIT
(lambda [amount]
(set balance (+ balance amount))))
(define CURRENT-BALANCE
(lambda [] balance)))
This will nicely support:
11> (current-balance)
1= 100
1> (deposit 23)
1= 123
1> (withdraw 40)
1= 83
1> (withdraw 100)
1= "Insufficient funds"
However, this supports only a single account, which clearly won’t do. So define a MAKE-ACCOUNT that takes an initial balance:
1(define MAKE-ACCOUNT
(lambda [balance]
(define WITHDRAW
(lambda [amount]
(if (< balance amount)
"Insufficient funds"
(begin (set balance (- balance amount))
balance))))
(define DEPOSIT
(lambda [amount]
(set balance (+ balance amount))))
(define CURRENT-BALANCE
(lambda [] balance)))))
Note that the bound variable balance does the work of acting as the place that information is stored.
However this redefines WITHDRAW, DEPOSIT, etc., every time a new account is made. Not only is the balance local to the account, in other words, so are the closures to which WITHDRAW, DEPOSIT, and CURRENT-BALANCE are bound. So this doesn’t solve the problem.
Here is a solution:
1(define MAKE-ACCOUNT
(lambda [balance]
(letrec [[WITHDRAW-FUN
(lambda [amount]
(if (< balance amount)
"Insufficient funds"
(begin (set balance (- balance amount))
balance)))]
[DEPOSIT-FUN
(lambda [amount]
(set balance (+ balance amount)))]
[CURRENT-BALANCE-FUN
(lambda [] balance)]]
(lambda [message]
(cond [(= message ’withdraw) withdraw-fun]
[(= message ’deposit) deposit-fun]
[(= message ’current-balance) current-balance-fun]
[$T (error "unknown message" message)])))))
MAKE-ACCOUNT, in other words, sets up three private (local) procedures that do the right thing with the balance, and then returns, as the result of making an account, a procedure that takes "messages", and returns the appropriate private procedure. So, if A1 was an account, then (A1’deposit) would designate the deposit function that was specialized for this particular account.
A "message", essentially, is an argument that is merely a flag or structural identifier that is used solely for its identity.
So we would have:
11> (set A1 (make-account 100))
1= {simple closure ... }
1> ((A1 ’deposit) 2000)
1= 2100
1> ((A1 ’withdraw) 40)
1= 2060
1> ((A1 ’current-balance))
1= 2060
This is perfectly good functionality, but it isn’t sufficiently abstract: the way in which the account is being represented is visible. So define corresponding procedures:
1(define WITHDRAW
(lambda [account amount]
((account ’withdraw) amount)))
1(define DEPOSIT
(lambda [account amount]
((account ’deposit) amount)))
1(define CURRENT-BALANCE
(lambda [account]
((account ’current-balance)))
So, the four functions:
MAKE-ACCOUNT
WITHDRAW
DEPOSIT
CURRENT-BALANCE
form the interface of this abstract data type.
Can similarly define a more abstract type, a CELL:
1(define CELL
(lambda [contents]
(letrec [[UPDATE-FUN (lambda [new-contents]
(set contents new-contents))]
[CONTENTS-FUN (lambda [] contents)]]
(lambda [message]
(cond [(= message ’contents) contents-fun]
[(= message ’update) update-fun]
[$T (error "unknown message" message)])))))
1(define UPDATE
(lambda [cell new-contents]
((cell ’update) new-contents)))
1(define CONTENTS
(lambda [cell]
((cell ’contents))))
Which would support such things as:
11> (set C1 (cell "This is a string"))
1= "This is a string"
1> (set C2 (cell 200))
1= 200
1> (contents C1)
1= "This is a string"
1> (update C1 factorial)
1= {factorial closure}
1> ((contents C1) 6)
1= 120
1> (+ 3 (contents C2))
1= 203
We could now define ACCOUNTS in terms of CELLS, which would be good for modularity (since it localizes the reading/updating functionality in one place), although it actually makes the definition a little more complex and difficult to read:
1(define MAKE-ACCOUNT
(lambda [balance]
(let [[account (cell balance)]]
(letrec [[WITHDRAW-FUN
(lambda [amount]
(if (< (contents account) amount)
"Insufficient funds"
(update account
(- (contents account) amount))))]
[DEPOSIT-FUN
(lambda [amount]
(update account (+ (contents account) amount)))]
[CURRENT-BALANCE-FUN
(lambda [] (contents account))]]
(lambda [message]
(cond [(= message ’withdraw) withdraw-fun]
[(= message ’deposit) deposit-fun]
[(= message ’current-balance) current-balance-fun]
[$T (error "unknown message" message)]))))))
(define WITHDRAW
(lambda [account amount]
((account ’withdraw) amount)))
(define DEPOSIT
(lambda [account amount]
((account ’deposit) amount)))
(define CURRENT-BALANCE
(lambda [account]
((account ’current-balance)))
More interesting, however, is to abstract the message-passing definitions (the definitions that make WITHDRAW a procedure that sends ’WITHDRAW to the account, etc.). We would like something like the following:
1(define DEFINE-MESSAGE
(lambda [message]
(define <message>
; ???
(lambda args
(((first args) message) . (rest args))))
since we wouldn’t know, for any given message, how many arguments the corresponding internal function should receive. But, as indicated in the comment line, we can’t quite do this, because <message> will be a handle, and we want the unquoted atom in that part of the define.
What is going on is that we would like a (DEFINE-MESSAGE ... ) form to be roughly a schema for definitions of messages. It is not a function, in the ordinary sense.
I.e., DEFINE-MESSAGE should be associated with a procedure that will produce that corresponding DEFINE forms. In other words, with the name DEFINE-MESSAGE we would like to associate a procedure, so that given for example that following structure:
1(define-message WITHDRAW)
it would produce the structure:
(define WITHDRAW
(lambda args
(((first args) ’withdraw) . (rest args))))
which of course is equivalent to our original definition:
(define WITHDRAW
(lambda [account amount]
((account ’withdraw) amount)))
Such procedures that map structure onto structure, so that "everything works out properly" are called macros. We won’t talk about in this class (or at least, not yet), but they can be defined.
In fact a definition of exactly such a DEFINE-MESSAGE procedure is listed at the end of these notes.
So, let’s just assume we have it for now. This means we can define CELLS as follows:
1(define CELL
(lambda [contents]
(letrec [[UPDATE-FUN (lambda [new-contents]
(set contents new-contents))]
[CONTENTS-FUN (lambda [] contents)]]
(lambda [message]
(cond [(= message ’contents) contents-fun]
[(= message ’update) update-fun]
[$T (error "unknown message" message)])))))
(define-message UPDATE)
(define-message CONTENTS)
It should be clear that we could abstrac the code at the end, that associates messages with internal functions. Indeed, that wouldn’t be too hard to do. But let’s be more ambitious, and wrap all of this stuff up together into a DEFINE-OBJECT-TYPE:
First, identity the functionality we want. It would be nice simply to be able to write such things as:
1(define-object CELL
(lambda [contents]
[UPDATE (lambda [new-contents]
(set contents new-contents))]
[CONTENTS (lambda [] contents)]))
and
1(define-object MAKE-ACCOUNT
(lambda [balance]
[WITHDRAW (lambda [amount]
(if (< balance amount)
"Insufficient funds"
(set balance (- balance amount))))]
[DEPOSIT (lambda [amount]
(set balance (+ balance amount))]
[CURRENT-BALANCE (lambda [] balance)]))
and have all of the message-passing stuff done for us. I.e., this last would define MAKE-ACCOUNT, and have it return a procedure that fields messages, associating them with the functions corresponding to WITHDRAW, DEPOSIT, etc. Similarly, the names WITHDRAW, DEPOSIT, and CURRENT-BALANCE would be defined to send the appropriate messages.
Once again, this is perfectly straightforward, but it requires a macro, (a very simple version of which is) listed at the end of the notes.
We can try this on a new problem:
1(define-object FAMILY
(lambda [mother father kids]
[MOTHER (lambda [] mother)]
[FATHER (lambda [] father)]
[KIDS (lambda [] kids)]
[NEW-KID (lambda [kid] (set kids (append kids [kid])))]))
supporting:
1> (set LeVesques (family "HECTOR" "PAT" []))
1= {simple closure
... }
1> (father LeVesques)
1= "Hector"
1> (mother LeVesques)
1= "Pat"
1> (new-kid LeVesques "Michele")
1= [Michele]
1> (new-kid LeVesques "Rene")
1= ["Michele" "Rene"]
1> (kids LeVesques)
1= ["Michele" "Rene"]
C. Mutable Structures
Given this machinery, we will define something very much like a rail, except that it can be changed. Call it an m-rail, in which you can replace any element or any tail.
In original 3-LISP, all rails were like this.
Need to define versions of FIRST, REST, CONS, NULL, LENGTH, RAIL (the type predicate), etc.
Straightforward code:
1(define-object M-CONS
(lambda [element tail]
[M-RAIL (lambda [] $true)]
[M-FIRST (lambda [] element)]
[M-REST (lambda [] tail)]
[NEW-FIRST (lambda [new-element] (set element new-element))]
[NEW-REST (lambda [new-tail] (set tail new-tail))]
[M-NULL (lambda [] $false)]
[M-LENGTH (lambda [] (+ 1 (m-length tail)))]))
(define-object EMPTY-M-RAIL
(lambda []
[M-RAIL (lambda [] $true)]
[M-FIRST (lambda [] (error "empty m-rail" ’?))]
[M-REST (lambda [] (error "empty m-rail" ’?))]
[NEW-FIRST (lambda [new-element] (error "empty m-rail" ’?))]
[NEW-REST (lambda [new-tail] (error "empty m-rail" ’?))]
[M-NULL (lambda [] $true)]
[M-LENGTH (lambda [] 0)]))
Various problems.
For example, this isn’t any good as a definition of M-RAIL, because M-RAIL of anything that isn’t an m-rail will cause an error; should return $FALSE.
Nonetheless, it works OK, supporting such behaviour as:
11> (set M1
(m-cons 10
(m-cons 20
(m-cons 30
(empty-m-rail)))))
1= {simple closure
... }
1> (m-first M1)
1= 10
1> (m-length M1)
1= 3
1> (m-rest M1)
1= {simple closure
... }; Not too useful
1> (new-first M1 "Hi there")
1= "Hi there"
1> (m-length M1)
1= 3
1> (m-first M1)
1= "Hi there"
1> (new-rest M1 (empty-m-rail))
1= {simple closure
... }
1> (m-length M1)
1= 1
Why do we call it a rail, as opposed to a sequence?
It looks, after all, as if we can have (M-RAIL 10 factorial) i.e., can have "external" objects part of it.
Answer is same old problem: you can’t change a sequence, since they are mathematical abstractions.
Can define some new platonic entity, called a mutable sequence, but that is crazy.
Also, the point of representing state here is to change information in the machine; saying (NEW-KID LeVesques ’Rene) didn’t give them a new child.
Same old problem! want to characterize the machine externally, but don’t want to take that as a reason to forget that it is the machine that we are characterizing.
As indicated in the comment, it is hard to see these new rails. Define a special M-EXTERNALIZE that will convert them into a more convenient notational format. Uses angle brackets:
1(define-object M-CONS
(lambda [element tail]
[M-RAIL (lambda [] $true)]
[M-FIRST (lambda [] element)]
[M-REST (lambda [] tail)]
[NEW-FIRST (lambda [new-element] (set element new-element))]
[NEW-REST (lambda [new-tail] (set tail new-tail))]
[M-NULL (lambda [] $false)]
[M-LENGTH (lambda [] (+ 1 (m-length tail)))]
[M-EXTERNALIZE (lambda []
(let [[rs (m-externalize tail)]]
(string-append
"<"
(externalize ↑element)
(if (m-null tail) "" " ")
(substring 2 (string-length rs) rs))))]))
(define-object EMPTY-M-RAIL
(lambda []
[M-RAIL (lambda [] $true)]
[M-FIRST (lambda [] (error "empty m-rail" ’?))]
[M-REST (lambda [] (error "empty m-rail" ’?))]
[NEW-FIRST (lambda [new-element] (error "empty m-rail" ’?))]
[NEW-REST (lambda [new-tail] (error "empty m-rail" ’?))]
[M-NULL (lambda [] $true)]
[M-LENGTH (lambda [] 0)]
[M-EXTERNALIZE (lambda [] "<>")]))
Thus we get:
11> (set M1
(m-cons 10
(m-cons 20
(m-cons 30
(empty-m-rail)))))
1= {simple closure
... }
1> (m-externalize m1)
1= "<10 20 30>"
1> (begin (new-first M1 (factorial 6))
(m-externalize m1))
1= "<120 20 30>"
1> (new-rest M1 (m-empty-rail))
1= {simple closure
... }
1> (m-externalize M1)
1= "<120>"
D. Generic Operators and Polymorphism
This is all fine, but there is another abstraction that we need. Define it so that the same LENGTH, FIRST, REST, etc., can work on all of these various types:
We can’t redefine FIRST and REST, etc., because they are primitive (and 3-LISP isn’t set up this way), but we can define a generic (or polymorphic) version, called REST!:
1(define REST!
(lambda [arg]
(cond [(rail arg) (rest arg)]
[(sequence arg) (rest arg)]
[(m-rail arg) (m-rest arg)]
[$T (error "type error for rest" ↑arg)])))
This requires that we fix up our definition of M-RAIL, which isn’t so easy, as it happens, because we are modelling everything with functions. But it can be done; let’s just assume it for now.
Supports:
11> (length! "This is a test")
1= 14
1> (length! [10 20 30])
1= 3
1>
In fact, once this is proposed, imagine how useful it would be to do this for:
LENGTH combining LENGTH, STRING-LENGTH, M-LENGTH, etc.
REVERSE combining REVERSE, STRING-REVERSE, etc.
EXTERNALISE
The point of the last would be that, by defining M-EXTERNALIZE, we would be able to tap into the way that the system prints things out -- i.e., so that when an M-RAIL was returned, it would print out using angle-brackets directly; we wouldn’t have to call M-EXTERNALIZE explicitly in order to see it.
What about =?
Here we go again: same old problem. Come back in a moment.
In fact we wouldn’t need to define these polymorphic functions (whose definitions, as illustrated in the example just given, basically dispatch on the type of the argument), if everything were done with messages.
Objects that do this are called message-passing languages. Like function application, message-passing can be turned into the only combinatory mechanism in a system.
Then function application, which we have used to model message-passing, can in these languages be modelled with message-passing.
In fact I have played a little with a language design in which there is no fact of the matter as to whether function application or message-passing is the primitive means of combination; it is rather a matter of the point of view.
Hierarchies of types:
Often want sub-types, such as:
families with, say, adopted children as well as their own.
Polygons (drawing from A&S page 130): rectangle, parallelogram, etc.
Justified text strings (under text strings more generally)
Point is that many of the operations will be inherited from the higher types
Length in characters, for the justified text, for example;
Father, mother, etc., for families
Sometimes you may have special ways of doing things; otherwise default to the higher types
Area for polygons, for example
This is an enormously complex area, lots of active research:
Typical problems that must be dealt with:
Clashes in multiple super-types
E. Semantics
Go back to the question of a polymorphic equality. Question, like last time, is what are we to do with:
11> (set F1 (family "Tully" "Cleopatra" []))
1= {simple closure}
1> (set F1 (family "Cicero" "Cleopatra" []))
1= {simple closure}
1> (= f1 f2)
Error: Equality not defined over functions
But this isn’t the point: this is taking our question as one over the model, not over the families being modelled.
Chances are, need equations about who is who, and who isn’t who else, and so forth. No particular reason to suppose that a simple data base about families will know which representations are about the same family, which aren’t. I.e., it is a genuine question.
What is going on, however, can be described.
Roughly, the code within the functions associated with the data type deals with the representation of information;
The functional interface of the data type is meant to be interpretable as defined over the object represented.
I.e., there is, roughly, a shift as between the internal code and the externally-available procedures.
However, there is an ambiguity as to whether the shift is:
a.between model and thing modelled, in our standard sense, or
b.between structural representation and thing represented.
May not, in the end, be able to distinguish the two very easily, if you model the representation!
Don’t want to unleash another version of the same discussion we had at the end of last week’s class.
Merely say that there is an apparent tension here between abstraction, for which we are pushing so hard, and semantics, in terms of both the modelling relationship, and the designation relationship.
When we get to talking about explicit theorizing, will try to tell a clearer story.
Meanwhile, we will just try to be aware of the semantic import of everything we do.
Appendix. Supporting Macro Definitions
Included without comment:
(define DEFINE-MESSAGE
(mlambda [call]
’(define ,(arg 1 call)
(lambda args
(((first args) ,↑(arg 1 call)) . (rest args))))))
(define DEFINE-OBJECT
(mlambda [call]
(let [[name (arg 1 call)]
[variables (arg 1 (arg 2 call))]
[pairs (rest (pargs (arg 2 call)))]]
(let [[fun-names (map (lambda [pair] (acons)) pairs)]]
’(begin
(define ,name
(lambda ,variables
(letrec ,(map (lambda [pair fun-name]
’[,fun-name ,(second pair)])
pairs
fun-names)
(lambda [message]
(cond . ,(map (lambda [pair fun-name]
’[(= message ,↑(first pair))
,fun-name])
pairs
fun-names))))))
,(map (lambda [pair]
’(define-message ,(first pair)))
pairs)
,↑name)))))