Ciao,
I have implemented a syntactic layer on top of (rnrs
records procedural (6)), which allows the definition of
class types in a library and their use in another library by
importing only the class name and the general OOP macros:
(library (defs)
(export <alpha>)
(import (classes))
(define-class <alpha>
(fields a b c)))
(library (use)
(export)
(import (classes) (defs))
(let ((o (make <alpha> 1 2 3)))
---))
I have taken the idea on how to do it from Sperber's
reference implementation of the syntactic records layer for
R6RS (the class name <alpha> is bound to a syntax, etc).
With this step I think it is possible to write some
OOP-looking stuff; I would be glad if some kind soul can
comment on the syntaxes I am using for my classes library.
In what follows I am using (nausicaa) as language, rather
than (rnrs), to make things simpler to read. (nausicaa)
reexports all the bindings from (rnrs), plus some other
utilities, plus it exports a version of DEFINE, LAMBDA,
CASE-LAMBDA and the LET variants which support class types
specifications; when the LET syntaxes detect that no type is
selected, they default to the LET bindings from (rnrs), so
there should be no performance penalty in untyped code.
I will not illustrate all the features and syntaxes
detail, only a brief introduction and some of the things I
still have to decide.
If someone wants to try the current version:
<http://github.com/downloads/marcomaggi/nausicaa/nausicaa-0.2d2-src.tar.bz2>
it works with Ikarus, Petite Chez and the latest Mosh from
its repository's master branch; Ypsilon support is broken
for the known hygiene problems; Larceny may work, not tested
yet (it takes forever to compile).
Dot notation
------------
The main reason for the existence of the classes library is
to allow dot notation to access fields and methods; the core
syntax is WITH-CLASS:
(import (nausicaa))
(define-class <alpha>
(fields a b c)
(method (d self)
4))
(let ((o (make <alpha>
1 2 3)))
(with-class ((o <alpha>))
(list o.a o.b o.c (o.d))))
=> (1 2 3 4)
the augmented DEFINE, LAMBDA, CASE-LAMBDA and LET variants
all expand to WITH-CLASS forms which in turn expand to
nested LET-SYNTAX forms; the same code with the augmented
LET is:
(import (nausicaa))
(define-class <alpha>
(fields a b c)
(method (d self)
4))
(let (((o <alpha>) (make <alpha>
1 2 3)))
(list o.a o.b o.c (o.d)))
=> (1 2 3 4)
On identifier syntaxes
----------------------
I have decided that I am going to accept whatever
performance penalty results from accessing record fields
through identifier syntaxes.
Classes have virtual fields which result in calls to
functions or expansion of macros; I will put in the
documentation style notes about what to do and what not to
do; these will be:
(1) Identifier reference should have no side effects.
(2) Functions and syntaxes invoked by identifier references
should never intentionally raise exceptions.
is there some other known bad practice?
Common vs knitting inheritance
------------------------------
Class instances are regular records from (rnrs records
procedural (6)) and (classes) uses the single inheritance
implementation of that library; what is open to decide is
how to use dot notation to access the type-specific members.
Everybody is used to "common" inheritance in which
subclass members override superclass ones; this is what
happens when using the simple form of the INHERIT clause:
(import (nausicaa))
(define-class <alpha>
(fields a b z))
(define-class <beta>
(inherit <alpha>)
(fields c d z))
;;accessing fields of both class and superclass
(let (((p <beta>) (make <beta>
1 2 3
4 5 6)))
(list p.a p.b p.c p.d))
=> (1 2 4 5)
;;precedence to subclass fields
(let (((p <beta>) (make <beta>
1 2 3
4 5 6)))
p.z)
=> 6
;;custom precedence of superclass fields
(let (((p <beta> <alpha>) ;the last takes precedence
(make <beta>
1 2 3
4 5 6)))
p.z)
=> 3
;;accessing both fields with the same name
(let* (((p <beta>) (make <beta>
1 2 3
4 5 6))
((q <alpha>) p))
(list q.z p.z))
=> (3 6)
But, through methods and virtual fields it is also
possible to implement "knitting" inheritance: a
do-it-yourself way which I have NOT yet implemented but
would not be difficult to do.
Basically, the INHERIT clause would optionally specify
that dot notation for superclasses must not be used; the
superclass fields are still included in the subclass. The
following shows how to achieve the same result of "common"
inheritance with "knitting" inheritance:
(import (nausicaa))
(define-class <alpha>
(fields a b z)
(methods red))
(define (<alpha>-red (o <alpha>))
(list o.a o.b o.z))
(define-class <beta>
(inherit <alpha>
(dry))
(fields c d z)
(virtual-fields (immutable a <alpha>-a)
(immutable b <alpha>-b))
(methods (red <alpha>-red)))
(let (((o <beta>) (make <beta>
1 2 3
4 5 6)))
(list o.a o.b o.c o.d o.z (o.red)))
=> (1 2 4 5 6 (1 2 3))
Knitting inheritance would allow fine selection of what is
accessible and what is not, plus renaming of fields and
methods. It would not be difficult to add selectors to the
INHERIT clause to let only fields or only virtual fields or
only methods pass through.
Would knitting inheritance be useful or just make a mess
of things?
Methods as macros
-----------------
An in-definition method is automatically expanded to a
function definition:
(define-class <alpha>
(fields a b c)
(method (d (self <alpha>))
(display self.a)))
is equivalent to:
(define-class <alpha>
(fields a b c)
(methods (d automatically-generated-id)))
(define (automatically-generated-id (self <alpha>))
(display self.a))
External methods can be either functions or macros:
(define-class <alpha>
(fields a b c)
(methods (d <alpha>-d)
(e <alpha>-e)))
(define (<alpha>-d self)
---)
(define-syntax <alpha>-e
(syntax-rules ()
((_ ?self)
---)))
It should be possible to have in-definition methods
expanded to syntaxes:
(define-class <alpha>
(fields a b c)
(method d
(syntax-rules ()
((_ ?self)
---)))
(method e
(lambda (stx) ;macro transformer
---)))
but would this syntax be "beautiful" and comprehensible?
TIA
--
Marco Maggi