More about Lutz's new macro

Started by m i c h a e l, October 06, 2007, 10:58:28 AM

Previous topic - Next topic

m i c h a e l

I decided to dig up the http://www.angelfire.com/tx4/cus/shapes/index.html">link to a site that contains a number of object-oriented implementations of the classic shape example used in most OOP books. Armed with only Lutz's new macro, I set out to see how far I could get. I used the Ruby implementation as a starting point. Here's shape.lsp (note: I'm using data-type instead of def-class in the following code :-):


(load "/lisp/data-type.lsp")

(data-type (shape x y))

(define (shape:move shp newx newy)
(shape:x shp newx)
(shape:y shp newy)
)

(define (shape:r-move shp newx newy)
(shape:move shp (+ (shape:x shp) newx) (+ (shape:y) newy))
)


If you compare this code with the Ruby version, you'll see it's quite a bit shorter. And if you run it, you'll also see that it doesn't work!



Here is how I had to rewrite shape:move for it to function properly:


(define-macro (shape:move shp newx newy)
(nth-set ((eval shp) 0) (eval newx))
(nth-set ((eval shp) 1) (eval newy))
)


It works, but it also seems too low-level. I'm not even sure how to change shape:r-move to make it work correctly. Am I overlooking something? (Most probable.) Should a companion macro be introduced to create methods for these new data types? Will Lutz begin hating me for my unhealthy fixation on OO?! ;-)



m i c h a e l

rickyboy

#1
Oh boy!  It seems as if we can't get around pass-by-value unless our data is in a context (and we wanted to try to avoid that in the first place! :-).  However, one way to get around it is to rewrite data-type as
(define-macro (data-type)
  (let (ctx (context (args 0 0)))
    (set (default ctx) (lambda () (args)))
    (dolist (item (rest (args 0)))
      (set (sym item ctx)
           (expand '(lambda (_str _val)
                      (if _val
                          (nth-set ((eval _str) $idx) _val)  
                          ((eval _str) $idx))) '$idx)))
    ctx))

Notice that the setters and getters are NOT macros now, but regular functions.  But now your calling code should look like this:
> (setq s1 (shape 4 2))
(4 2)
> s1
(4 2)
> (shape:move 's1 5 6)
2
> s1
(5 6)
> (shape:r-move 's1 2 3)
6
> s1
(7 9)

Notice now that all the functions are passing around the *symbol* s1 until it floats down to the setter/getter which resolves the symbol to the actual object -- kind of a poor man's pass-by-reference.  The good news is that definition of your move functions remain the same.



Hope that helps.  --Ricky



P.S. -- BTW I took the liberty of fixing a typo in the definition of r-move: I had to change the expression (shape:y) to (shape:y shp).
(λx. x x) (λx. x x)

rickyboy

#2
Well, what I just said in the previous post still stands, HOWEVER ... a potential problem still remains: we still can't deal in anonymous "objects".



Recall m i c h a e l's example:
> (data-type (person last first))
person
> (data-type (book title author isbn))
book
> (set 'catch-22 (book "Catch-22" (person "Heller" "Joseph") "0-684-83339-5"))
("Catch-22" ("Heller" "Joseph") "0-684-83339-5")
> (person:first (book:author 'catch-22))

value expected in function eval : "Joseph"
called from user defined function person:first

Oops!  What happened?  Well what happened is that the inside expression is resolving to ("Heller" "Joseph"), and so person:first is now trying to deal with an anonymous object.  We could make yet another change to data-type which could fix this situation:
(define-macro (data-type)
  (let (ctx (context (args 0 0)))
    (set (default ctx) (lambda () (args)))
    (dolist (item (rest (args 0)))
      (set (sym item ctx)
           (expand '(lambda (_str _val)
                      (let ((_str (if (symbol? _str)
                                      (eval _str) _str)))
                        (if _val
                            (nth-set (_str $idx) _val)  
                            (_str $idx)))) '$idx)))
    ctx))

This has the getter/setter check if the object passed to it is anonymous or not.  It works for this situation:
> (person:first (book:author 'catch-22))
"Joseph"

But if I tried to use the setter to change the author's first name, it doesn't work:
> (person:first (book:author 'catch-22) "Dweezil")
"Joseph"
> (person:first (book:author 'catch-22))
"Joseph"

Now we are back to the original problem: the setter person:first is working with a COPY of (book:author 'catch-22), not the original object.



We have to think of a better implementation.  Hmm, interesting puzzle ...
(λx. x x) (λx. x x)

Lutz

#3
QuoteNotice that the setters and getters are NOT macros now, but regular function


They are macros, the 'lambda' in my post was a temporary change, which has been taken back to 'lambda-macro'. Here again the correct implementation (now called def-type ;-) ):


(define-macro (def-type)
  (let (ctx (context (args 0 0)))
    (set (default ctx) (lambda () (args)))
    (dolist (item (rest (args 0)))
      (set (sym item ctx)
      (expand '(lambda-macro (_str _val)
          (if (set '_val (eval _val))
          (nth-set ((eval _str) $idx) _val)    
          ((eval _str) $idx))) '$idx)))
ctx))


The macro is necessary for the setter part, so the symbol can be accessed without quoting it (as in Rickyboy's workaround in the previous post). The only way around this is using contexts, which can be used to pass wrapped data by reference.



But the 'anonymous' object question still remains. In newLISP each object you want to access must be somehow anchored in a symbol. This is why the original object implementation in the manual works with contexts as objects, because you can pass them to a function without a macro:


(set 'person1:name "joseph")
(set 'person1:address "San Francisco")

(define (change-name p n)
(set 'p:name n))

(change-name person1 "Dweezil")

person1:name => "Dweezil"


The original template object implementation with contexts works pretty well except for two details: (1) each object carries along with it is a full copy of all the methods, and (2) no anonymous objects, each object must be attached to a context/symbol.



Again newLISP is not designed to be an OO language. A 'def-type' like macro works well for structuring data and packageing all functions related to that data type into one context. But its not original OO where data and methods are together in one object.



Lutz

m i c h a e l

#4
Note: I wrote a response to Ricky's first response before I saw his second response or even Lutz's response to his responses (did you follow that?). I'll post my original response following this one.



Lutz,



You have often said (and you must be getting well sick of it by now ;-) that newLISP is not designed to be an OO language. Fair enough. But what about those of us who have already had the OO way embedded into our brains? If it is possible, why discourage us from writing our newLISP programs in a way that is natural to us? Aren't you gently forcing us to convert to FP or be turned away in frustration?



I myself have earnestly tried to embrace the functional way of programming. But long-developed habits die hard (and to be honest, I'm not sure I want to lose them) and, as for me, once a functional program grows to a certain size, I can no longer understand how it works (FontComp is beginning to make my head swim). I concede that this difficulty may stem from my ignorance of maths and proper programming practices. Maybe that's why object-oriented programming has always appealed to me. It's fun to build things out of Legos!



I guess what I'm trying to say is: Please Lutz, have a little sympathy for us poor object-heads :-) If a module filled with a couple of macros and functions can fake it well enough to fool us into believing newLISP is the greatest OOP language invented, then why not?



Besides, we wouldn't be using OOP anyway. We would be FOOPing. Functional object-oriented programming ;-)



m i c h a e l

m i c h a e l

#5
Note: This is the original response promised in the above post. This code will no longer work with Lutz's redefined macro def-type :-(



Hi Ricky!



Thank you for taking the time to solve this. With your solution, I was able to finish the implementation with only one small change.



The polymorphism test required shapes in a list to respond in a polymorphic way to the same messages (draw and r-move-to). Because our methods are separated into different contexts (rightly so), how could one do this? My solution was to modify data-type to include the context as the first element of the object's list representation. This also required the index for the generated accessors to be one greater than before. Now we can get the object's type by retrieving its first element:


> (data-type (point x y))
point
> (set 'pt (point 11 33) 'type (pt 0))
point
> (type:y 'pt)
33
> _


All in all, this makes for a beautifully simple and clean expression of the OO shape example:


(load "/lisp/data-type.lsp")

; shape

(data-type (shape x y))

(define (shape:move-to shp newx newy)
(shape:x shp newx)
(shape:y shp newy)
)

(define (shape:r-move-to shp newx newy)
(shape:move-to shp
(+ (shape:x shp) newx)
(+ (shape:y shp) newy)
)
)

; rectangle

(data-type (rectangle x y width height))

(new shape rectangle)

(define (rectangle:draw rec)
(println
"Draw a rectangle at:(" (rectangle:x rec) "," (rectangle:y rec)
"), width " (rectangle:width rec)
", height " (rectangle:height rec)
)
)

; circle

(data-type (circle x y radius))

(new shape circle)

(define (circle:draw cir)
(println
"Draw a circle at:(" (circle:x cir) "," (circle:y cir)
"), radius " (circle:radius cir)
)
)

; polymorphism test

; create a collection containing various shape instances
(set 'scribble
(list
(rectangle 10 20 5 6)
(circle 15 25 8)
)
)

; iterate through the collection and handle shapes polymorphically
(dolist (ea scribble)
(set 'type (ea 0))
(type:draw 'ea)
(type:r-move-to 'ea 100 100)
(type:draw 'ea)
)

; access a rectangle specific function
(set 'a-rectangle (rectangle 0 0 15 15))
(rectangle:width 'a-rectangle 30)
(rectangle:draw 'a-rectangle)


The newLISP version is about half the size of the Ruby version. Of course, this is because our macro is writing the constructor and accessors for us, but still, even at this early stage, the new macro feels comfortable to use and fits newLISP well.



With the addition of the context in the type's representation, the macro could now also create functions for testing an object's type:


> (data-type (complex real imaginary))
complex
> (set 'cn (complex 3 2))
(complex 3 2)
> (complex:? 'cn)
true
>


What else could the macro write for us?



Here is the updated macro. I'm not sure my solution is that great, so any suggested changes are welcome:


(define-macro (data-type)
  (let (ctx (context (args 0 0)))
    (set (default ctx)
(expand
'(lambda () (append (list ctx) (args)))
'ctx
)
)
    (dolist (item (rest (args 0)))
      (set
'idx (+ $idx 1)
(sym item ctx)
       (expand
'(lambda (_str _val)
          (if _val
            (nth-set ((eval _str) idx) _val)    
             ((eval _str) idx)
)
)
'idx
)
)
)
    ctx
)
)


m i c h a e l



P.S. Good catch on the missing argument to shape:y. Thanks!



P.P.S. An interesting thing about this new representation: it mirrors the expression that created it, so we can evaluate the result and keep getting the same result:



> (data-type (pixel x y color))
pixel
> (eval (eval (eval (pixel 45 23 "blue"))))
(pixel 45 23 "blue")
> _

newBert

#6
Quote from: "Lutz"


Again newLISP is not designed to be an OO language. A 'def-type' like macro works well for structuring data and packageing all functions related to that data type into one context. But its not original OO where data and methods are together in one object.



Lutz


Fortunately ! OOP is not panacea and is a little heavy by comparison with "functional" solutions.



For lighter OOP 'contexts' are maybe THE solution ... see what is NOOP (Natural OOP) with ELICA (a Lisp-descendant language since it's a Logo-like language) :

http://www.elica.net/download/papers/ElicaLogoObjects.pdf">//http://www.elica.net/download/papers/ElicaLogoObjects.pdf

http://www.elica.net/download/papers/NOOP-TheEvolutionMetaphorInElica.pdf">//http://www.elica.net/download/papers/NOOP-TheEvolutionMetaphorInElica.pdf



Sorry if my message is inopportune ( I don't understand in detail everything you wrote ... as a non-english speaking guy ;) )



long live newLisp !

:)
<r><I>>Bertrand<e></e></I> − <COLOR color=\"#808080\">><B>newLISP<e></e></B> v.10.7.6 64-bit <B>>on Linux<e></e></B> (<I>>Linux Mint 20.1<e></e></I>)<e></e></COLOR></r>

cormullion

#7
I'm enjoying these OO posts, although I can't contribute much, apart from the occasional murmur of appreciation.

Lutz

#8
Quote from: "m i c h a e l"This is the original response promised in the above post. This code will no longer work with Lutz's redefined macro def-type :-(


Should work by just passing the object symbol unquoted?


Quote from: "m i c h a e l"My solution was to modify data-type to include the context as the first element of the object's list representation.


This idea is crucial. Now we have the data linked to its interface without copying in each new object. Here a small improvement in the constructor template:


; instead of
(expand '(lambda () (append (list ctx) (args))) 'ctx)
; use
(expand '(lambda () (cons ctx (args))) 'ctx)


'cons' is rarely used in newLISP, but this is the place ;)



Lutz

rickyboy

#9
Quote from: "m i c h a e l"Thank you for taking the time to solve this. With your solution, I was able to finish the implementation with only one small change.

Small yes, but also significant and smart.  Sure it doesn't look like much on the surface, but that would belie the work that it took to figure out the right change to make. What did you do, like change *two* lines of code, and boom, you have polymorphism?  Awesome.  I rarely ever get that kind of ROI. :-)  Nice work, m i c h a e l!
(λx. x x) (λx. x x)

m i c h a e l

#10
Quote from: "cormullion self-deprecatingly"although I can't contribute much, apart from the occasional murmur of appreciation.


Are you kidding? You can give us a perspective I myself lost long ago. I'm too marinated in the object-think soup, now. Sometimes when you get too familiar with a subject, someone simply paying attention can see what you have become blind to.



m i c h a e l

m i c h a e l

#11
Quote from: "Lutz"Should work by just passing the object symbol unquoted?


Unfortunately, we still have the problem mentioned by Ricky. It doesn't work with the following example run from within newLISP edit:


(load "/lisp/def-type.lsp")

(def-type (shape x y))

(set 's1 (shape 0 0))

(shape:x s1  25)
(shape:y s1 624)

s1

(define (shape:move-to shp newx newy)
(shape:x shp newx)
(shape:y shp newy)
)

(shape:move-to s1)

s1

=============================o=============================

newLISP v.9.2.3 on OSX UTF-8, execute 'newlisp -h' for more info.

> (lambda-macro ()
 (let (ctx (context (args 0 0)))
  (set (default ctx) (expand '(lambda () (cons ctx (args))) 'ctx))
  (dolist (item (rest (args 0)))
   (set 'idx (+ $idx 1) (sym item ctx) (expand '(lambda-macro (_str
       _val)
      (if (set '_val (eval _val))
       (nth-set ((eval _str) idx) _val)
       ((eval _str) idx))) 'idx))) ctx))
shape
(shape 0 0)
0
0
(shape 25 624)
(lambda (shp newx newy) (shape:x shp newx) (shape:y shp newy))
624
(shape 25 624)
>


As you can see, s1 is still (shape 25 624) instead of the correct (shape 50 1248).



If macros are necessary for our data type functions to operate correctly, maybe we need a function (or macro) to write them for us (like def-type is doing).


Quote from: "Lutz"'cons' is rarely used in newLISP, but this is the place ;)


Thanks, Lutz. I knew that code of mine smelled, but like you said, I don't think I've used the cons function even once.



m i c h a e l

m i c h a e l

#12
Ricky,



Thank you for you kind words. Even though I've been working with programming languages all these many years, I'm still an embarrassingly poor programmer. Your encouragement gives me hope that all this time hasn't been spent in vain :-)



m i c h a e l

Lutz

#13
Quote from: "m i c h a e l"If macros are necessary for our data type functions to operate correctly, ...


Either write all data modifying accessor functions using lambda-macro and not have to quote symbols or write all desctrucive accessors as normal lambda funtions and quote the object symbol when required. Code is easier to read and slightly faster when using the norma define/lambda and quote the symbols if necessary ... decisions, decisions ... ;-)



Perhaps writing a bigger app gives an answer what is more convenient in the long run.



Lutz

m i c h a e l

#14
Quote from: "Lutz"Either write all data modifying accessor functions using lambda-macro and not have to quote symbols or write all desctrucive accessors as normal lambda funtions and quote the object symbol when required. Code is easier to read and slightly faster when using the norma define/lambda and quote the symbols if necessary ... decisions, decisions ... ;-)


Using Ricky's modification (where quoting was necessary) still felt perfectly natural and not at all obtrusive. Best to keep it as close to the way newLISP already does things.


Quote from: "Lutz"Perhaps writing a bigger app gives an answer what is more convenient in the long run.


That is a good idea. OO was supposed to be the silver bullet against complexity*, so many problems wouldn't crop up until a program obtains a certain mass to it. Any idea what program it should be?



m i c h a e l



* We know how that turned out ;-)