Mizzen details
From HalyardWiki
This tutorial describes Mizzen, the Halyard object model, from the bottom up. This will generally only be of interest to advanced Scheme programmers and language hackers. If you'd prefer a top-down introduction, please see Mizzen tutorial or Halyard tutorial.
You might find it convenient to run these snippets in the Halyard listener. Launch Halyard Test, and type Control-L to bring up the listener.
If you'd like to just take a look at the source code for the object model, you can take a look at ruby-objects.ss. Note that the code looks a little funny in places because all of our syntactic hacks have not been set up at this level.
Contents |
[edit] Classes
You can define a new class using define-class. Currently, there's no "class lambda" form which allows you to construct classes on the fly, though we are considering this.
(define-class %foo% (%object%))
This code defines a class %foo% which inherits from %object%.
[edit] Calling methods with send
You can call a method on an instance using send. Since classes are also objects (of class %class%), you can send messages to classes:
(send %foo% 'name) # Returns the symbol %foo% (send %foo% 'superclass) # Returns the value %object%
[edit] Creating an instance
To create a new instance of a class, call the method new:
(define obj (send %foo% 'new)) # Creates a new instance of %foo%. (send obj 'to-string) # Returns a string representation of obj.
[edit] Methods
A method is a lambda with implicit bindings for self and super. You can create an anonymous method using the method special form:
(method (x) ;; 'self' is bound in this scope. (* x x))
[edit] instance-exec and with-instance
To call a method directly, you can use instance-exec:
(instance-exec myobj (method (x) (* x x)) 2) # Returns 4.
We also provide a with-instance macro:
(define-syntax with-instance
(syntax-rules ()
[(_ object . body)
(instance-exec object (method () . body))]))
[edit] Defining methods on a class
If we want to attach a method to a class, we can send define-method:
(send %foo% 'define-method 'square
(method (x) (* x x))
(define foo (send %foo% 'new))
(send foo 'square 2)
# Returns 4.
We can also do the same thing from directly within %foo% itself:
(define-class %foo% (%object%)
(send self 'define-method 'square
(method (x) (* x x))))
Note that self is bound to the class being defined (in this case %foo%) within the class definition. This idea is borrowed from Ruby, and is the core of our object model.
We can further abbreviate the above definition using the def special form:
(define-class %foo% (%object%)
(def (square x)
(* x x)))
When we use def, it picks up self from the surrounding lexical scope, and calls define-method.
If we had already defined %foo% elsewhere, we could always "reopen" the definition of %foo% using with-instance:
(with-instance %foo%
(def (square x)
(* x x)))
[edit] Disturbing syntactic sugar for send
Instead of writing:
(send obj 'square 2)
...you may write:
(obj .square 2)
This is implemented using PLT's #%app transformer. Yes, it's a little disturbing, but if you can think of a better syntax, please feel free to suggest it.
A further (dubious) abbreviation is also possible. If we want to call a method on self, we can omit the actual object:
(.square 2) # Equivalent to (self .square 2)
[edit] Overriding methods
Subclasses may override methods defined in their superclass. The overridden method may be called using super.
(define-class %greeter% ()
(def (greet)
"Hello"))
(define-class %fancy-greeter% (%greeter%)
(def (greet)
(cat (super) ", world!")))
(define fancy (%fancy-greeter% .new))
(fancy .greet)
# Returns "Hello, world!"
Note that super takes no arguments. Instead, it passes the original function arguments directly to the overridden method. Ultimately, we would like to make the behavior optional.
[edit] Slots and attributes
Each object has an associated hash table. The entries in this table are referred to as slots. Slots are always private. If you want to access them from outside the object, you must define getter and setter methods:
(define-class %car% ()
(def (initialize &rest keys)
(super)
(set! (slot 'model-year) 1999))
(def (model-year)
(slot 'model-year))
(def (set-model-year! value)
(set! (slot 'model-year) value))
)
Here, we override initialize to set the initial value of our slot.
[edit] Defining attributes automatically
An attribute is a slot, plus a default value, a getter method, a setter method, and a few other things. We can define an attribute using attr:
(%car% .attr 'model-year :default (method () 1999))
There is, of course, a helper macro:
(define-class %car% () (attr model-year 1999)))
[edit] The internals of attr
Internally, attr calls a variety of helper methods. All these helper methods are public, so it's possible to write your own custom version of attr.
(def (attr name &key default (writable? #f) (mandatory? #t)
(type #f) (getter? #t) (setter? #t))
(when default
(.attr-initializer name default #t))
(when mandatory?
(.mandatory-attr name))
(when getter?
(check-method-not-defined self name)
(.attr-getter name :default default)
(.seal-method! name))
(when setter?
(let [[setter-name (symcat "set-" name "!")]]
(check-method-not-defined self setter-name)
(.attr-setter name :writable? writable? :type type)
(.seal-method! setter-name))))
The actual getter and setters are defined using define-method.
(def (attr-getter name &key default)
(.define-method name (method () (slot name))))
(def (attr-setter name &key (writable? #f) (type #f))
(.define-method (symcat "set-" name "!")
(method (val)
(check-setter-writability self name writable?)
(check-setter-type self name type val)
(set! (slot name) val))))
[edit] Attribute initialization protocol
The default implementation of initialize will handle most object initialization for you. In general, you will only rarely need to override initialize.
The default initialization protocol is based on attr-initializer. For details, see the source code--this isn't fully documented yet.
At a user level, there are two handy helper macros, value and default:
(define-class %car% () (attr doors) (attr seats)) (define-class %coupe% (%car%) ;; Fix the value of 'doors', and prevent subclasses from ;; overriding it. (value doors 2) ;; Specify a default number of seats. This can be ;; overridden using a keyword argument. (default seats 2)) ;; A car with 4 doors and 5 seats. (%car% .new :doors 4 :seats 5) ;; A coupe with 2 doors and 2 seats. (%coupe% .new) ;; A coupe with 2 doors and 4 seats. (%coupe% .new :seats 4)
[edit] Metaclasses
All classes in our system are themselves objects, and since every object is an instance of a class, every class itself is an instance of a class. You can define methods on a class's class (also known as its metaclass), by re-opening it using with-instance. This can be useful for declaring new types of attribute or method declarations for that class and subclasses, or just for having regular per-class methods and attributes.
(define-class %car% ()
(attr doors)
(attr seats)
(with-instance (.class)
(attr type-name "car"))
(def (as-string)
(cat "A " ((.class) .type-name) " with " (.doors) " doors and " (.seats) " seats.")))
(define-class %coupe% (%car%)
(with-instance (.class)
(default type-name "coupe"))
(value doors 2)
(default seats 2))
(define sporty (%coupe% .new))
(sporty .as-string)
;;=> "A coupe with 2 doors and 2 seats."
Metaclasses really come into their own when they are used to define new types of methods or attributes. For instance, this example defines a simple unit test system, that includes a special test method declaration form that allows you to declare a test method along with a description of what you are testing.
(define (report-failure test exn)
(display (cat "FAILED: " (test .description) "\n" (exn-message exn))))
(define-class %test% ()
(attr description)
(attr method))
(define-class %test-case% ()
(with-instance (.class)
(attr tests '() :writable? #t)
(def (define-test description meth)
(set! (.tests) (cons (%test% .new :description description
:method meth)
(.tests))))
(def (run-tests)
(foreach [test (.tests)]
(with-handlers [[exn:fail?
(lambda (exn)
(report-failure test exn))]]
(define test-case (.new))
(test-case .setup)
(instance-exec test-case (test .method))
(test-case .teardown)))))
(def (setup)
(void))
(def (teardown)
(void)))
(define-syntax test
(syntax-rules ()
[[_ description . body]
(.define-test description (method () . body)))]))
(define *foo* 0)
(define-class %test-global% (%test-case%)
(def (setup)
(set! *foo* 0))
(test "Make sure that foo is setup correctly."
(assert (= 0 *foo*))
(set! *foo* 10))
(test "This test should fail."
(assert (= 10 *foo*)))
(def (teardown)
(set! *foo* 20)))


