Home > Blockchain >  On using structure literals in function and macro definitions
On using structure literals in function and macro definitions

Time:11-25

IMPORTANT: This question is motivated solely by my wanting to learn Common Lisp. Beyond this autodidactic goal, it has no ulterior practical purpose.


If, at my REPL1, I evaluate the following expression

CL-USER> (random-state-p
  #S(random-state
       :state #.(make-array 627
                            :element-type '(unsigned-byte 32)
                            :initial-element 7)))

...I get T. In other words, the literal

#S(random-state
     :state #.(make-array 627
                          :element-type '(unsigned-byte 32)
                          :initial-element 7))

...evaluates to a valid random-state object.


So far so good.

Now, I try to define a function, silly-random-state, so that the expression (silly-random-state 7) will produce the same random-state object as that produced by the literal above, like this:

(defun silly-random-state (some-int)
  #S(random-state
       :state #.(make-array 627
                            :element-type '(unsigned-byte 32)
                            :initial-element some-int)))

...but my REPL will have none of it! It won't even evaluate this defun! It complains that some-int is undefined2.

On further thought, I guess it makes sense that one cannot stick a variable inside a literal and expect the resulting expression to make sense... Maybe I need a macro for this?

So then I try to define a macro that, given an integer argument, will expand to such a random-state object, like this:

(defmacro silly-random-state-macro (some-int)
  `#S(random-state
        :state #.(make-array 627
                             :element-type '(unsigned-byte 32)
                             :initial-element ,some-int)))

Again, my REPL will have none of it. The evaluation of the expression above fails with

Comma inside backquoted structure (not a list or general vector.)


Each of the two failures above leads to a corresponding question:

  1. how can I define a function silly-random-state that takes an integer SOME-INT as argument, and returns the random-state equal to the one the REPL would produce if I gave it the expression below after replacing PLACEHOLDER with the integer SOME-INT?
#S(random-state
     :state #.(make-array 627
                          :element-type '(unsigned-byte 32)
                          :initial-element PLACEHOLDER))
  1. how can I define a macro silly-random-state-macro such that (silly-random-state-macro some-int) expands to a the same random-state object described in (1)?

(To repeat, my motivation here is only to better understand what can and cannot be done with Common Lisp3.)


1 SBCL SLIME/Emacs running on Darwin.

2 BTW, my REPL had never been so picky before! For example, it will evaluate something like (defun foo () undefined-nonsense). Granted, it does grouse a bit over the undefined variable in the body, but in the end it will evaluate the defun.

3 In fact, I debated whether I should include [prng] among the tags for this question, since its topics are really literals, functions, and macros, and the role of CL's default PRNG in it is only incidental. I finally opted to keep this tag just in case the role of the PRNG in the question is less incidental than it seems to me.


EDIT: OK, I think I found a way to define the function I described in (1); it's not very elegant, but it works:

(defun silly-random-state (some-int)
  (read-from-string
   (format nil "#S(random-state :state #.(make-array 627 :element-type '(unsigned-byte 32) :initial-element ~A))" some-int)))

CodePudding user response:

structures are quite tricky. They have many options and they are designed to support fast code. They are also not designed to be introspective, even though some Common Lisp implementations might support that. For example at runtime one might not get to know what slots a structure has.

There is also no backquote syntax for structure literals.

The random state is a structure in SBCL. There is a slot state, but it is read only.

The structure also defines a special constructor function.

We can then create one:

(sb-kernel::%make-random-state
  (make-array 627
              :element-type '(unsigned-byte 32)
              :initial-element 7))

Your solution with read-from-string is okay. One reason for using read-time evaluation with #., is that the specialized vector has no exact printed representation. Means, that there is no way to represent the element-type in a printed representation of the vector.

CodePudding user response:

Your question is about structure literals but relies on a example that is however unfortunate (random-state). You can generally write structure literals in your code, but as said in another answer, there is no way to have backquotes inside structs literal. If I try to do so, I have an error:

Comma inside backquoted structure (not a list or general vector.)

You have to use constructors or maybe higher-level APIs around those structs.

The only way to inject values is read-time evaluation, but as you noticed, you need to be in a reading phase, hence the use of read-from-string. This works but as you said not very elegant.

Besides, here you are already depending on a particular representation of the random-state, so you might as well use the extensions offered by SBCL, namely sb-ext:seed-random-state.

This is thus how you could write a function that builds a random state given a placeholder value:

USER> (defun silly-random-state (some-int)
        (sb-ext:seed-random-state (make-array 627
                                              :element-type '(unsigned-byte 32)
                                              :initial-element some-int)))
SILLY-RANDOM-STATE
USER> (silly-random-state 5)
#S(RANDOM-STATE :STATE #.(MAKE-ARRAY 627 :ELEMENT-TYPE '(UNSIGNED-BYTE 32)
                                     :INITIAL-CONTENTS
                                     '(0 2567483615 624 2147483648 2465029273 ...)))

Now that you have a regular function that does not rely on read-time macros, once you have it loaded in your environment you can produce random states at various points of evaluation, at read-time, etc. but be careful, a random-state is mutated when used.

In the following examples I bind *random-state* to force the use of a specific state for random (I could also pass it directly to random, this is not important here). The resulting function is called multiple times to show how the result changes or not.

Here the function recreates a fresh state each time, the result is always the same:

USER> (lambda () (let ((*random-state* (silly-random-state 3)))
                   (random 10)))
#<FUNCTION (LAMBDA ()) {536DDB4B}>
USER> (loop repeat 5 collect (funcall *))
(7 7 7 7 7)

One state literal is computed and injected in the source code, thanks to read-time evaluation, but note however that different values are produced over time:

USER> (lambda () (let ((*random-state* #.(silly-random-state 3)))
                   (random 10)))
#<FUNCTION (LAMBDA ()) {536E5C7B}>
USER> (loop repeat 5 collect (funcall *))
(7 0 9 3 6)
                

This is due to the fact that random mutates the state. But since the state is now a literal, we entered the realm of undefined behavior. This is quite explicit in 3.7.1 Modification of Literal Objects:

The consequences are undefined if literal objects are destructively modified. For this purpose, the following operations are considered destructive:

random-state: using it as an argument to the function random.

So you shouldn't be trying to have such literal objects in code.

Alternatively, you can use load-time-value to seed the random state to a particular state. Here below I define roll-1d6 which picks a number from 1 to 6 (inclusive). The intent is that the dice always behaves the same when loading a program, regardless of the global random state:

USER> (defun roll-1d6 ()
      (let ((*random-state* (load-time-value (silly-random-state 3))))
        (1  (random 6))))
ROLL-1D6
USER> (roll-1d6)
5 (3 bits, #x5, #o5, #b101)
USER> (roll-1d6)
5 (3 bits, #x5, #o5, #b101)
USER> (roll-1d6)
1 (1 bit, #x1, #o1, #b1)
USER> (roll-1d6)
2 (2 bits, #x2, #o2, #b10)
USER> (roll-1d6)
4 (3 bits, #x4, #o4, #b100)

If I redefine it (in a different but equivalent way), the state is reset when I reload the function, and the same numbers are computed:

USER> (defun roll-1d6 () (1  (random 6 (load-time-value (silly-random-state 3)))))
WARNING: redefining PARROT.USER::ROLL-1D6 in DEFUN
ROLL-1D6
USER> (roll-1d6)
5 (3 bits, #x5, #o5, #b101)
USER> (roll-1d6)
5 (3 bits, #x5, #o5, #b101)
USER> (roll-1d6)
1 (1 bit, #x1, #o1, #b1)
USER> (roll-1d6)
2 (2 bits, #x2, #o2, #b10)
USER> (roll-1d6)
4 (3 bits, #x4, #o4, #b100)
  • Related