(lib "class.ss") Notes ---------------------- [DEPRECATED: this material has moved onto the Schematics Cookbook, under the section: http://schemecookbook.org/Cookbook/IntroductionToMzlibClasses but I'll leave this here for posterity.] ====================================================================== 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. *grin* 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. First steps ----------- 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. Classes are people too ---------------------- 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 { private 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.] 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% # 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. There are other ways of instantiating objects: make-object instantiate 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: p.sayHello(); 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. (when ((new person% (name "harry")) . met . (new person% (name "sally")))) -------------------------------------------------------------------------- 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* let's 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 # 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 # And now we have something that simulates really strict privacy. In the code that I've seen so far, though, I have not seen so much worry about the privacy of initialization fields. [fixme: ask folks on plt-scheme mailing list if this is something that's really worth worrying about.] Inheritance ----------- 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 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. 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. Mixing things up with Mixins ---------------------------- 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: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (module folding mzscheme (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)) 'sentinel)))) (define vector2% (class vector% (super-new) (define/public (for-each f) (send this fold (lambda (x _) (f x)) 'sentinel)))) 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)) 'sentinel)))) (define list2% (foreach-mixin list%)) (define vector2% (foreach-mixin vector%)) This is something very new if you'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 from the previous section: (define (foreach-mixin class%) (class class% (super-new) (inherit fold) (define/public (for-each f) (fold (lambda (x _) (f x)) 'sentinel)))) 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. GUIs ---- The stuff that we've seen so far is fine, but we could do all of this without too much trouble, with the use of structures. If we compare: > (define person% (class object% (init-field name) (define/public (say-hello) (printf "hello, my name is ~a~%" name)) (super-new))) with a straightforward structure approach: > (define-struct person (name)) > (define (say-hello p) (printf "my name is ~a~%" (person-name p))) > > (define-values (l h) (values (make-person "linda") (make-person "hamilton"))) > (say-hello l) my name is linda > (say-hello h) my name is hamilton we can see that for simple programs, we don't need OOP. Things are different when we approach graphical user interfaces. [fill me in --- go through simple gui examples to follow with MrEd intro documentation.] Things I'm intentionally skipping for the moment ------------------------------------------------ Beta-style methods (inner/augment) Interfaces make-object/super-make-object, instantiate/super-instantiate serialization inspectors Other Resources --------------- There's much more to the class system that 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) Also, the Schematics Cookbook has notes on how to do OOP programming for Scheme in general. (http://schemecookbook.org/view/Cookbook/IdiomObjectOrientedProgramming) 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/