Today we'll implement/tour the simple class system that comes with Gazelle. This makes a good example to work with because it is simple but involves modules and macro programming, and it is, in fact, generally useful.
The implementation is in the hooves/class-utils module. If you want
to follow along, setup your Gazelle implementation as described in
a previous post. You can just look at the code that is in the
repository at "scripts/hooves/class-utils.gazelle", or you can create
your own version of the file and put it someplace else in the
"scripts" hierarchy.
An Empty Module
We begin with an empty module:
(module (("hooves/hooves" :all)) )
Where we are importing all identifiers from the "hooves/hooves" module. Starting with the latest version of Gazelle, this module includes function definitions for all the javascript operators. It is somewhat expensive, in terms of compilation, to include a module, and you almost always want to use the operator functions and "hooves", which contains more basic utils, that I felt it was a good idea to just combine them.
Our class system is going to borrow liberally from that described by Kevin Lindsey here. The internet, as a collective organism, seems to think that Kevin's implementation is more or less the right one. So the first thing we can do is give credit where it is due, by adding the following to the empty module:
(module (("hooves/hooves" :all)) (comment "A simple class system based on Kevin Lindsey's code:" "http://www.kevlindev.com/tutorials/javascript/inheritance/"))
The comment form will render its string values into Javascript comments, in case anyone looks at the js code directly.
Now, we could use Gazelle's include-js form to include Kevin's
Javascript directly. We'd need to put it in a file next to the
Gazelle code, and refer to it there, but let's port the implementation
of the basic extend operation to Gazelle itself.
Extend
(module (("hooves/hooves" :all)) (comment "A simple class system based on Kevin Lindsey's code:" "http://www.kevlindev.com/tutorials/javascript/inheritance/")) (define+ (extend sub-class base-class) (comment "extend sub-class base-class: " "Where both sub-class and base-class are constructors, extend" "contrives that sub-class will be a sub class of base-class, " "able to access its methods and values.") (var inheritance (lambda () undefined)) (set! inheritance.prototype base-class.prototype) (set! sub-class.prototype (new inheritance)) (set! sub-class.prototype.constructor sub-class) (set! sub-class.super-constructor base-class) (set! sub-class.super-class base-class.prototype))
Briefly, we are creating a stub-constructor in the variable
inheritance, which we use create a clean prototype and constructor for
instances of that class. We then ensure that the inheritance chain is
set up appropriate for the stub. If you are interested in all the
sordid details of why such an approach is needed in Javascript, read
Kevin's tutorial. Javascript gives me a headache!
Recall that `define+` defines an external function to the module. We could basically stop here, since this basic solution gives you most of what you want. You can say things like:
(require (("hooves/hooves" :all) ("hooves/class-utils" :all) ("jquery/jquery" (:as $))) (console.log "Testing class system.") (define (Person first-name last-name) (set! this.first-name first-name) (set! this.last-name last-name)) (define (Employee first-name last-name company) (Employee.super-constructor.call this first-name last-name) (set! this.company company)) (extend Person Employee) (var emp (new Employee "Jane" "Doe" "IBM")) (console.log (+ "instanceof emp Person: ") (instanceof emp Person)) (console.log (+ "instanceof emp Employee: ") (instanceof emp Employee)))
And you will indeed see that emp is an instance of both Person and
Employee.
Riding the Gazelle
If this were Javascript, we'd pretty much be done. We could write a
few functions to shuffle things around, but we'd basically be stuck
with a few ugly things leftover. For instance, it sort of sucks that
we have to invoke the super-constructor with call, and that we have to
refer to the employee super-class directly, given that we are in the
constructor for Employee and so it is implicit what super class we
want.
Gazelle is lispy enough that we can "fix" this with a macro. What we'd like to do is give a single form which let's us evaluate the code describing the constructor of a new class in a context where some conveniences are available. The macro will need to introduce some bindings before the body of the new constructor is run.
Consider the following implementation:
(define-macro+ define-class ((! (non-kw-symbol class-name))
super-class
(tail constructor-lambda-forms))
(let ((impl-name (gensym (symbol-name class-name)))
(cons-args (gensym "cons-args"))
(temp-args (gensym "temp-args"))
(self-holder (gensym "self"))
(super-class-val (gensym "super-class")))
`(_newline-sequence
(var ,impl-name (lambda ,@constructor-lambda-forms))
(var ,super-class-val ,super-class)
(define ,class-name
(lambda ((tail ,cons-args))
(var ,self-holder this)
(set! this.super-constructor
(lambda ((tail ,temp-args))
(.. ,super-class-val (apply ,self-holder ,temp-args))))
(set! this.super (.. ,super-class-val prototype))
(.. ,impl-name (apply this ,cons-args))
this))
(.. (from "hooves/class-utils" extend)
(call null ,class-name ,super-class-val)))))
We define an external macro called define-class, which inserts a
series of expressions separate by newlines into the compiled code.
The first two are temporary variables which contain the implementation
of the new class's constructor and the super-class, which might, after
all, be an arbitrary expression in the macro expression. We want the
value of that expression to use in the rest of the expansion. We then
use define to create the new class constructor. The constructor is
just a shim: it captures the arguments passed to it, creates a self
binding, and adds a binding to a function to this, under
super-constructor that just passes its arguments to the super class
constructor, with the appropriate this.
We also give this.super a binding to the superclass prototype, so that
methods can easily invoke superclass methods. The only wrinkle here
is that we use (from "hooves/utils" ...) to invoke the function extend
from this module.
For good measure, let's write one more macro that helps us conveniently define sub-class methods.
The idea is similar to that above: we want a form which allows the
user to define a method, and in the body of that method, we want
(super-method arg1 ... argN) to refer to the superclass method of the
same name.
Consider:
(define-macro+ define-method (class method-name (tail lambda-part)) (let ((class-value (gensym "class-value")) (super-method (gensym "super-method")) (args (gensym "args")) (explicit-this (gensym "explicit-this"))) `(_newline-sequence (var ,class-value ,class) (var ,super-method (if (&& (defined? (.. ,class-value super-class)) (defined? (.. ,class-value super-class ,method-name))) (lambda ((tail ,args)) (.. (.. ,class-value super-class ,method-name) (apply this ,args))) (lambda ((tail ,args)) (_throw (+ "No superclass method " ',method-name " in class " ,class-value))))) (set! (.. ,class-value prototype ,method-name) (lambda ((tail ,args)) (var ,explicit-this this) (var super-method (lambda ((tail ,args)) (.. ,super-method (apply ,explicit-this ,args)))) (.. (lambda ,@lambda-part) (apply this ,args)))))))
This macro pre-calculates the super method so that we don't waste time
doing that on each invokation. The implementation throws an error if
no super method exists and one is called, but it might be more
appropriate to look up the method at call time in that case. In any
case, there is a bit of misdirection as we set up the environment
where the lambda will execute, but the upshot is that super-method
invokes the super method.
Testing
We will adapt the main.gazelle that Gazelle ships with, and use the
example.html file to test this code:
(comment "main.gazelle") (require (("hooves/hooves" :all) ("hooves/class-utils" :all) ("jquery/jquery" (:as $))) (console.log "Testing class system.") (define-class Person Object (first-name last-name) (set! this.first-name first-name) (set! this.last-name last-name) this) (define-method Person to-string () (+ "Don't shoot, I'm " this.first-name " " this.last-name "!")) (define-class Employee Person (first-name last-name company) (this.super-constructor first-name last-name) (set! this.company company) this) (define-method Employee to-string () (+ (super-method) " Plus, I work for " this.company ".")) (define (newline) (.. ($ "body") (append ($ "<br>")))) (.. ($ "body") (append "Hello World.")) (newline) (.. ($ "body") (append (+ "" (new Person "James" "Cooper")))) (newline) (var card (new Employee "Orson" "Card" "IBM")) (.. ($ "body") (append (+ "" card))) (newline) (.. ($ "body") (append (+ "instanceof card Person is " (instanceof card Person)))) (newline) (.. ($ "body") (append (+ "instanceof card Employee is " (instanceof card Employee)))))
When you run this, you should see strings appear in the browser window
indicating that our inheritance hierarchy is in place and that we can
invoke super methods by using super-method.
That's all folks!
