From: ···········@gmail.com
Subject: Java OO syntax reader macro
Date: 
Message-ID: <1153595393.985939.126860@m79g2000cwm.googlegroups.com>
Hi,

This is my first attempt to create a reader macro for experimentation.

Consider the following silly example:

(defclass fruit ()
  ((origin :initarg :origin)
   (count :initarg :count)))

(defclass fruit-basket ()
  ((apples :initform (make-hash-table :test 'equalp))
   (oranges :initform (make-hash-table :test 'equalp))))

(defparameter *fruit-basket* (make-instance 'fruit-basket))

(setf (gethash "texas" (slot-value *fruit-basket* 'oranges))
            (make-instance fruit :origin "texas" :count 10))

(setf (gethash "fuji" (slot-value *fruit-basket* 'apples))
            (make-instance fruit :origin "fuji" :count 15))


Here's a simple reader macro

(defun single-p (list)
  (and (listp list)
       (car list)
       (null (cdr list))))

(defun split-symbols (symbol)
  (mapcar #'intern (split-sequence:split-sequence #\. (string
symbol))))

(defun translate-java-oo-form (names)
  (if (single-p names)
      (car names)
      (translate-java-oo-form (cons `(slot-value ,(first names)
',(second names))
                                    (nthcdr 2 names)))))
(defun dollar-reader (stream char)
   (declare (ignorable char))
   (translate-java-oo-form (split-symbols (read stream t nil t))))

(set-macro-character #\$ #'dollar-reader)

With this I can say

$obj.x.m

which get translated to

(SLOT-VALUE (SLOT-VALUE OBJ (QUOTE X)) (QUOTE M))

So $*fruit-basket*.apples will get me the hash table.

My next attempt is to make the reader macro to translate

$*fruit-basket*.apples["fuji"].count

into

(slot-value (gethash "fuji" (slot-value *fruit-basket* 'apples))
'count)

But it seems to be more difficult than I think because I cannot reuse
(read stream t nil t) recursively ( read only return up to
*fruit-basket*.apples[ )

So I need to consume the stream directly. But if I do that I still need
to somehow use read in my parsing routine because something like this

$*fruit-basket*.apples[ <SOME ARBITARY LISP EXPR> ].count

can happen and I should still be able to translate it properly.


If anyone can give some hints I'd highly appreciate.

(BTW, this is just an experiment; please don't criticize this reader
macro's usefulness. I just want to educate myself to see what can be
done in lisp and how easy/hard it would be.)

TIA.
fungsin

From: Pascal Costanza
Subject: Re: Java OO syntax reader macro
Date: 
Message-ID: <4ifoimF3l5bqU1@individual.net>
···········@gmail.com wrote:
> Hi,
> 
> This is my first attempt to create a reader macro for experimentation.
> 
> Consider the following silly example:
> 
> (defclass fruit ()
>   ((origin :initarg :origin)
>    (count :initarg :count)))
> 
> (defclass fruit-basket ()
>   ((apples :initform (make-hash-table :test 'equalp))
>    (oranges :initform (make-hash-table :test 'equalp))))
> 
> (defparameter *fruit-basket* (make-instance 'fruit-basket))
> 
> (setf (gethash "texas" (slot-value *fruit-basket* 'oranges))
>             (make-instance fruit :origin "texas" :count 10))
> 
> (setf (gethash "fuji" (slot-value *fruit-basket* 'apples))
>             (make-instance fruit :origin "fuji" :count 15))
> 
> 
> Here's a simple reader macro
> 
> (defun single-p (list)
>   (and (listp list)
>        (car list)
>        (null (cdr list))))
> 
> (defun split-symbols (symbol)
>   (mapcar #'intern (split-sequence:split-sequence #\. (string
> symbol))))
> 
> (defun translate-java-oo-form (names)
>   (if (single-p names)
>       (car names)
>       (translate-java-oo-form (cons `(slot-value ,(first names)
> ',(second names))
>                                     (nthcdr 2 names)))))
> (defun dollar-reader (stream char)
>    (declare (ignorable char))
>    (translate-java-oo-form (split-symbols (read stream t nil t))))
> 
> (set-macro-character #\$ #'dollar-reader)
> 
> With this I can say
> 
> $obj.x.m
> 
> which get translated to
> 
> (SLOT-VALUE (SLOT-VALUE OBJ (QUOTE X)) (QUOTE M))
> 
> So $*fruit-basket*.apples will get me the hash table.
> 
> My next attempt is to make the reader macro to translate
> 
> $*fruit-basket*.apples["fuji"].count
> 
> into
> 
> (slot-value (gethash "fuji" (slot-value *fruit-basket* 'apples))
> 'count)
> 
> But it seems to be more difficult than I think because I cannot reuse
> (read stream t nil t) recursively ( read only return up to
> *fruit-basket*.apples[ )
> 
> So I need to consume the stream directly. But if I do that I still need
> to somehow use read in my parsing routine because something like this
> 
> $*fruit-basket*.apples[ <SOME ARBITARY LISP EXPR> ].count
> 
> can happen and I should still be able to translate it properly.

You have to modify how brackets are read - by default, they don't behave 
similar to parentheses, which is what you want here. (See Section 2.1.4 
in the HyperSpec for how they differ.)

Section 22.1.5 in CLtL2 [1] has an elaborate example for programming the 
reader, whose study should help you to get closer to a working solution.


Pascal

[1] Guy Steele, Common Lisp the Language, 2nd Edition. Google for it, 
you can find it free for download.

-- 
My website: http://p-cos.net
Closer to MOP & ContextL:
http://common-lisp.net/project/closer/
From: ···········@gmail.com
Subject: Re: Java OO syntax reader macro
Date: 
Message-ID: <1153617251.688138.321820@m73g2000cwd.googlegroups.com>
Pascal Costanza wrote:
>
> You have to modify how brackets are read - by default, they don't behave
> similar to parentheses, which is what you want here. (See Section 2.1.4
> in the HyperSpec for how they differ.)

Thanks for the pointer. I did read that part from CLHS and CLTL2.
However, I still can't figure out an easy way to reuse the read and
read-xxx functions.

So I took the easier route and not to introduce any special characters
after the $ macro char.

Instead of

$*fruit-basket*.apples["fuji"].count

I change the syntax to

$*fruit-basket*.apples{fuji}.count

Now read will return the whole token *fruit-basket*.apples{fuji}.count

There are several downsides:

- I can't use arbitrary lisp expression as the hash key (string name
only)
- hash key are converted to upper case
- I can't specify hash key with embedded spaces
- other bugs and corner cases.

It seems that to achieve what I want I still have to consume the stream
and parse the string as the lisp reader does, take out the parts that I
need to handle and pass various (subseq ...) to the reader.

-- fungsin
(PS: there's a typo in my 1st post, I forget to add a single quote
after make-instance)


(defclass fruit ()
  ((origin :initarg :origin)
   (count :initarg :count)))

(defclass fruit-basket ()
  ((apples :initform (make-hash-table :test 'equalp))
   (oranges :initform (make-hash-table :test 'equalp))))

(defparameter *fruit-basket* (make-instance 'fruit-basket))

(setf (gethash "texas" (slot-value *fruit-basket* 'oranges))
            (make-instance 'fruit :origin "texas" :count 10))

(setf (gethash "fuji" (slot-value *fruit-basket* 'apples))
            (make-instance 'fruit :origin "fuji" :count 15))


(defun position* (items sequence &rest args &key from-end test test-not
start end key)
  "Like position but you can search for multiple items at
once. Position* returns the index of the first item found and the item
as the second value."
  (declare (ignorable from-end test test-not start end key))
  (let* ((results
          (mapcar (apply #'rcurry #'position sequence args)
                  items))
         (filtered-result (remove-if #'null results))
         (pos (when filtered-result (apply (if from-end #'max #'min)
filtered-result))))
    (when pos
      (values pos (nth (position pos results) items)))))


(defun translate-java-oo-form (string)
  (multiple-value-bind (pos ch)
      (position* (list #\} #\.) string :from-end t)
    (if pos
        (ecase ch
          (#\}
           (let (({pos (position #\{ string :end pos :from-end t)))
             (if {pos
                 `(gethash ,(subseq string (1+ {pos) pos)
                   ,(translate-java-oo-form (subseq string 0 {pos)))
                 (error "Non-matching { } syntax"))))
          (#\.
           `(slot-value ,(translate-java-oo-form (subseq string 0 pos))
             ',(intern (subseq string (1+ pos))))))
        (with-input-from-string (in string)
          (read in t nil)))))


(defun dollar-reader (stream char)
   (declare (ignorable char))
   (translate-java-oo-form (string (read stream t nil t))))

(set-macro-character #\$ #'dollar-reader)



$*fruit-basket*.apples{fuji}.count

=>

(SLOT-VALUE (GETHASH "FUJI" (SLOT-VALUE *FRUIT-BASKET* (QUOTE APPLES)))
(QUOTE COUNT))


=> 15
From: Nathan Baum
Subject: Re: Java OO syntax reader macro
Date: 
Message-ID: <1153623023.136999.264520@s13g2000cwa.googlegroups.com>
(defun ignore-reader (stream char)
  nil)

(defvar *dollar-readtable* (copy-readtable))

(let ((*readtable* *dollar-readtable*))
  (set-macro-character #\. #'ignore-reader nil)
  (set-macro-character #\[ #'ignore-reader nil)
  (set-macro-character #\] #'ignore-reader nil))

(defun dollar-reader (stream char)
  (let* ((*readtable* *dollar-readtable*)
         (object (read stream nil nil t)))
    (loop
       (let ((char (read-char stream)))
         (case char
           (#\. (setf object `(slot-value ,object ',(read stream nil
nil t))))
           (#\[ (setf object `(gethash ,(car (read-delimited-list #\]
stream)) ,object)))
           (t (unread-char char stream)
              (return object)))))))

(set-macro-character #\$ #'dollar-reader)

(defstruct struct
  bar)

(let ((foo (make-struct)))
  (setf $foo.bar (make-hash-table :test #'equal))
  (setf $foo.bar["foo"] 12)
  (print $foo.bar["foo"]))

Future work is to replace the calls to slot-value and gethash with
calls to generic methods, so that e.g. $(1 2 3 4)[1] can work.
From: ···········@gmail.com
Subject: Re: Java OO syntax reader macro
Date: 
Message-ID: <1153714168.375110.157840@h48g2000cwc.googlegroups.com>
Nathan Baum wrote:
> Future work is to replace the calls to slot-value and gethash with
> calls to generic methods, so that e.g. $(1 2 3 4)[1] can work.

Your solution is short and concise. Thanks!

BTW, does anyone know if there is a standard reader-macro-expand
function for examining expanded form?

I tried the following in lispworks and it doesn't quite work.

(with-input-from-string (in "$foo.bar[\"foo\"]" ) (dollar-reader in
#\$))

Error: The variable IO:*DECODED-INPUT* is unbound.
  1 (continue) Try evaluating IO:*DECODED-INPUT* again.
  2 Specify a value to use this time instead of evaluating
IO:*DECODED-INPUT*.
  3 Specify a value to set IO:*DECODED-INPUT* to.
  4 (abort) Return to level 0.
  5 Return to top loop level 0.

Type :b for backtrace, :c <option number> to proceed,  or :? for other
options

--fungsin
From: Nathan Baum
Subject: Re: Java OO syntax reader macro
Date: 
Message-ID: <1153718987.396882.136640@i3g2000cwc.googlegroups.com>
···········@gmail.com wrote:
> Nathan Baum wrote:
> > Future work is to replace the calls to slot-value and gethash with
> > calls to generic methods, so that e.g. $(1 2 3 4)[1] can work.
>
> Your solution is short and concise. Thanks!
>
> BTW, does anyone know if there is a standard reader-macro-expand
> function for examining expanded form?
>
> I tried the following in lispworks and it doesn't quite work.
>
> (with-input-from-string (in "$foo.bar[\"foo\"]" ) (dollar-reader in
> #\$))
>
> Error: The variable IO:*DECODED-INPUT* is unbound.
>   1 (continue) Try evaluating IO:*DECODED-INPUT* again.
>   2 Specify a value to use this time instead of evaluating
> IO:*DECODED-INPUT*.
>   3 Specify a value to set IO:*DECODED-INPUT* to.
>   4 (abort) Return to level 0.
>   5 Return to top loop level 0.
>
> Type :b for backtrace, :c <option number> to proceed,  or :? for other
> options

This is probably because dollar-reader indicates that it's being called
recursively, and Lispworks expects the internal state of the reader to
be set up to reflect that. Sometimes the reader doesn't indicate it's
being called recursively; that's a bug. (CLISP doesn't appear to care,
so I haven't been bitten by it.)

When I was testing my code, I just used:

  CL-USER> (print '$foo.bar["foo"])
  (GETHASH "foo" (SLOT-VALUE FOO 'BAR))

If you particularly need to parse a string, then

  CL-USER> (read-from-string "$foo.bar[\"foo\"]")
  (GETHASH "foo" (SLOT-VALUE FOO 'BAR))

For that to work, dollar-reader needs to be fixed so that it doesn't
break upon EOF: (read-char stream) should be (read-char nil nil t).

Also you'll need to do the EVAL-WHEN dance to make it work when you
want to use it with COMPILE-FILE. I put everything up to
(read-from-string "$foo.bar[\"foo\"]") in a (eval-when
(:compile-toplevel :load-toplevel :execute) ... ), which seems to work
for all combinations of compiling and loading I tried.

> 
> --fungsin
From: Nathan Baum
Subject: Re: Java OO syntax reader macro
Date: 
Message-ID: <1153720001.713586.325560@p79g2000cwp.googlegroups.com>
Nathan Baum wrote:
> Also you'll need to do the EVAL-WHEN dance to make it work when you
> want to use it with COMPILE-FILE. I put everything up to
> (read-from-string "$foo.bar[\"foo\"]")

That should have been (set-macro-character #\$ #'dollar-reader). The
darned two-faced X clipboard bit me again. :(

> 
> > 
> > --fungsin
From: Nathan Baum
Subject: Re: Java OO syntax reader macro
Date: 
Message-ID: <1153719211.000164.241380@h48g2000cwc.googlegroups.com>
···········@gmail.com wrote:
> Nathan Baum wrote:
> > Future work is to replace the calls to slot-value and gethash with
> > calls to generic methods, so that e.g. $(1 2 3 4)[1] can work.
>
> Your solution is short and concise. Thanks!
>
> BTW, does anyone know if there is a standard reader-macro-expand
> function for examining expanded form?
>
> I tried the following in lispworks and it doesn't quite work.
>
> (with-input-from-string (in "$foo.bar[\"foo\"]" ) (dollar-reader in
> #\$))

Also, in real life the stream wouldn't contain $ by the time
dollar-reader is called. That wouldn't cause this error, though: the
reader should just interpret the $ as being the first character of the
symbol $FOO since *dollar-reader-readtable* doesn't treat $ specially.

>
> Error: The variable IO:*DECODED-INPUT* is unbound.
>   1 (continue) Try evaluating IO:*DECODED-INPUT* again.
>   2 Specify a value to use this time instead of evaluating
> IO:*DECODED-INPUT*.
>   3 Specify a value to set IO:*DECODED-INPUT* to.
>   4 (abort) Return to level 0.
>   5 Return to top loop level 0.
>
> Type :b for backtrace, :c <option number> to proceed,  or :? for other
> options
> 
> --fungsin