1

I'm trying to build a hash table (among other actions) while reading. I don't want the hash table to have global scope (yet), so I'm doing this with a macro and gensym. Inside the macro x, I'm defining a macro s which is similar to setf, but defines an entry in a hash table instead of defining a symbol somewhere. It blows up. I think I understand the error message, but how do I make it work?

The code:

#!/usr/bin/clisp -repl

(defmacro x (&rest statements)
  (let ((config-variables (gensym)))
    `(macrolet ((s (place value)
                  (setf (gethash 'place ,config-variables) value)))
       (let ((,config-variables (make-hash-table :test #'eq)))
         (progn ,@statements)
         ,config-variables))))

(defun load-config ()
  (let ((config-file-tree (read *standard-input*)))
    (eval config-file-tree)))

(defun load-test-config ()
  (with-input-from-string (*standard-input* "(x (s fred 3) (s barney 5))")
    (load-config)))

(load-test-config)

The output:

*** - LET*: variable #:G12655 has no value
The following restarts are available:
USE-VALUE      :R1      Input a value to be used instead of #:G12655.
STORE-VALUE    :R2      Input a new value for #:G12655.
SKIP           :R3      skip (LOAD-TEST-CONFIG)
STOP           :R4      stop loading file /u/asterisk/semicolon/build.l/stackoverflow-semi
Bill Evans at Mariposa
  • 3,590
  • 1
  • 18
  • 22
  • This looks so complicated that I would bet that you don't need anything of that. A macro, a macrolet, EVALuation of read data, single letter macro names, ... – Rainer Joswig Dec 25 '11 at 13:19
  • Ordinarily I'd agree: it's way too complicated. But it's part of something more involved. I distilled it down to a bite-size situation which made the question easier to ask. – Bill Evans at Mariposa Dec 25 '11 at 13:43
  • Why would you want to encode a hash table as Lisp source code? – Rainer Joswig Dec 25 '11 at 13:58
  • If it were only a static hash table, I wouldn't. Some of the values in this hash table will be lambda expressions which may be called upon to set the value of (for example) fred, at some point after the eval has been long completed. – Bill Evans at Mariposa Dec 25 '11 at 14:41

2 Answers2

4

Just guessing what Bill might really want.

Let's say he wants a mapping from some keys to some values as a configuration in a file.

Here is the procedural way.

  • open a stream to the data
  • read it as an s-expression
  • walk the data and fill a hash-table

Example code:

(defun read-mapping (&optional (stream *standard-input*))
  (destructuring-bind (type &rest mappings) (read stream)
    (assert (eq type 'mapping))
    (let ((table (make-hash-table)))
      (loop for (key value) in mappings
            do (setf (gethash key table) value))
      table)))

(defun load-config ()
  (read-mapping))

(defun load-test-config ()
  (with-input-from-string (*standard-input* "(mapping (fred 3) (barney 5))")
    (load-config)))

(load-test-config)

Use:

CL-USER 57 > (load-test-config)
#<EQL Hash Table{2} 402000151B>

CL-USER 58 > (describe *)

#<EQL Hash Table{2} 402000151B> is a HASH-TABLE
BARNEY      5
FRED        3

Advantages:

  • no macros
  • data does not get encoded in source code and generated source code
  • no evaluation (security!) via EVAL needed
  • no object code bloat via macros which are expanding to larger code
  • functional abstraction
  • much easier to understand and debug

Alternatively I would write a read-macro for { such that {(fred 3) (barney 5)} would be directly read as an hash-table.


If you want to have computed values:

(defun make-table (mappings &aux (table (make-hash-table)))
  (loop for (key value) in mappings
        do (setf (gethash key table) (eval value)))
  table)

CL-USER 66> (describe (make-table '((fred (- 10 7)) (barney (- 10 5)))))

#<EQL Hash Table{2} 4020000A4B> is a HASH-TABLE
BARNEY      5
FRED        3

Turning that into a macro:

(defmacro defmapping (&body mappings)
  `(make-table ',mappings))

(defmapping
  (fred 3)
  (barney 5))
Rainer Joswig
  • 136,269
  • 10
  • 221
  • 346
  • Most instructive, thank you. It turns out that I'm not doing a simple load of values from a configuration file. There will be real live LISP code in this configuration file; fred might get set to 3 sometimes, get set to 4 sometimes, and never get set at other times. When the expression is first evaluated, fred won't get set at all. Some of the values in the hash will be lambda expressions which might set fred based on time of day and other environmental considerations. – Bill Evans at Mariposa Dec 25 '11 at 14:31
  • Rainer, I'm still new at this. The samples of code I see here, like any exposure to a new foreign language, help me become more fluent in the language. So thank you very much! It turns out that the hash table won't all be defined at the same time. I might set fred today at 15:23, wilma tomorrow at 08:00, barney sometime next week, and betty perhaps never. – Bill Evans at Mariposa Dec 25 '11 at 17:37
  • 1
    @Bill Evans at Mariposa: what triggers that the various values are actually computed? Usually one would store a function, which computes the value on demand. Mostly this is used with CLOS (or similar) objects. – Rainer Joswig Dec 25 '11 at 18:06
  • Simplified: a string of digits comes in at some random time. Let's say the string is "31415926535". I want to see, for this string, whether fred is defined. First I check whether it's defined for 3. Then for 31. Then for 314, and so on. If it's defined for more than one of these, the later one takes precedence. To do this, I walk down a tree; a node on the tree _might_ point to a lambda expression in the hash table. Any of these lambda expressions might (or might not) define fred, again in the same hash table. – Bill Evans at Mariposa Dec 25 '11 at 18:33
  • 1
    Rainer, although I'm still using the S macro (and a G macro for pulling values out of the hash table), it turns out that your advice is quite apropos in my situation: there are other macros here for which I really have no use, and am removing for the reasons you mention, including (* drum roll *) the overall macro X! Thanks again! – Bill Evans at Mariposa Dec 28 '11 at 10:54
3

In a macrolet you are as well defining a macro, so the usual rules apply, i.e. you have to backquote expressions, that are to be evaluated at run-time. Like this:

(defmacro x (&rest statements)
  (let ((config-variables (gensym)))
    `(macrolet ((s (place value)
                 `(setf (gethash ',place ,',config-variables) ,value)))
      (let ((,config-variables (make-hash-table :test #'eq)))
        (progn ,@statements)
        ,config-variables))))
Vsevolod Dyomkin
  • 9,343
  • 2
  • 31
  • 36
  • Works perfectly, thanks. I'm puzzled by the single quote between the two commas in `,',config-variables`. I took out that single quote and it blew up. What's it doing there? – Bill Evans at Mariposa Dec 25 '11 at 13:42