A simple minimal OO system for newLISP

Notices and updates
Lutz
Posts: 5289
Joined: Thu Sep 26, 2002 4:45 pm
Location: Pasadena, California
Contact:

A simple minimal OO system for newLISP

Post by Lutz »

(1) An object is represented by a list in which the first member is a symbol identifying the class of which this object is an instance.

(2) Methods are defined in a context with the class name.

Code: Select all

(define (rectangle:area p)
	(mul (p 3) (p 4)))

(define (circle:area c)
	(mul (pow (c 3) 2) (acos 0) 2))

(set 'myrect '(rectangle 5 5 10 20)) ; x y width height
(set 'mycircle '(circle 1 2 10)) ; x y radius
Note, that the methods should be defined first to establish 'rectangle' and 'circle' as context symbols. In a similar fashion 'rectangle:move' and 'circle:move' could be defined.

(3) we define the : (colon) as a function which extracts the class name from the object, constructs the class:method symbol and applies it to the object:

Code: Select all

(define-macro (: _func _obj) 
	(let (_data (eval _obj))
		(apply (sym _func (_data 0)) (list _data))))
now we can code:

Code: Select all

(:area myrect) => 200
(:area mycircle) => 314.1592654
Visually and in an intuitive manner this looks like the type polymorphic application of a generic functor ':area' to an object 'myrect' or 'mycircle', both of different types. But in reality this is the ':' function taking the symbol 'area' as the method name and extracting the correct class from the object. The newLISP parser allows the colon : to be attached directly to the following symbol.

The above macro will be a built-in primitive in the next development version for maximum speed an minimum overhead. The : function solves the polymorphism problem. Objects defined as in rule (1) can be anonymous and can be nested.

Lutz

ps:

- defining the : colon doesn't disturb any existing functionality related to contexts

- subclassing is done using (new 'Class 'Subclass) and adding or overwriting methods in 'Subclass' doing: (define (Subclass:some-method) ...)

- multiple inheritance is possible using repeated 'new' (new 'Aclass 'Subclass), (new 'Bclass 'Subclass). This can add methods from 'Aclass and 'Bclass to an already existing 'Subclass.

- 'def-new' can be used to do mix-ins of single methods.

- a 'def-type' macro could be used to define <class-name>:<class-name> as a default functor to construct objects (<class-name> p1 p2 ...), but isn't really essential although convenient.
Last edited by Lutz on Wed Oct 24, 2007 10:39 am, edited 1 time in total.

m i c h a e l
Posts: 394
Joined: Wed Apr 26, 2006 3:37 am
Location: Oregon, USA
Contact:

Post by m i c h a e l »

Hi Lutz,

Thank you! I know you're not overly fond of OOP, so for you to spend time on it means a lot to me.

What a great idea to use :. I didn't even know you could define it as a function! And you're right, it is intuitive.

m i c h a e l

Jeff
Posts: 604
Joined: Sat Apr 07, 2007 2:23 pm
Location: Ohio
Contact:

Post by Jeff »

Can a Shape type contain multiple Side objects?
Jeff
=====
Old programmers don't die. They just parse on...

Artful code

Lutz
Posts: 5289
Joined: Thu Sep 26, 2002 4:45 pm
Location: Pasadena, California
Contact:

Post by Lutz »

Nothing new here really ;-), you can do whatever you can do when nesting functions and lists in newLISP. The : colon macro is just a special 'apply' which constructs the functor on the fly with info found in the object. It helps creating type polymorphism used in OO. (:area ...) will call two different functions depending on the object type following.

Lutz
Last edited by Lutz on Wed Oct 24, 2007 12:48 pm, edited 1 time in total.

Jeff
Posts: 604
Joined: Sat Apr 07, 2007 2:23 pm
Location: Ohio
Contact:

Post by Jeff »

So these are not being created as contexts then?
Jeff
=====
Old programmers don't die. They just parse on...

Artful code

Lutz
Posts: 5289
Joined: Thu Sep 26, 2002 4:45 pm
Location: Pasadena, California
Contact:

Post by Lutz »

contexts are only created to hold the methods. The context symbol in the object as in 'rectangle' (rectangle x y width height) is just like pointer indicating where to look up the method.

Lutz

Jeff
Posts: 604
Joined: Sat Apr 07, 2007 2:23 pm
Location: Ohio
Contact:

Post by Jeff »

So the built-in def-type macro will internally store these references and simulate context symbols pointing to other contexts? Or must all types be declared in the main namespace? So that, for example, first I must define Side, and then Shape can use Sides?
Jeff
=====
Old programmers don't die. They just parse on...

Artful code

Lutz
Posts: 5289
Joined: Thu Sep 26, 2002 4:45 pm
Location: Pasadena, California
Contact:

Post by Lutz »

There is no built-in 'def-type'. The 'def-type' macro can be used to spit out a constructor and basic accessors, but is not necessary. The only thing really necessary is what you see in my post at the beginning of this thread. The contexts only hold methods. There is no nesting of contexts only nesting of classic Lisp lists. The lists are the objects. The first field in the object list is the symbol of the method context.

All types (represented by contexts) are part of the main name space, but don't need to be declared there. You can declare them inside a module/context as shown in the following code:

Code: Select all

(define-macro (: _func _obj) 
	(let (_data (eval _obj))
		(apply (sym _func (_data 0)) (list _data))))
(global ':)

(context 'Foo)

(define (rectangle:area p)
	(mul (p 3) (p 4)))

(set 'myrect '(rectangle 5 5 10 20)) ; x y width height

(:area myrect) => 200

(context MAIN)

(:area Foo:myrect) => 200
first I must define Side, and then Shape can use Sides?
normally yes, but you can even define the 'rectangle:area' function after using 'rectangle' in an object, but then you must predeclare 'rectangle:area' as in here:

Code: Select all

(define-macro (: _func _obj) 
	(let (_data (eval _obj))
		(apply (sym _func (_data 0)) (list _data))))
(global ':)

(context 'Foo)

(define rectangle:area) ; pre declaration

(set 'myrect '(rectangle 5 5 10 20)) ; x y width height

(define (rectangle:area p)
	(mul (p 3) (p 4)))

(:area myrect) => 200

(context MAIN)

(:area Foo:myrect) => 200
The pre declaration can be inside or outside of Foo.

Lutz

ps: all this code runs now in the normal release, does not need new built-in primitives, but the colon : definition will be built-in in the future for speed and convenience.
Last edited by Lutz on Wed Oct 24, 2007 3:37 pm, edited 1 time in total.

m i c h a e l
Posts: 394
Joined: Wed Apr 26, 2006 3:37 am
Location: Oregon, USA
Contact:

Post by m i c h a e l »

Hi Jeff!

Note: I wrote the following before Lutz responded. I'll post it anyway, but there is one confusing part in Lutz's answer that I wanted to clear up first:
Lutz wrote:There is now nesting of contexts only nesting of classic Lisp lists.
I think he meant to say: "There is no nesting of contexts, only nesting of classic Lisp lists".

----

Objects are lists. The first element in the list is its type. The object's methods are stored in a context. The name of the context is the same as the type.

Here is a minimal example using the new : (colon) macro:

Code: Select all

> (define (side:string) "a side")
(lambda () "a side")
> (define (shape:string sh) (string "a shape with " (length (1 sh)) " sides"))
(lambda (sh) (string "a shape with " (length (1 sh)) " sides"))
> (set 's '(shape (side) (side) (side)))
(shape (side) (side) (side))
> (:string s)
"a shape with 3 sides"
> (:string (s 1))
"a side"
> _
The def-type macro is really just for convenience (notice I didn't use it in the above example). It makes a constructor and accessors for the type being defined (in a context with the same name) but is itself totally optional (at least for now ;-)

You must create the context before making the first object, as I did with side:string and shape:string, which created the side and shape contexts.

Hopefully I answered your question in there somewhere :-)

m i c h a e l

Lutz
Posts: 5289
Joined: Thu Sep 26, 2002 4:45 pm
Location: Pasadena, California
Contact:

Post by Lutz »

michael wrote:I think he meant to say: "There is no nesting of contexts, only nesting of classic Lisp lists"
yes, I meant "no nesting of contexts", sorry for the confusion (corrected my last post)

Lutz

Jeff
Posts: 604
Joined: Sat Apr 07, 2007 2:23 pm
Location: Ohio
Contact:

Post by Jeff »

Gotcha. That's what I was asking.
Jeff
=====
Old programmers don't die. They just parse on...

Artful code

Lutz
Posts: 5289
Joined: Thu Sep 26, 2002 4:45 pm
Location: Pasadena, California
Contact:

Post by Lutz »

michael wrote:You must create the context before making the first object, as I did with side:string and shape:string, which created the side and shape contexts.
yes, and there are many ways to do it, the important thing is: the first time newLISP sees the symbol in question it must know "this is a context symbol, not a normal symbol". All of the following methods are Ok:

Code: Select all

(context 'Foo)     ; 1
(set 'Foo:bar)      ; 2
(define Foo:bar)  ; 3
(define (Foo:bar x y) ....) ; 4
the 1 might be dangerous because it also switches to Foo, which we only want when writing modules.

2 and 3 are actually exactly the same thing, but using (define Foo:bar) doesn't need the quote and it also shows better our intention, which is "define"ing something.

4 is defining the symbol right away as a function as as used in our program.

All of the above establish Foo as a context symbol when newLISP reads in the code and parses/translates it.
Lutz

Dmi
Posts: 408
Joined: Sat Jun 04, 2005 4:16 pm
Location: Russia
Contact:

Post by Dmi »

Wow!

I like it!
WBR, Dmi

Fanda
Posts: 253
Joined: Tue Aug 02, 2005 6:40 am
Contact:

Re: A simple minimal OO system for newLISP

Post by Fanda »

Lutz wrote:In a similar fashion 'rectangle:move' and 'circle:move' could be defined.
I believe that at this moment, we cannot define these methods, because macro ':' doesn't allow for any additional parameters. Also any set-x,y,width,height,radius methods cannot be created.

Also, any access to inner attributes has to be done using indexing. We cannot use x, y, width, p1, p2 or other symbols as attribute names. For example, line defined as:

Code: Select all

(line (point x y) (point x y))
will have line:length for example defined as:

Code: Select all

(define (line:length l)
  (sqrt (apply add (map pow (map sub (rest (l 1)) (rest (l 2)))))))
Because of this we cannot change position of attributes in an object list and we always have to add new attributes at the end, so that there is a bigger chance that we don't break existing functionality.

Fanda

m i c h a e l
Posts: 394
Joined: Wed Apr 26, 2006 3:37 am
Location: Oregon, USA
Contact:

Post by m i c h a e l »

Need to map a polymorphic function over a list of objects? Use curry:

Code: Select all

> ; Note: The following uses the ':' macro.
(define (point:point (x 0) (y 0)) (list point x y))
(lambda ((x 0) (y 0)) (list point x y))
(define (point:print pt) (string (pt 1) "@" (pt 2)))
(lambda (pt) (string (pt 1) "@" (pt 2)))
> (map (curry :print) (map point '(23 54 76 34) '(43 76 34 23)))
23@43
54@76
76@34
34@23
("23@43" "54@76" "76@34" "34@23")
> ; now for some polymorphism . . .
> (define (pixel:print px) (println (string "x" (px 1) ":y" (px 2))))
(lambda (px) (println (string "x" (px 1) ":y" (px 2))))
> (map (curry :print) '((point 23 54) (pixel 76 34) (point 43 76) (pixel 34 23)))
23@54
x76:y34
43@76
x34:y23
("23@54" "x76:y34" "43@76" "x34:y23")
> _
:-)

m i c h a e l

Fanda
Posts: 253
Joined: Tue Aug 02, 2005 6:40 am
Contact:

Post by Fanda »

This version allows for more parameters:

Code: Select all

(define-macro (: _func _obj)
   (let (_data (eval _obj))
      (apply (sym _func (_data 0)) (cons _data (args)))))
But... I am still having problems with "setters" (methods). Can anyone implement circle:set-xy or circle:set-radius???

Lutz
Posts: 5289
Joined: Thu Sep 26, 2002 4:45 pm
Location: Pasadena, California
Contact:

Post by Lutz »

setters are not possible this way when using the : macro, its pure functional. But you could do the following:

Code: Select all

(set 'mypoint (:move mypoint 3 4))
Lutz

ps: thanks for adding the missing additional parameters

m i c h a e l
Posts: 394
Joined: Wed Apr 26, 2006 3:37 am
Location: Oregon, USA
Contact:

Post by m i c h a e l »

Based on Lutz's last response, I've rewritten the Elica example in the FOO (functional object-oriented) style. Before I could get it to work, though, I needed to modify the : macro:

Code: Select all

(define-macro (: _func _obj)
	(let (_data (eval _obj))
		(apply (sym _func (_data 0)) (cons _data (map eval (args))))
	)
)

Code: Select all

(load "colon.lsp") ; this contains the ':' macro

;; D I S P L A Y A B L E
(define (displayable:string obj) (string obj))
(define (displayable:print obj) (println ((context (obj 0) 'string) obj) ""))

;; P O I N T
(new displayable 'point)
(define (point:point (x 0) (y 0)) (list point x y))
(define (point:string pt) (string (pt 1) "@" (pt 2)))
(define (point:move pt dx dy) (point (+ dx (pt 1)) (+ dy (pt 2))))
(define (point:oper op ags) 
  (cons point (apply map (cons op (map (fn (e) (1 e)) ags))))
)
(define (point:+) (point:oper + (args)))
(define (point:-) (point:oper - (args)))
(define (point:*) (point:oper * (args)))

;; S E G M E N T
(new displayable 'segment)
(define (segment:segment (a (point)) (b (point))) (list segment a b))
(define (segment:string sg) (string (:string (sg 1)) " to " (:string (sg 2))))
(define (segment:move sg ax ay bx by) 
  (list segment (:move (sg 1) ax ay) (:move (sg 2) bx by))
)

;; T R I A N G L E
(new displayable 'triangle)
(define (triangle:triangle (ab (segment)) (bc (segment)) (ca (segment))) 
  (list triangle ab bc ca)
)
(define (triangle:string tr) 
  (string (:string (tr 1)) ", " (:string (tr 2)) ", " (:string (tr 3)))
)
(define (triangle:move tr sg ax ay bx by) 
  (set-nth sg tr (:move (tr sg) ax ay bx by))
)

;; S A M P L E   R U N
(:print (set 'a (point)))
(:print (set 'b (point 20 0)))
(:print (set 'c (point 10 5)))
(:print (set 'tri (triangle (segment a b) (segment b c) (segment c a))))

(println "Move 'bc' segment of 'tri' by (30 5) (20 10):")
(set 'tri (:move tri 2 30 5 20 10))
(:print tri)

(println "Point addition, subtraction, and multiplication:")
(:print (point:+ a b c))
(:print (point:- a b c))
(:print (point:* (point 2 43) (point 22 1) c))

;; E N D
Here's the output:

Code: Select all

0@0
20@0
10@5
0@0 to 20@0, 20@0 to 10@5, 10@5 to 0@0
Move 'bc' segment of 'tri' by (30 5) (20 10):
0@0 to 20@0, 50@5 to 30@15, 10@5 to 0@0
Point addition, subtraction, and multiplication:
30@5
-30@-5
440@215
m i c h a e l

Lutz
Posts: 5289
Joined: Thu Sep 26, 2002 4:45 pm
Location: Pasadena, California
Contact:

Post by Lutz »

I finished implementing the colon : macro natively and it runs your point/segment/triangle and polymorphism example code with identical results :-)

Lutz

cormullion
Posts: 2038
Joined: Tue Nov 29, 2005 8:28 pm
Location: latiitude 50N longitude 3W
Contact:

Post by cormullion »

A few questions from someone struggling to keep up ... :)

When i try to run your example, michael, I get this error:

Code: Select all

string expected in function context : 'MAIN:string
called from user defined function point:print
which is:

Code: Select all

(define (displayable:print obj)  (println ((context (obj 0) 'string) obj) ""))
I suppose?

What is the 'displayable' here, anyway? Some kind of abstract class, I expect?

And can

Code: Select all

(:print tri)
be read as "applying the method :print to the 'tri' object, which is an instance of class 'triangle', which was created by

Code: Select all

(set 'tri (triangle (segment a b)...
".... (or something similar)?

m i c h a e l
Posts: 394
Joined: Wed Apr 26, 2006 3:37 am
Location: Oregon, USA
Contact:

Post by m i c h a e l »

Hi Cormullion!

I wondered when you were going to join the party ;-)

I'm not sure why you are getting that error. I re-ran my local copy and even copied it again from the above post (in case it was a formatting issue), and neither produced any errors. Lutz also seems to be able to run it correctly.

Wait! I bet you're using an earlier version of newLISP (before symbols were allowed in context), so changing the following:

Code: Select all

(define (displayable:print obj)  (println ((context (obj 0) 'string) obj) "")) 
to

Code: Select all

(define (displayable:print obj)  (println ((context (obj 0) "string") obj) "")) 
should fix it.
Cormullion wrote:What is the 'displayable' here, anyway? Some kind of abstract class, I expect?
I'm using it as a mixin. What this is saying is: A point, segment, and triangle are displayable. (Really, I was just too lazy to write a print method for each data type ;-)
Cormullion wrote:And can (:print tri) be read as "applying the method :print to the 'tri' object, which is an instance of class 'triangle',
Yes, but to be a little more exact, I would say "polymorphically apply the print method to the triangle object referenced by the symbol tri." Polymorphic is just a fancy word for objects responding to messages according to their type. Notice that :print actually ended up calling point:print. print is really just the first argument to :, which resolves the call by extracting the type from the object (the first element).

Feel free to pick my brain!

m i c h a e l

m i c h a e l
Posts: 394
Joined: Wed Apr 26, 2006 3:37 am
Location: Oregon, USA
Contact:

Post by m i c h a e l »

Here's a complex example ;-)

Code: Select all

(load "colon.lsp") ; soon, this won't be necessary ;-)

(define (complex:complex (r 0) (i 0)) (list complex r i))
(define (complex:rad c) 
	(set 're (c 1) 'im (c 2)) 
	(sqrt (add (mul re re) (mul im im)))
)
(define (complex:theta c) (atan (div (c 1) (c 2))))
(define (complex:add a b) (complex (add (a 1) (b 1)) (add (a 2) (b 2))))
(define (complex:mul a b) 
	(set 
		'a.re (a 1) 'a.im (a 2) 
		'b.re (b 1) 'b.im (b 2)
	)
	(complex 
		(sub (mul a.re b.re) (mul a.im b.im))
		(add (mul a.re b.im) (mul a.im b.re))
	)
)
(define (complex:square c) (:mul c c))
Here is an example of use:

Code: Select all

> (set 'c1 (complex 34.2 54.2))
(complex 34.2 54.2)
> (set 'c2 (complex 19.8 73.9))
(complex 19.8 73.9)
> (:add c1 c2)
(complex 54 128.1)
> (:mul c1 c2)
(complex -3328.22 3600.54)
> (:rad c1)
64.08806441
> (:theta c1)
0.5628996527
> (:square c2)
(complex -5069.17 2926.44)
> _
I have no idea if I'm using these complex numbers correctly or not :-)

complex still needs sub and div methods, but I've already been distracted by wanting to model the soda-dispensing machine from my Booch book!

m i c h a e l

cormullion
Posts: 2038
Joined: Tue Nov 29, 2005 8:28 pm
Location: latiitude 50N longitude 3W
Contact:

Post by cormullion »

Aha - I'm using 9.2.3 - I'd forgotten I was one step behind. You get used to it.

I've been at this party since it started - but I'm just hanging around near the drinks table being inconspicuous, rather than dancing naked in the middle of the room...

I'll read up about mixins, see if it makes more sense.

Currently ':print tri' feels slightly backwards - but that might change as I think about it some more.

Thanks for the explanations...!

newdep
Posts: 2038
Joined: Mon Feb 23, 2004 7:40 pm
Location: Netherlands

Post by newdep »

This OO issue smells like fresh baked cookies... ;-)



..Cant wait to eat them ;-)
-- (define? (Cornflakes))

m i c h a e l
Posts: 394
Joined: Wed Apr 26, 2006 3:37 am
Location: Oregon, USA
Contact:

Post by m i c h a e l »

Cormullion wrote:Currently ':print tri' feels slightly backwards - but that might change as I think about it some more.
Think of it as leaving off the context so the specific method can be determined dynamically. We have point:print, segment:print, and triangle:print. When we leave off the context, as in (:print some-obj), the : macro determines which method to call for us, based on the object passed as the second argument (remember, print is the first argument, even though there is no space between the : and print). If you know for sure an object will always be of a certain type, you can (and should?) call the method directly: (point:print a-point), for example. You only need polymorphism when you are unsure of the type of the object beforehand or you have a list of objects of differing types, each having a method defined with the same name (print in this case).
Cormullion wrote:I'll read up about mixins, see if it makes more sense.
Mixins are just another form of inheritance, used in languages that have only single, rather than multiple inheritance. Ruby uses mixins, for example. Think of them as abilities that can be mixed into objects in various combinations. In my example, I want to give the objects the ability to display themselves, so I mix in displayable. Now any object with this mixed in can display itself and can even override displayable's string method to display itself in an object-sepecific way (as I did with point, segment, and triangle).
Cormullion wrote:I've been at this party since it started - but I'm just hanging around near the drinks table being inconspicuous, rather than dancing naked in the middle of the room...
:-) You'll warn us before you get the urge, won't you, Cormullion? There may be children present!

m i c h a e l

Locked