While wrapping the IUP library (a cross-platform GUI library) for Chez scheme, I wondered if it was worth making the bindings more like the official IUPLua bindings rather than mapping so closely to the native C API. For instance in C, creating a button and assigning an action looks like:
Ihandle* myButton = IupButton("OK", NULL);
IupSetCallback(myButton, "ACTION", (Icallback)myButton_action);
Whereas in Lua, simply setting a button's action property does the same:
myButton = iup.button{ title: "OK" }
myButton.action = myButton_action
And, for good measure, in Scheme:
(define my-button (iup-button "OK"))
(iup-set-callback my-button "ACTION" my-button-action)
Despite the line count being the same, it's easy to appreciate the terseness of Lua's bindings. The key difference of course is that Lua's bindings are working within an object oriented programming model, much unlike the C and Scheme bindings.
Exploring Object Oriented Programming in Scheme
I've never done any object oriented programming in Scheme, (unless you count using records) so the first step was research. Very quickly it became apparent that CLOS, or more specifically tinyCLOS, was very popular in this regard. tinyCLOS is an simple implementation of the Common Lisp Object System for scheme.
Support for tinyCLOS and CLOS-likes is widespread amongst implementations, including:
GOOPS for Guile COOPS for Chicken Bigloo has a Meroon-like implementation Sagittarius features a partial CLOS implementation Gauche implements an STKlos-like implementation The list goes on... However, absent in the list was Chez. On the one hand, that isn't terribly remarkable, other popular implementations also stuck to the spec and let object systems be handled by external libraries. Meanwhile, some others schemes eschew CLOS-like systems in favor of other flavors, like Racket's smalltalk-like classes. What did catch my attention was when @erkin on the Scheme discord brought to my attention SOOP, introducing it as a Simula-style object system for Chez.
No SOOP for you.
Soop is the object system library that lives in the mats
folder of Chez's repo. It is mostly undocumented and beyond that, I couldn't find a single paper, post, or article about it. (If any exist, please get in touch!) Yet here its is, on every machine that downloads Chez. It features a list of 'TODO' items (insofar as improvements, not incompleteness) but hasn't been touched in a decade... Maybe that's an indication not to proceed, there are certainly a number of other object libraries for Chez I could have gone with, but, and as is the trend of this blog, curiosity prevailed.
Okay, some SOOP for you.
The source for it, and the tests, serve as the only extant documentation of the system, primarily the following comments in oop.ss
(feel free to skip ahead and come back to this as reference afterwards):
#|
define-class:
definition -> (define-class (class-name class-formal*)
(base-name base-actual*)
clause*)
clause -> (implements interface*)
| (ivars ivar*)
| (init init-expr*)
| (methods method*)
| (constructor id)
| (predicate id)
| (prefix string)
ivar -> (modifier* ivar-name ivar-expression)
modifier -> mutability | visibility
mutability -> mutable | immutable
visibility -> public | private
method -> (method-name formals method-body+)
formals -> (var*) | var | (var+ . var)
notes:
- at most one of each kind of clause may be present
- at most one of each kind of modifier may be present
- multiple methods of the same name but different formals can be present
products:
- class-name is bound to class information in the expand-time environment
- make-class-name (or specified constructor name) is bound to creation procedure
- class-name? (or specified predicate name) is bound to predicate procedure
- new (not inherited) method names are bound to method-dispatch procedures
- for each public ivar, <prefix>-ivar is bound to an accessor procedure,
where <prefix> is the specified prefix or "class-name-"
- for each public, mutable ivar, <prefix>-ivar-set! is bound to a mutator
procedure, where <prefix> is the specified prefix or "class-name-"
define-interface:
definition -> (define-interface interface-name method*)
| (define-interface interface-name base-name method*)
method -> (method-name formals)
products:
- interface-name is bound to interface information in the expand-time environment
- new (not inherited) method names are bound to method-dispatch procedures
|#
Let's dive in and examine this system.
Creating Classes
Let's start by defining a simple class:
(define-class (<chess-piece> a1 a2) (<root>)
(ivars [x a1]
[y a2]))
Starting at the top, the <name>
-style class-name is a common scheme convention for naming classes. The name is followed by the arguments needed to create an instance of the class. In this case, I've named them to correspond with arguments + index, a1
and a2
, respectively. This is in order to better show the flow and bindings of defining a class. In reality, x
and y
would probably make better argument labels.
This is followed by a parent object from which to inherit from. This is the single-hierarchy model at the core of the object system. Here we used <root>
, which the library itself provides.
Next, we declare the instance-variables, or ivars
. In this case we want each instance to have an x
and y
property and we're given those as arguments when creating an instance. The bracket convention hints to ivars
purpose as "bindings", binding each property-name to an incoming argument in this case.
Creating Instances
The definition of the <chess-piece>
class has resulted in the expansion of the function make-<class-name>
into the current scope. New classes are created by calling the generated function and passing in initial arguments.
(define my-chess-piece (make-<chess-piece> 0 0))
The only problem is that currently, there's nothing that can be done with that class. It can't be accessed. That's where the ivar modifiers
come into use. Let's edit our class definition above.
(define-class (<chess-piece> x y) (<root>)
(ivars [public mutable x x]
[public mutable y y]))
These changes, the addition of the ivar modifiers
, have resulted in the expansion of several more functions: <class-name>-ivar
and <class-name>-ivar-set!
.
So retrieving a chess-piece's x property would be:
(<chess-piece>-x my-chess-piece) ;; -> 0
And setting it's value would be:
(<chess-piece>-x-set! my-chess-piece 10)
ivars
are private by default, but can be explicitly set to private via the private
keyword. Private ivars
do not generate getters or setters, and immutable ivars
do not generate setters. Likewise, as we did here, 'mutability' can be explicitly set despite being the default behavior.
More on ivars
and Instantiating Objects
Calculated Variables
Instance variables can also be calculated. Let's take a look at the following definition:
(define-class (<rectangle> x y l w) (<root>)
(ivars [public x x]
[public y y]
[public length l]
[public width w]
[public area (* length width)]))
Besides area being a calculated property, it's also worth noting that ivars can reference previous ivars, meaning length and width are available to area, not just the arguments, l and w.
The init
Clause
In addition to handling the initialization of the object within the ivar
bindings, an init
clause is also available. The previous class definition could similarly be expressed as:
(define-class (<rectangle> x y l w) (<root>)
(ivars [public x x]
[public y y]
[public length l]
[public width w]
[public area 0])
(init
(set! area (* length width))))
Noteworthy that only set! is needed in the init
clause to modify the ivar
.
Inheriting From a New Base Class
Lets split out the x and y position as a <point>
class and build our <rectangle>
class on top of that.
(define-class (<point> x y) (<root>)
(ivars
[public x x]
[public y y]))
(define-class (<rectangle> x y l w) (<point> x y)
(ivars
[public length l]
[public width w]
[public area (* length width)]))
After defining my-rect
, calls to (<point>-x my-rect)
would return the bound ivar
. It is worth pointing out that inherit classes may have differing arity from their base class.
Modifying the generated names
There are several ways in which the auto-generated names may be manipulated, the constructor
clause allows the usual make-<class-name>
function to be an id of your choosing. While the prefix
clause will change the generated getter and setter functions to derive from a prefix besides the default of <class-name>
.
For example:
(define-class (<point> x y) (<root>)
(ivars
[public x x]
[public y y])
(predicate is-<p>?)
(prefix "<p>-")
(constructor make-<p>))
Would result in (make-<p> x y)
being the constructor and <p>-x
being the getter for x
on <point>
's. And although we haven't used it, SOOP has also been generating a default predicate
clause, so while generally <point>?
would be created, in the above example (is-<p>? ...)
would be used to determine if an instance is of that class.
Methods
A major aspect of objects in SOOP we are still to cover is method creation and usage. So far Chez SOOP has proven itself to be rather flexible, and methods are no different. Let's define a simple object with a method:
(define-class (<greeter>) (<root>)
(methods [greet () "Hi!"]))
Again, the square brackets help hint at how the methods clause is behaving, binding a function to name. However, using methods doesn't rely on generated method names, the bound name serving as the method-caller itself:
(define my-greeter (make-<greeter>))
(greet my-greeter) ;; -> "Hi!"
Let's add another method to our class:
(define-class (<greeter>) (<root>)
(methods
[greet () "Hi!"]
[greet (name) (string-append "Hello " name)]))
I tried being a little sneaky here, we've re-used the method name greet
, and as you can see, supporting overloaded methods in SOOP is very straightforward:
(define my-greeter (make-<greeter>))
(greet my-greeter) ;; -> "Hi"
(greet my-greeter "Jules") ;; -> "Hello Jules"
Super Salad
Methods that shadow their inherited methods still have access to the original method using super
.
(define-class (<greeter>) (<root>)
(methods [greet (name) (string-append "Hello " name)]))
(define-class (<formal-greeter>) (<greeter>)
(methods [greet (name) (super (string-append "Mr. " name))]))
(define my-formal-greeter (make-<formal-greeter>))
(greet my-formal-greeter "Jules") ;; -> Hello Mr. Jules
Methods down the inherited chain of classes must have matching arity or they will raise an exception. This is hardly a limitation as the usual syntax of (car . rest)
for passing arguments to lambdas is perfectly valid.
Implementing Interfaces
The SOOP library also export's a function, define-interface
. An interface simply describes the formals of a method. Let's dig in by elaborating on the earlier <point>
example.
(define-interface 2d-interface [move-to (x y)])
(define-class (<point> x y) (<root>)
(implements 2d-interface)
(ivars
[public x x]
[public y y])
(methods [move-to (new-x new-y) (begin (set! x new-x)(set! y new-y))]))
First an interface, 2d-interface
, is defined. It contains, (and again the square bracket convention hints at it) a binding of symbols to argument signatures. In the above example, it let's us know that if a method implements move-to
, that it should accept two arguments. We then define the actual class. This time, however, we add the implements
clause and pass into it the interfaces we want to implement; in this case, just the one. Then, in our methods clause, a method that shares the same name as a corresponding symbol in the interface bind, is held to that interface's formals. Here, move-to
matches the arity of the interface it implements, so no exception is raised.
Back to IUP
Despite this side-track into object-systems, the initial seed for all of this was my curiosity in regards to wrapping IUP. Anti-climatically, I decided to keep the API free of an object-system and more like the C API. In it, I'm merely addressing some concepts that feel out-of-place or cumbersome in scheme. The way I saw it was that I wouldn't like a user to be forced to use an object system to interact with their UI layer that may be at odds with one selected for their own application. I would rather have a clean base API upon which object-system specific wrappers could be implemented if needed by library consumers.
I did create a fork of SOOP here: https://github.com/vidjuheffex/chez-soop for use with akku, the package itself is here: https://akkuscm.org/packages/chez-soop/