These are a set of notes I'm taking about learning the class system in
mzlib, the standard library that comes with mzscheme. I'm not already
that familiar with it; for me, it's always a little hard for me to
learn from the existing reference documentation:
http://download.plt-scheme.org/doc/301/html/mzlib/mzlib-Z-H-4.html#node_chap_4
but that's precisely because it's doing its job at being a very terse
and dense repository of knowledge.
But since I'm also a bit dense, I'll try to write a leisurely-paced
tutorial on the class system, targeted toward Java or Python
programmers; it should help people who come from those backgrounds to
get a minimal foothold on using the standard OOP framework in
mzscheme. Basically, this is the beginner's guide I wish someone had
written for me.
There's another guide to the OOP system that may be very helpful to people:
Scheme with Classes, Mixins, and Traits.
The OOP system in mzscheme is extensive enough that there are several
ways to express the same thing, and there are other places where the
system goes beyond the support that "mainstream" languages provide.
So these notes are certainly not supposed to be comprehensive. Also,
I'm still a newbie, so some of what I write might be horribly wrong.
Feedback and corrections will be greatly appreciated.
I'll try to maintain a list of links on the bottom for supplementary
material.
Like many of the subsystems in mzscheme, the class system is actually
a part of
mzlib, and it's a bit surprising to realize that it's
treated as an optional add-on, just like any other module library.
If we start off
mzscheme and try something obvious, like:
> class
reference to undefined identifier: class
repl-1:1:0: class
we see that
class isn't even a syntactic keyword at this point. So
let's begin by pulling in the class support.
> (require (lib "class.ss"))
> class
repl-3:1:0: class: bad syntax in: class
Ok, that's progress, since we now see that the error message has
changed to say that we're just writing something syntactically silly.
But now that we have class support, let's first start off with a
simple toy example. In Java, we might see a beginning class like:
public class Person extends Object {
public String name;
public Person(String name) {
this.super();
this.name = name;
}
public void sayHello() {
System.out.println("hello, my name is " + this.name);
}
} |
which demonstrates how to define a Java class with a constructor and a
simple method for salutation's sake. [Side note: In Java, some of the
things above can be left out, like the
extends Object part. But to
make things easier to see in comparison to mzscheme's class system,
I'm making those constructs explicit. Also, we're making name a public
attribute for the moment, though we'll touch on privacy in just a moment.]
Let's see what this looks like in
mzscheme:
> (define person%
(class object%
(init-field name)
(super-new)
(define/public (say-hello)
(printf "hello, my name is ~a~%" name))))
Ok, this is not too different from Java. The conventional way to name
a mzscheme class is to use a
% suffix on the class name, and I'll
follow that convention for the rest of this tutorial. We also see
that there is already a built-in
object% base class:
> object%
#<struct:class:object%>
Ok, now that we have a simple
person% class defined, how do we
make people? In Java, we'd say:
Person p = new Person("mccarthy"); |
In
mzscheme, we can fire off an instantiation similarly:
> (define p (new person% [name "mccarthy"]))
new is a special form that allows us to make instances of classes,
and as we can see here, it can take class name and parameters that we
use to initialize our instance and its fields.
There are other ways of instantiating objects:
The differences between these forms has to do with the way we pass
initial parameters to our instances.
new gives us the option to
pass things via keywords,
make-object allows us to pass values
positionally, and
instantiate allows both positional and keyword
arguments.
For these notes, we'll be using
new just because, well, I want these
notes to be as simple as I can.
All subclasses must call the subclass's
super initializer, just to
ensure that everything's initialized properly. But out of curiosity, what
happens if we don't? Let's make a quick class that doesn't call
super-new.
> (define broken% (class object%))
We don't see anything break yet, but if we start trying to use the
class, we'll see problems:
> (new broken%)
instantiate: superclass initialization not invoked by initialization for class: broken%
So we end up seeing a runtime error during instance instantiation, and
the error message accurately reflects this.
(Also note that the error message mentions "instantiate" --- I guess
this means that
new expands out to a call to the more general
instantiate form.)
Of course, once we have such an instance, we need to know how to fire
messages off to an instance. In Java, we do this by using dot-notation:
In mzscheme, we use the
send form to send a message off:
> (send p say-hello)
hello, my name is mccarthy
And that's our first example.
Let's try another
person% example where people can meet other people.
> (define person%
(class object%
(init-field name)
(define friends '())
(super-new)
(define/public (add-friend other)
(set! friends (cons other friends)))
(define/public (meet-all)
(for-each
(lambda (f) (printf "hi ~a~%" (get-field name f)))
friends))))
Like the first example, we define each person to have a name, but we
also give them a list of friends. And because these people didn't
live through the liberating sixties, a person's list of friend's is a
bit closed and private.
Let's make a few people, and have a social gathering.
> (define danny (new person% (name "danny")))
> (define andy (new person% (name "andy")))
> (define jerry (new person% (name "jerry")))
>
> (send danny add-friend andy)
> (send danny add-friend jerry)
> (send andy add-friend jerry)
> (send jerry add-friend andy)
>
> (send danny meet-all)
hi jerry
hi andy
> (send andy meet-all)
hi jerry
> (send jerry meet-all)
hi andy
One thing to notice that
get-field lets us peek into the public
fields of a class:
> (get-field name danny)
"danny"
But because
friends isn't defined to be a public field, we can't just dig into a person's mind and violate their privacy.
> (get-field friends danny)
get-field: expected an object that has a field named friends, got #<struct:object:person%>
We can get a little creepy, though, and can make it so that this is open:
> (define person%
(class object%
(init-field name)
(field (friends '()))
...))
where
friends is now a public field.
Following on that idea, if we are used to enforcing object
encapsulation, having all this openness is a bit disconcerting. If
we're really paranoid, we can go to an extreme and do something like
this:
> (define (make-person name)
(let ((person%
(class object%
(super-new)
(define/public (say-hello)
(printf "hi, my name is ~a~%" name)))))
(new person%)))
>
> (define n6 (make-person "john drake"))
>
> (send n6 say-hello)
hi, my name is john drake
> (get-field name n6)
get-field: expected an object that has a field named name, got #<struct:object:person%>
And now we have something that simulates really strict privacy.
But a more idiomatic way of doing this uses an intialization parameter that then serves as the base of a private field.
(define person%
(class object%
(init name)
(define -name name)
(super-new)
(define/public (say-hello)
(printf "hi, my name is ~a~%" -name))))
in which case we only use
name here as the public-facing interface to an instance's instantiation. Internally, we stick with
-name. This uses the
init form to break things down to this level of granularity.
Let's look at a
person% definition that has the features we've talked about so far:
(define person%
(class object%
(init-field name)
(define friends '())
(super-new)
(define/public (add-friend other)
(set! friends (cons other friends)))
(define/public (say-hello)
(printf "hello, my name is ~a. It's working, it's working!~%" name))
(define/public (meet-all)
(for-each
(lambda (f)
(printf "hi ~a. join me and I will complete your training~%"
(get-field name f)))
friends))))
But isn't it peculiar that everyone answers the same way? One
limitation of
person% is that a person will say-hello with the same
generic phrase, and that a
person% will meet-all in the same way.
That is what a class is all about --- to define a regular behavior ---
but sometimes, we'd like to extend that behavior.
Let's add some diversity. Let's say that we'd like to create another
kind of person that behaves the same as the
person%, well, except that
their lingo is slightly different. Let's see what that might look
like:
(define valley-person%
(class person%
(inherit-field name)
(super-new)
(define/override (say-hello)
(printf "like, so totally, hello. I'm ~a. Duh!" name))))
Here we have a new class called
valley-person% that extends our
person% class. By extension, we mean that it does everything a
person% would do, except in the cases that we explicitely override.
> (define leia (new valley-person% [name "Leia"]))
> (send leia say-hello)
like, so totally, hello. I'm Leia. Duh!
In contrast to Java, we have to be a little more explicit when we
relate to our superclass. In particular, any superclass fields that
we'd like to access from our subclass are the ones that we'll name
using
inherit-field. Furthermore, any functions we'd like to override
will be marked by our use of the
define/override form.
Let's bring someone else into the party.
> (define leia (new valley-person% [name "Leia"]))
> (define luke (new valley-person% [name "Luke"]))
> (send leia add-friend luke)
> (send leia meet-all)
hi Luke. join me and I will complete your training
> (send luke meet-all)
>
Hmmm.. luke is not quite as warm to leia as leia is. Our version of
valley-person% is not gregarious in the sense that
add-friend is a bit
asymmetric. Let's fix that.
(define valley-person%
(class person%
(inherit-field name)
(super-new)
(define/public (do-lunch other)
(send this add-friend other)
(send other add-friend this))
(define/override (say-hello)
(printf "like, so totally, hello. I'm ~a. Duh!" name))))
Now our
valley-person% can
do-lunch with another person.
> (define leia (new valley-person% [name "Leia"]))
> (define luke (new valley-person% [name "Luke"]))
> (send leia do-lunch luke)
> (send leia meet-all)
hi Luke. join me and I will complete your training
> (send luke meet-all)
hi Leia. join me and I will complete your training
Gnarly. These people do have a particularly dark-sided way of meeting
each other. Sins of the father superclass, I suppose.
One thing to note is that when leia does lunch with luke, she *send*s
herself an
add-friend message, and also gets luke to add herself as
a friend. The identifier
this is there so that leia can send
messages to herself.
Another thing to realize is that this message passing is all being
done at run-time: we could just as easily ask leia to try dancing,
with an error message showing up only as the point where we call
send:
> (define (dance)
(let ([leia (new valley-person% [name "Leia"])])
(send leia dance)))
> (dance)
send: no such method: dance for class: valley-person%
An alternative approach to the above class definition looks like this,
with one less
this:
(define valley-person%
(class person%
(inherit-field name)
(inherit add-friend)
(super-new)
(define/public (do-lunch other)
(add-friend other)
(send other add-friend this))
(define/override (say-hello)
(printf "like, so totally, hello. I'm ~a. Duh!" name))))
The difference here is that our first call to
add-friend explicitly
reuses the add-friend method by inheritence. Rather than use
send
to trigger an external message to ourselves, we're directly calling
our inherited method.
Why would we want to call inherited --- actually, any! --- methods this way, if we already
have
send? One advantage of doing it this way is that
valley-person% can check at class-creation time that its superclass
does have an
add-friend method, so that we can do earlier error
trapping. That is, although:
> (define broken-person%
(class object%
(super-new)))
> (define broken-valley-person
(class broken-person%
(super-new)
(define/public (do-lunch other)
(send this add-friend other)
(send other add-friend this))))
will compile fine and error out when we call do-lunch, the alternative
will break as soon as we try defining the class:
> (define broken-person%
(class object%
(super-new)))
> (define broken-valley-person
(class broken-person%
(super-new)
(inherit add-friend)
(define/public (do-lunch other)
(add-friend other)
(send other add-friend this))))
class*: superclass does not provide an expected method for inherit: add-friend for class: broken-valley-person
Early error messages are nicer than late ones. By using the second
form ---- by using
inherit to access methods on
this, we can add an
additional level of checking to catch typos as early as we can.
inherit obligates the superclass to have the method we'd like to
inherit.
(In general, we should consider
send to be the message
passing form we use to send external messages to other objects.)
This might seem a little weird to Java people: isn't it just
blindingly obvious by looking at the superclass what methods belong in
there? A class must have a fixed superclass, right? It's right in
the class definition! In Java, this would be true. But things can be
different in mzscheme, and that's what the next section is about.
We're going to mix things up by changing our domain from people to
containers. One common thing that people might like to do is "fold"
across lists or vectors. Let's write this:
(require (lib "class.ss"))
(require (lib "list.ss"))
(define list%
(class object%
(init-field data)
(super-new)
(define/public (fold f acc)
(foldl f acc data))))
(define vector%
(class object%
(init-field data)
(super-new)
(define/public (fold f acc)
(let ([N (vector-length data)])
(let loop ([i 0]
[acc acc])
(cond
[(= i N) acc]
[else (loop (add1 i) (f (vector-ref data i) acc))]))))))
And let's try this out.
> (define my-list (new list% [data '(3 1 4 1 5 9 2 6)]))
> (define my-vec (new vector% [data #(3 1 4 1 5 9 2 6)]))
>
> (send my-list fold cons empty)
(6 2 9 5 1 4 1 3)
> (send my-vec fold cons empty)
(6 2 9 5 1 4 1 3)
Ok, looks good so far. With this, we might later want to reusing
these classes, but with some additional functionality. For example,
what if we'd like to have
for-each?
One's immediate approach might be to make a superclass that holds the
common functionality. But let's make ourselves an artificial
restriction, just for the sake of playing things out: let's say that
we restrict ourselves from touching
list% or *vector%*'s definition.
What then?
If we tie ourselves to this, then one approach might be to subclass:
(define list2%
(class list%
(super-new)
(define/public (for-each f)
(send this fold (lambda (x _) (f x)) (void)))))
(define vector2%
(class vector%
(super-new)
(define/public (for-each f)
(send this fold (lambda (x _) (f x)) (void)))))
But this feels a little foolish. Don't we hate code duplication?
Of course there's another approach. Let's look at it:
(define (foreach-mixin class%)
(class class%
(super-new)
(define/public (for-each f)
(send this fold (lambda (x _) (f x)) (void)))))
(define list2% (foreach-mixin list%))
(define vector2% (foreach-mixin vector%))
This is something very new if we're coming from Java, and probably
very surprising! We're taking in a superclass, and "mixing in" a few
more methods to create a new class. Now we don't have to repeat
ourselves, and things still work out:
> (define my-vec (new vector2% [data #(3 1 4)]))
> (send my-vec fold cons empty)
(4 1 3)
> (send my-vec for-each (lambda (x) (printf "~a~n" x)))
3
1
4
The real kicker here is that
foreach-mixin can take in anything that
implements a
fold, and spit out a new class that also implements a
for-each method.
Of course, the mixin depends on
fold: it would also be silly to
apply this on people, but if we try it out:
> (define valley-person2% (foreach-mixin valley-person%))
>
... we don't get an error! It just means we have a
valley-person2%
that is bogus. Can we do better?
Let's fix this and restrict the mixing to superclasses that provide
fold, by using the
inherit form again from the previous section:
(define (foreach-mixin class%)
(class class%
(super-new)
(inherit fold)
(define/public (for-each f)
(fold (lambda (x _) (f x)) (void)))))
With this version, we'll catch errors a little more quickly:
> (define valley-person2% (foreach-mixin valley-person%))
class*: superclass does not provide an expected method for inherit: fold for class: foreach-mixin
Mixins provide us a robust mechanism for adding functionality in a
general way, and they're used quite a bit in
DrScheme's internals.
We can do even better if we use interfaces, which haven't been covered yet. [fixme: but they should!]
There's much more to the class system than what's covered here. The
paper below is an excellent guide to the features that set (lib
"class.ss") far apart: D. S. Goldberg, R. B. Findler, and M. Flatt.
Super and Inner --- Together at Last!
(
http://library.readscheme.org/page4.html)
More recently,
Scheme with Classes, Mixins, and Traits provides an excellent overview of the system.
Also, the Schematics Cookbook has notes on how to do OOP programming
for Scheme in general. See
IdiomObjectOrientedProgramming for more details.
There's a brief example of mixins in:
Modular Object-Oriented Programming with Units and Mixins
http://www.cs.utah.edu/plt/publications/icfp98-ff/
The paper Classes and Mixins give further examples of mixin-based programming
Classes and Mixins
http://www.cs.brown.edu/~sk/Publications/Papers/Published/fkf-classes-mixins/
I wonder if anyone has figured out how to do default arguments yet? I can't figure it out from the official docs.
--
HaraldKorneliussen - 11 Aug 2006
Done; added a small recipe about this here:
DefaultArgumentsClassInitialization
--
DannyYoo - 4 Sep 2006
''Singleton Design Pattern using class.ss,''
Matthias was kind enough to reply to my query to the list with the following:
>
One of the things you can do is instantiate a class once and export
only that instance:
(module singleton mzscheme
(require (lib "class.ss"))
(define one%
(class object%
(super-new)
(define/public (hello) one))
(define one (new one%))
(provide one))
>
Inside the class you never use new; instead you refer to this
instance.
--
StephenDeGabrielle - 12 Dec 2007
--
DannyYoo - 19 Apr 2006