s c h e m a t i c s : c o o k b o o k

/ Cookbook.IntroductionToMzlibClasses

This Web


WebHome 
WebChanges 
TOC (with recipes)
NewRecipe 
WebTopicList 
WebStatistics 

Other Webs


Chicken
Cookbook
Erlang
Know
Main
Plugins
Sandbox
Scm
TWiki  

Schematics


Schematics Home
Sourceforge Page
SchemeWiki.org
Original Cookbook
RSS

Scheme Links


Schemers.org
Scheme FAQ
R5RS
SRFIs
Scheme Cross Reference
PLT Scheme SISC
Scheme48 SCM
MIT Scheme scsh
JScheme Kawa
Chicken Guile
Bigloo Tiny
Gambit LispMe
GaucheChez

Lambda the Ultimate
TWiki.org

(lib "class.ss") Notes


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.

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 {
        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:

    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 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.

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 --- 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.

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:

(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!]

Other Resources


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/


Comments about this recipe

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

Contributors

-- DannyYoo - 19 Apr 2006

CookbookForm
TopicType: Recipe
ParentTopic: GettingStartedRecipes
TopicOrder:

 
 
Copyright © 2004 by the contributing authors. All material on the Schematics Cookbook web site is the property of the contributing authors.
The copyright for certain compilations of material taken from this website is held by the SchematicsEditorsGroup - see ContributorAgreement & LGPL.
Other than such compilations, this material can be redistributed and/or modified under the terms of the GNU Lesser General Public License (LGPL), version 2.1, as published by the Free Software Foundation.
Ideas, requests, problems regarding Schematics Cookbook? Send feedback.
/ You are Main.guest