From: Rodney
Subject: Newbee: Lispy alternatives for configuration information file
Date: 
Message-ID: <y0kQc.110456$bp1.3238@twister.nyroc.rr.com>
I currently have a program that stores configuration information
in a modified window's INI file format.  The information is
read at runtime and loads internal values and arrays.  I would
like to replace this file with a Lisp friendly format to allow me
to utilize the lisp reader facilities, but not make the data appear
to criptic for a non-lisp maintainer to be able to make minor
manual edits to the file, or at minimum, view it and understand
the data in it.  I feel XML is too verbose and seems to require
a fairly large, not-very-easy-to-use library, at least for this
particular purpose.


The current (non-lisp) file format looks something like this:

[File]
    Version=1.5
    Name=my_data

[Section 1]
    Name="Section name 1"

    [Feature 1]
        Id=123
        Value="hi there 1 - 1"

    [Feature 2]
        Id=456
        Value="so long"

[Section 2]
    Name="Section name 2"

    [Feature 1]
        Id=123
        Value="hi there 2 - 1"

In this example, there are multiple sections with both key value
pairs and other sections under them.  The current program reads
and parses out the sections (stuff with [brackets]) and the
key value pairs, and puts them in internal arrays and variables that
allow the user to find out how many Features are under Section 1,
and then cycle through the array to pull out the key value pairs
that fall under each [Section].

For example the following call would set x to "hi there 2 - 1".
x = GetValue("[Section 2].[Feature 1]:Value")

The notation is arbitrary and doesn't really matter, but there
has to be a way to specify where in the data tree you want to
look, and how many of the entries you want to get, if there
is more than one.

But to do that parsing and processing is ugly and hard to maintain,
as it is in a scripting language that does not have much in the way
of data storage and searching facilities.  I have created a Common
Lisp plug-in that I can now use to give me a more robust data handling
capablility. However, being new to Lisp, I haven't been able to come
up with a sensible Lispy method or data format that allows
that would allow easy fetching of both individual values and
groups of values, for example, all of the "Id" values under [Section 1].

Any suggestions would be welcome.

Rodney

From: Yuji Minejima
Subject: Re: Newbee: Lispy alternatives for configuration information file
Date: 
Message-ID: <pan.2004.08.05.07.39.31.924605@nifty.ne.jp>
How about using a property list (plist for short).

(setq plist '(:file (:version 1.5 :name my_data)
              :section-1 (:name "Section name 1"
                          :feature-1 (:id 123 :value "hi there 1 - 1")
                          :feature-2 (:id 456 :value "so long"))
              :section-2 (:name "Section name 2"
                          :feature-1 (:id 123 :value "hi there 2 - 1"))))

(getf (getf (getf plist :section-2) :feature-1) :value) => "hi there 2 - 1"

In a case you don't like typing lots of `getf',

(defun get-value (plist keys)
  (reduce #'(lambda (plist k) (getf plist k)) keys :initial-value plist))
(get-value plist '(:section-2 :feature-1 :value)) => "hi there 2 - 1"

To collect values of the same key,
(defun collect-values (plist key)
  (loop for (key0 value) on plist by #'cddr
        if (eq key0 key) collect value
        else if (consp value) append (collect-values value key)))

(collect-values (getf plist :section-1) :id) => (123 456)

The code is not tested but you get the idea.
Hope this helps.

Yuji.
From: Rodney
Subject: Re: Newbee: Lispy alternatives for configuration information file
Date: 
Message-ID: <u9FQc.25790$Qp.9216@twister.nyroc.rr.com>
"Yuji Minejima" <········@nifty.ne.jp> wrote in message
···································@nifty.ne.jp...
> How about using a property list (plist for short).
>
> (setq plist '(:file (:version 1.5 :name my_data)
>               :section-1 (:name "Section name 1"
>                           :feature-1 (:id 123 :value "hi there 1 - 1")
>                           :feature-2 (:id 456 :value "so long"))
>               :section-2 (:name "Section name 2"
>                           :feature-1 (:id 123 :value "hi there 2 - 1"))))
>
> (getf (getf (getf plist :section-2) :feature-1) :value) => "hi there 2 -
1"
>
> In a case you don't like typing lots of `getf',
>
> (defun get-value (plist keys)
>   (reduce #'(lambda (plist k) (getf plist k)) keys :initial-value plist))
> (get-value plist '(:section-2 :feature-1 :value)) => "hi there 2 - 1"
>
> To collect values of the same key,
> (defun collect-values (plist key)
>   (loop for (key0 value) on plist by #'cddr
>         if (eq key0 key) collect value
>         else if (consp value) append (collect-values value key)))
>
> (collect-values (getf plist :section-1) :id) => (123 456)
>
> The code is not tested but you get the idea.
> Hope this helps.
>
> Yuji.

Oh, very nice, Yuji,  thank you.  I had attempted using a simple property
list,
but got stuck when trying to navigate down a nested entry.  I wish you
could see the 10 pages of (non-lisp) scripting code that your 7 line
example replaces!

This has given me a great place to start.  And it should only be 4 or 5 more
hours before I figure out how it works ;-)

So many choices...So little time....

Rodney
From: Yuji Minejima
Subject: Re: Newbee: Lispy alternatives for configuration information file
Date: 
Message-ID: <pan.2004.08.06.07.46.47.675234@nifty.ne.jp>
On Fri, 06 Aug 2004 06:02:02 +0000, Rodney wrote:

> This has given me a great place to start.  And it should only be 4 or 5 more
> hours before I figure out how it works ;-)

You mean you're not familiar with the extended loop facility used in
collect-values?  Some people hate it but I think it's very powerful and
even beautiful sometimes.

I'd like to add one more benefit of using plists.
You can bind lots of values of particular fields in a plist to keyword
parameters when you call a function and that eliminates the necessity of
mumbling `getf' again and agan in the function body.

First define a function like this.
(defun foo (&rest plist &key id value &allow-other-keys)
  (list id value))

Then call it using `apply' with a plist as keyword arguments.
(apply #'foo (get-value plist '(:section-1 :feature-1)))
=>(123 "hi there 1 - 1")

Yuji.
From: Kalle Olavi Niemitalo
Subject: Re: Newbee: Lispy alternatives for configuration information file
Date: 
Message-ID: <87k6wcxamu.fsf@Astalo.kon.iki.fi>
Yuji Minejima <········@nifty.ne.jp> writes:

> First define a function like this.
> (defun foo (&rest plist &key id value &allow-other-keys)
>   (list id value))
>
> Then call it using `apply' with a plist as keyword arguments.
> (apply #'foo (get-value plist '(:section-1 :feature-1)))
> =>(123 "hi there 1 - 1")

Or use DESTRUCTURING-BIND.

  (destructuring-bind (&key id value &allow-other-keys)
      (get-value plist '(:section-1 :feature-1))
    (list id value))

If you like hairy lambda lists, you can even do the whole
traversal with this.

  (destructuring-bind (&key ((:section-1
                              (&key ((:feature-1
                                      (&key id value
                                       &allow-other-keys)))
                               &allow-other-keys)))
                       &allow-other-keys)
      plist
    (list id value))

The syntax is rather unreadable, but a macro will fix that, at
the cost of losing the ability to specify defaults:

  (defmacro plist-bind (pattern expression &body body)
    ;; TODO: document
    (labels ((transform-pattern (pattern)
               (etypecase pattern
                 (list          ; plist of indicators and subpatterns
                  `(&key
                    ;; TODO: signal error if pattern has odd length
                    ,@(loop for (indicator subpattern) on pattern by #'cddr
                            collect `((,indicator
                                       ,(transform-pattern subpattern))))
                    &allow-other-keys))
                 ;; TODO: signal error if one of lambda-list-keywords
                 (symbol        ; variable
                  pattern))))
      `(destructuring-bind ,(transform-pattern pattern) ,expression
         ,@body)))

  (plist-bind (:section-1 (:feature-1 (:id id :value value)))
      plist
    (list id value))

In SBCL 0.8.12.34, the macroexpansion of the plist-bind form
looks much more complicated than what could be done with a simple
series of GETFs.  CLHS section 3.5.1.6 apparently requires
DESTRUCTURING-BIND to check that the property list has an even
length, but the rest of the code could be pared down, I think.

Here's another macro with the same interface but a much more
straightforward expansion:

  (defmacro plist-bind (pattern expression &body body)
    ;; TODO: document
    (let* ((bindings `()))
      (labels ((process-pattern (pattern expression)
                 (etypecase pattern
                   (list        ; plist of indicators and subpatterns
                    (loop with plist = (gensym "PLIST")
                          initially (push `(,plist ,expression) bindings)
                          ;; TODO: signal error if pattern has odd length
                          for (indicator subpattern) on pattern by #'cddr
                          do (process-pattern subpattern
                                              `(getf ,plist ',indicator))))
                   ;; TODO: signal error if one of lambda-list-keywords
                   (symbol      ; variable
                    (push `(,pattern ,expression) bindings)))))
        (process-pattern pattern expression)
        `(let* ,(nreverse bindings)
           ,@body))))
From: Yuji Minejima
Subject: Re: Newbee: Lispy alternatives for configuration information file
Date: 
Message-ID: <pan.2004.08.07.02.05.54.328126@nifty.ne.jp>
On Fri, 06 Aug 2004 23:41:13 +0300, Kalle Olavi Niemitalo wrote:

> Or use DESTRUCTURING-BIND.
> 
>   (destructuring-bind (&key id value &allow-other-keys)
>       (get-value plist '(:section-1 :feature-1))
>     (list id value))
Oh, I didn't notice this.  This is another useful technique I'd like
to use. Thank you.


By the way, it's rather a matter of taste, but I'd like to add
something to my first post that get-value can be defined as
follows, changing the lambda list,
(defun get-value (plist &rest keys)
  (reduce #'(lambda (plist k) (getf plist k)) keys :initial-value plist))
then you can now stlip off parentheses around the keys when you call it.
(get-value plist :section-2 :feature-1 :value)=>"hi there 2 - 1"
And when you have a list of keys,
(apply #'get-value plist keys)


I'm really impressed by your plist-bind.

Yuji.
From: Kalle Olavi Niemitalo
Subject: Re: Newbee: Lispy alternatives for configuration information file
Date: 
Message-ID: <87brhnxtrl.fsf@Astalo.kon.iki.fi>
Yuji Minejima <········@nifty.ne.jp> writes:

> (defun get-value (plist &rest keys)
>   (reduce #'(lambda (plist k) (getf plist k)) keys :initial-value plist))

I wonder if it would be reasonable to shorten this to:

  (defun get-value (plist &rest keys)
    (reduce #'getf keys :initial-value plist))

This version is shorter, and potentially faster if neither REDUCE
nor GETF is inlined.  On the other hand, it is now less obvious
that GETF will always be called with exactly two arguments.

> I'm really impressed by your plist-bind.

Here's a related setf expander.  It has some code that tries to
keep the expansion simple in the most common cases;
unfortunately, this in turn makes the definition more complex.

  (defun plist (&rest indicators-and-values)
    indicators-and-values)

  (define-setf-expander plist (&rest indicators-and-places &environment env)
    ;; TODO: document
    (flet ((make-single-value-bind (stores form &rest body)
             "Generate a MULTIPLE-VALUE-BIND form or something simpler."
             (if (= (length stores) 1)
               `(let ((,(first stores) ,form)) ,@body)
               `(multiple-value-bind (,@stores) (values ,form) ,@body))))
      (let* ((rev-vars (list))
             (rev-vals (list))
             (store (gensym "PLIST"))
             (rev-writer (list 'progn))
             (rev-reader (list 'list)))
        ;; TODO: signal error if pattern has odd length
        (loop for (indicator place) on indicators-and-places by #'cddr
              for indicator-var = (if (constantp indicator env)
                                     indicator
                                     (gensym "INDICATOR"))
              do (multiple-value-bind (i-vars i-vals i-stores
                                       i-writer i-reader)
                     (get-setf-expansion place env)
                   (unless (eql indicator-var indicator)
                     (push indicator-var rev-vars)
                     (push indicator     rev-vals))
                   (dolist (i-var i-vars) (push i-var rev-vars))
                   (dolist (i-val i-vals) (push i-val rev-vals))
                   (push (make-single-value-bind
                          i-stores `(getf ,store ,indicator-var) i-writer)
                         rev-writer)
                   (push indicator-var rev-reader)
                   (push i-reader rev-reader)))
        (push store rev-writer)
        (values (nreverse rev-vars)
                (nreverse rev-vals)
                (list store)
                (nreverse rev-writer)
                (nreverse rev-reader)))))

  (let (id value)
    (setf (plist :section-1 (plist :feature-1 (plist :id id
                                                     :value value)))
          plist)
    (list id value))
From: Yuji Minejima
Subject: Re: Newbee: Lispy alternatives for configuration information file
Date: 
Message-ID: <pan.2004.08.08.02.18.37.767771@nifty.ne.jp>
On Sat, 07 Aug 2004 11:00:14 +0300, Kalle Olavi Niemitalo wrote:

> I wonder if it would be reasonable to shorten this to:
> 
>   (defun get-value (plist &rest keys)
>     (reduce #'getf keys :initial-value plist))
> 
> This version is shorter, and potentially faster if neither REDUCE
> nor GETF is inlined.  On the other hand, it is now less obvious
> that GETF will always be called with exactly two arguments.
I prefer your shorter one.  I got satisfied with the fact I used reduce
and didn't try to see if I could make it more concise.
 
> Here's a related setf expander.  It has some code that tries to keep the
> expansion simple in the most common cases; unfortunately, this in turn
> makes the definition more complex.
Let me call you The Plist Master. :-)

Yuji.