Opened 12 years ago

Closed 11 years ago

#114 closed task (wontfix)

Improvements to tinyclos

Reported by: Tony Sidaway Owned by: Tony Sidaway
Priority: not urgent at all Milestone:
Component: extensions Version: 4.2.x
Keywords: tinyclos clos Cc:
Estimated difficulty:

Description

Attention should be paid to the following aspects of tinyclos.

  1. Internal documentation. The software can appear intimidatingly complex even to those who have worked extensively on it in the past. Where appropriate, large subsystems should be separated into program units or modules to improve maintainability.
  2. Testing. while unit tests show the software to be basically sound it would benefit from more detailed tests of the interaction of inheritance and polymorphism.
  3. Usability. The programmer interface shows signs of convergence with that of Common Lisp. While this could be useful for software portability and to enable Common Lisp programmers to import their CLOS-programming skills to Scheme, it hampers full exploitation of the programming environment. The interface should be extended so as to enable more natural use in Scheme's Algol-like block structure. The programmer should be encouraged to treat generics as classes, classes as objects, and all of them as Scheme objects subject to Scheme's lexical scoping rules.
  4. Swig support. Tinyclos forms an important part of Swig's support for Chicken Scheme, being essential for wrapping C++ classes. This functionality should be tested and, if necessary, fixed.
  5. Performance. The software should be profiled and improved where possible. Lessons learned from other implementations of tinyclos, such as the hybrid C/Scheme implementation of STKlos, should be applied where appropriate.

Change History (9)

comment:1 in reply to:  description Changed 12 years ago by felix winkelmann

Replying to tonysidaway:

  1. Usability. The programmer interface shows signs of convergence with that of Common Lisp. While this could be useful for software portability and to enable Common Lisp programmers to import their CLOS-programming skills to Scheme, it hampers full exploitation of the programming environment. The interface should be extended so as to enable more natural use in Scheme's Algol-like block structure. The programmer should be encouraged to treat generics as classes, classes as objects, and all of them as Scheme objects subject to Scheme's lexical scoping rules.

I would suggest to add let[rec]-generic and let[rec]-method, for example, but the implementation may be non-trivial.

  1. Swig support. Tinyclos forms an important part of Swig's support for Chicken Scheme, being essential for wrapping C++ classes. This functionality should be tested and, if necessary, fixed.

This also applies to easyffi, which desperately needs a working tinyclos.
I will try to look how well it works now, but that may take some time.

  1. Performance. The software should be profiled and improved where possible. Lessons learned from other implementations of tinyclos, such as the hybrid C/Scheme implementation of STKlos, should be applied where appropriate.

As can be seen from the state of the code, quite some effort has been put into improving performance. What hasn't been tried yet is using some form of inline-caching. Chicken's (undocumented) support for compiler-syntax may also be useful here.

comment:2 Changed 12 years ago by Tony Sidaway

Not sure how let-generic, etc, would work.

You can see what my coding style is like in the file tests/run.scm In testing it's very useful to be able to create and destroy classes and methods on the fly, and I'm using a new define-class* macro that can be used like this:

  (let* ((<pos> (define-class* () (x y)))
        (<circle> (define-class* (<pos>) (radius)))
        (p (make <pos>))
        (c (make <circle>)))
   ...)

It's just syntactically a little more natural than

 (let* ((<pos> (make-class '() '(x y)))
        (<circle> (make-class (list <pos>) '(radius)))

You lose the class name because I don't want to force the programmer to type the name of his symbol twice. Presumably if he becomes attached to the class he can set its class name later.

There is already a (make-generic [NAME]) so that's not an issue. It works well with Scheme.

define-method doesn't actually create a new Scheme binding so I do something like

 (let* ((gen (make-generic))
        (ignore (define-method (gen (b <boolean>)) (if b 1 0))))
 ...)

It looks awkward. You could shunt the define-method into the body of a block but then you're off starting a new block as soon as you want to create objects and manipulate them using your new generic.

define-method actually works by mutating the generic, so by Scheme conventions it could be called "define-method!" or "add-method-to-generic!" instead--not that I'm suggesting a change of name at this stage. Well not seriously.

If you don't have time to look at easyffi I may do so myself. swig is a bit of a monster so for small-scale wraps easyffi is preferable. I didn't even know it had any object support features.

comment:3 in reply to:  2 Changed 12 years ago by felix winkelmann

Replying to tonysidaway:

Not sure how let-generic, etc, would work.

let-generic would bind a local variable to a newly created generic,
let[rec]-method would during the extent of the body add a method for
a specific generic.

You can see what my coding style is like in the file tests/run.scm In testing it's very useful to be able to create and destroy classes and methods on the fly, and I'm using a new define-class* macro that can be used like this:

  (let* ((<pos> (define-class* () (x y)))
        (<circle> (define-class* (<pos>) (radius)))
        (p (make <pos>))
        (c (make <circle>)))
   ...)

It's just syntactically a little more natural than

 (let* ((<pos> (make-class '() '(x y)))
        (<circle> (make-class (list <pos>) '(radius)))

I don't see the improvement, here. The make-class looks clearer
to me.

define-method doesn't actually create a new Scheme binding so I do something like

 (let* ((gen (make-generic))
        (ignore (define-method (gen (b <boolean>)) (if b 1 0))))
 ...)

It looks awkward. You could shunt the define-method into the body of a block but then you're off starting a new block as soon as you want to create objects and manipulate them using your new generic.

I'm not sure I understand. Anything starting with define should not be evaluated for a result, that's something I find most un-Scheme-ly.

define-method actually works by mutating the generic, so by Scheme conventions it could be called "define-method!" or "add-method-to-generic!" instead--not that I'm suggesting a change of name at this stage. Well not seriously.

define-method is not a function, it's a definition, so adding the ! would be non-idiomatic (IMHO).

If you don't have time to look at easyffi I may do so myself. swig is a bit of a monster so for small-scale wraps easyffi is preferable. I didn't even know it had any object support features.

I'll check out easyffi myself. It's quite a mess but crucial and rather complex. Thanks for the offer, though. I have very little time left for chicken hacking in the moment.

comment:4 Changed 12 years ago by Tony Sidaway

I think you're right about my early naive attempts to make tinyclos more Scheme-like. It needs much more thought to get it right. I invite your comments on the following.

At the moment I'm inclined to say we could provide two levels of support.

Firstly define-method could make minimal efforts to support CLOS-like behavior, by defining a generic if necessary, but following the semantics of Scheme's define when it does so. define-method inside a (begin ...) may create a top-level definition bound to the result of a call to define-generic. define-generic inside a block--a lambda or let form--may create a local definition bound to the same result. To ensure reliable top-level semantics, define-generic must be used to create the necessary top-level binding, and this binding must not be lexically shadowed when define-method is used. The effect of ignoring these strictures is not fatal but it results in behavior that may be difficult to explain to people who expect tinyclos to ignore Scheme's strict lexical scope.

Secondly something along the lines of your let-generic, let-method suggestions could be introduced to provide something Scheme-like. I like the idea of a let-class, too. A let-generic...let-class block hierarchy would foster the encapsulation of methods and classes in a way that fits the block structure of Scheme and the principles of object oriented design: the methods and classes can be defined computationally within the block. If top-level bindings are desired, methods for returning values from the block to the top level are built into Scheme.

(let-generic (IDENTIFIER...) BODY)
(let-class ((CLASSIDENTIFIER (DIRECT-SUPER ...) (SLOT ...)) ...) BODY)

let-method is a very different kettle of fish. That would require a way to undefine a method (to dissociate it from its generic). This could probably be done most cleanly by duplicating the generic into another newly-created generic of the same name (and using an identifier bound to the same symbol within the define-method block).

Something like this:

(let-method (((IDENTIFIER SPECIFIER...) METHOD-BODY) ...) BODY)

In the form I suggest above, semantics similar to those I have suggested for define-method would apply. If a generic is already defined with IDENTIFIER, it is cloned into a new generic bound to the same symbol for the scope of the block. If no such generic exists (the symbol IDENTIFIER may not be bound or it may bound to some other object in the enclosing block or at the top level) then a new blank generic is bound to IDENTIFIER for the scope of the block.

The cloning of generics like this is a bit odd, but it could be useful. It's very different from the definition of scope, so perhaps using a let form isn't the best way to do it.

An alternative would be to use fluid-let semantics. In this scenario the let-method form would require a pre-existing generic that would be temporarily altered in such a way that the behavior of any closure over the same generic would be affected. I still think cloning would be involved, though I hope it may be possible to do away with that.

I would suggest that the fluid-let form should be clearly named so as to indicate its underlying semantics.

Thus:

(fluid-let-method (((IDENTIFIER SPECIFIER...) METHOD-BODY) ...) BODY)

comment:5 Changed 12 years ago by Tony Sidaway

Status: newaccepted

comment:6 Changed 12 years ago by Tony Sidaway

Something like this:

(define (make-a-heap-of-useful-classes)
  (let-generic (pos-x pos-y move resize radius width height)
    (let*-class ((<pos> () (x y))
                 (<circle> (<pos>) (radius))
                 (<oblong> (<pos>) (width height)))
      (define-method (move (p <pos>) x y)
        (and
         (or (not (or (not x) (number? x)))
             (not (or (not y) (number? y))))
         (error "move <pos>: args must be #f or numeric"))
       (and x (slot-set! p 'x x))
       (and y (slot-set! p 'y y)))
      (define-method (resize (c <circle>) (r <number>))
        (slot-set! c 'radius r))
      (define-method (resize (o <oblong>) (w <number>) (h <number>))
        (slot-set! o 'w w)(slot-set! o 'h h))
      (define-method (pos-x (p <pos>) (slot-ref p 'x))
      (define-method (pos-y (p <pos>) (slot-ref o 'y))
      (define-method (pos-x-set! (p <pos>) (x <number>))
        (slot-set! p 'x x))
      (define-method (pos-y-set! (p <pos>) (y <number>))
        (slot-set! p 'y y))
      (define-method (radius (c <circle>)) (slot-ref c 'radius))
      (define-method (width (o <oblong>)) (slot-ref o 'width))
      (define-method (width-set! (o <oblong>) (w <number>)
        (slot-set! o 'width w))
      (define-method (height-set! (o <oblong>) (h <number>)
        (slot-set! o 'height h))
      (define-method (height (o <oblong>)) (slot-ref o 'height))
      (values (list <pos> <circle> <rectangle>)
              (list pos-x pos-x-set! pos-y pos-y-set! move
                    resize radius height)))))

Why go through all that? Because it encloses all the crap you have to go through to make those useful classes. The calling procedure doesn't need to know about generics, classes and whatnot, it doesn't even need to know about tinyclos. It only needs to know how to do the usual Scheme things: call a procedure and handle the results. Possibly even a cleverly written macro could receive the results and blend them in;

(define-classes make-a-heap-of-useful-classes)
(let ((p (make <pos>)))
  (move p 10 20)
  (move p #f (+ (pos-y p) 7)))

define-class semantics as used above are a little crude--there's no way to hide the symbolic links to the slots. It would be good to enable hiding of slots so that a truly encapsulated class system could be written. There's nothing to stop the programmer using make-class, of course, and that would make it easy to do that. But there ought to be a simple, unmessy way to do it all. A renaming scheme for slots would do that I suppose.

comment:7 in reply to:  4 Changed 12 years ago by felix winkelmann

Replying to tonysidaway:

Firstly define-method could make minimal efforts to support CLOS-like behavior, by defining a generic if necessary, but following the semantics of Scheme's define when it does so. define-method inside a (begin ...) may create a top-level definition bound to the result of a call to define-generic. define-generic inside a block--a lambda or let form--may create a local definition bound to the same result. To ensure reliable top-level semantics, define-generic must be used to create the necessary top-level binding, and this binding must not be lexically shadowed when define-method is used. The effect of ignoring these strictures is not fatal but it results in behavior that may be difficult to explain to people who expect tinyclos to ignore Scheme's strict lexical scope.

I always found the automatic definition of a generic (in case it doesn't exist yet) somewhat arbitrary (even though it is convenient). The implementation is also crude and needs to test whether the symbol has a toplevel binding or not. Simply requiring the define-generic is in the end more intuitive and simpler to understand and implement.

Secondly something along the lines of your let-generic, let-method suggestions could be introduced to provide something Scheme-like. I like the idea of a let-class, too. A let-generic...let-class block hierarchy would foster the encapsulation of methods and classes in a way that fits the block structure of Scheme and the principles of object oriented design: the methods and classes can be defined computationally within the block. If top-level bindings are desired, methods for returning values from the block to the top level are built into Scheme.

(let-generic (IDENTIFIER...) BODY)
(let-class ((CLASSIDENTIFIER (DIRECT-SUPER ...) (SLOT ...)) ...) BODY)

let-method is a very different kettle of fish. That would require a way to undefine a method (to dissociate it from its generic). This could probably be done most cleanly by duplicating the generic into another newly-created generic of the same name (and using an identifier bound to the same symbol within the define-method block).

Correct. AFAIK, there is no method-removal in the moment, right?

Something like this:

(let-method (((IDENTIFIER SPECIFIER...) METHOD-BODY) ...) BODY)

In the form I suggest above, semantics similar to those I have suggested for define-method would apply. If a generic is already defined with IDENTIFIER, it is cloned into a new generic bound to the same symbol for the scope of the block. If no such generic exists (the symbol IDENTIFIER may not be bound or it may bound to some other object in the enclosing block or at the top level) then a new blank generic is bound to IDENTIFIER for the scope of the block.

Wouldn't that mean we have to figure out whether IDENTIFIER is bound? That can't be done for local bindings, I think.

The cloning of generics like this is a bit odd, but it could be useful. It's very different from the definition of scope, so perhaps using a let form isn't the best way to do it.

The more I think about it, the more I like the simple approach as it is currently implemented.

An alternative would be to use fluid-let semantics. In this scenario the let-method form would require a pre-existing generic that would be temporarily altered in such a way that the behavior of any closure over the same generic would be affected. I still think cloning would be involved, though I hope it may be possible to do away with that.

I would suggest that the fluid-let form should be clearly named so as to indicate its underlying semantics.

Thus:

(fluid-let-method (((IDENTIFIER SPECIFIER...) METHOD-BODY) ...) BODY)

Yes, that is better. Still, it would have to remove the method from the generic after leaving the body.

comment:8 Changed 11 years ago by felix winkelmann

Priority: majornot urgent at all

comment:9 Changed 11 years ago by felix winkelmann

Resolution: wontfix
Status: acceptedclosed

closed, obsolete, forgotten.

Note: See TracTickets for help on using tickets.