【问题标题】:On using structure literals in function and macro definitions关于在函数和宏定义中使用结构字面量
【发布时间】:2022-01-02 22:11:35
【问题描述】:

重要提示:这个问题完全是因为我想学习 Common Lisp。除了这个自学目标之外,它并没有不可告人的实际目的。


如果在我的 REPL1 中,我评估以下表达式

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

...我得到T。换句话说,字面量

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

...计算为有效的random-state 对象。


到目前为止一切顺利。

现在,我尝试定义一个函数silly-random-state,以便表达式(silly-random-state 7) 将产生与上面文字产生的相同的random-state 对象,如下所示:

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

...但是我的 REPL 将没有它!它甚至不会评估这个 defun!它抱怨some-int 未定义2

进一步思考,我想这是有道理的这个宏?

然后我尝试定义一个,给定一个整数参数,它将扩展为这样的random-state 对象,如下所示:

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

再一次,我的 REPL 将没有它。上面表达式的评估失败了

反引号结构中的逗号(不是列表或一般向量。)


上述两个失败中的每一个都会导致一个相应的问题:

  1. 如何定义一个函数silly-random-state,它将整数SOME-INT 作为参数,并返回random-state,如果我在将PLACEHOLDER替换为整数SOME-INT?
#S(random-state
     :state #.(make-array 627
                          :element-type '(unsigned-byte 32)
                          :initial-element PLACEHOLDER))
  1. 如何定义一个 silly-random-state-macro 以使(silly-random-state-macro some-int) 扩展为与 (1) 中描述的相同的 random-state 对象?

(重复一遍,我的动机只是为了更好地了解 Common Lisp3 能做什么和不能做什么。)


1 SBCL + SLIME/Emacs 在 Darwin 上运行。

2 顺便说一句,我的 REPL 从来没有这么挑剔过!例如,它将评估类似(defun foo () undefined-nonsense) 的内容。当然,它确实对正文中未定义的变量有点不满,但最终它评估defun

3 事实上,我曾争论是否应该在这个问题的标签中包含 [prng],因为它的主题实际上是文字、函数和宏,以及 CL 的作用其中的默认 PRNG 只是偶然的。我最终选择保留这个标签,以防 PRNG 在问题中的作用不像我认为的那么偶然。


编辑:好的,我想我找到了一种方法来定义我在(1)中描述的函数;它不是很优雅,但很有效:

(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)))

【问题讨论】:

  • 阅读器宏#. 表示在阅读时进行评估。在初始情况下,值是在读取时指定的,是固定值。当您将示例转换为 defun 时,some-int 的值在读取时未绑定。只有在调用函数时才绑定。

标签: function random macros common-lisp literals


【解决方案1】:

结构 相当棘手。它们有很多选项,它们旨在支持快速代码。它们也不是为了内省而设计的,尽管一些 Common Lisp 实现可能支持这一点。例如,在运行时可能不知道结构有哪些插槽。

结构字面量也没有反引号语法

随机状态是SBCL中的一个结构。有一个插槽state,但它是只读的。

该结构还定义了一个特殊的构造函数。

然后我们可以创建一个:

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

您使用 read-from-string 的解决方案没问题。使用#. 的读取时评估的一个原因是专用向量没有精确的打印表示。意味着,没有办法在向量的打印表示中表示元素类型。

【讨论】:

  • 谢谢!通过这个练习,我对定义扩展为文字的宏的一般问题特别好奇,特别是结构文字。我是否应该从您的回答中得知无法编写扩展为我在帖子中描述的文字的宏?
【解决方案2】:

您的问题是关于结构文字的,但依赖于一个不幸的示例(随机状态)。您通常可以在代码中编写结构文字,但正如另一个答案中所说,没有办法在结构文字中使用反引号。如果我尝试这样做,则会出现错误:

反引号结构中的逗号(不是列表或一般向量。)

您必须围绕这些结构使用构造函数或更高级别的 API。

注入值的唯一方法是读取时评估,但正如您所注意到的,您需要处于读取阶段,因此使用read-from-string。这行得通,但正如你所说,不是很优雅。

此外,这里您已经依赖于random-state 的特定表示,因此您不妨使用SBCL 提供的扩展,即sb-ext:seed-random-state

因此,您可以编写一个在给定占位符值的情况下构建随机状态的函数:

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 ...)))

现在您有了一个不依赖于读取时宏的常规函数​​,一旦将它加载到您的环境中,您就可以在不同的评估点、读取时、宏中等调用它。这是不特定于随机状态,一般你应该尝试编写常规函数,然后才选择它们何时执行

但在这里你必须小心,因为随机状态在使用时会发生变异。

在以下示例中,我绑定*random-state* 以强制使用random 的特定状态(我也可以将其直接传递给random,这在这里并不重要)。结果函数被多次调用以显示结果如何变化。

这里的函数每次都重新创建一个新的状态,结果总是一样的:

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)

下面,由于读取时评估,一个状态文字被计算并注入到源代码中,但请注意,随着时间的推移会产生不同的值:

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)
                

这是因为random 改变了状态。但是由于状态现在是文字,我们进入了未定义行为的领域。这在3.7.1 Modification of Literal Objects 中非常明确:

如果文字对象被破坏性修改,后果是不确定的。为此,以下操作被认为是破坏性的:

random-state:将其用作random函数的参数。

所以你不应该试图在代码中包含这样的文字对象。

加载时间值

同样,您可以尝试使用LOAD-TIME-VALUE 将随机状态播种到特定状态。下面我定义了roll-1d6,它选择了一个从 1 到 6(含)的数字。目的是无论全局随机状态如何,骰子在加载程序时始终表现相同:

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)

如果我重新定义它(以不同但等效的方式),当我重新加载函数时,状态会被重置,并计算相同的数字:

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)

但是,即使load-time-value 允许生成的对象是可变的:

如果 [read-only-p is] nil(默认值),则结果既不能复制也不能合并;它必须被视为潜在的可修改数据。

它还说:

此编译时评估的结果在编译后的代码中被视为文字对象

还有:

如果两个在 equal 下相同但不相同的列表(加载时间值形式)被评估或编译,它们的值总是来自不同的形式评估。除非 read-only-p 是 t,否则它们的值可能不会被合并。

所以我不能 100% 确定示例是否定义明确,我认为如果有多个使用相同参数的调用,例如,编译器不允许构建和重用单个对象。 (silly-random-state 3),但结果对象是 random-state 类型的事实可能会对其进行进一步的操作 undefined (?),例如用户定义的类就不会出现这种情况。

别再放纵了

您还可以在函数中预先计算状态,如下所示:

(let ((state (silly-random-state 3)))
  (defun roll-1d6 ()
    (1+ (random 6) state)))

【讨论】:

  • 谢谢!我感到困惑的一件事是表达式(silly-random-state 7),其中silly-random-state 是你定义的,使用sb-ext:seed-random-state,计算出一个结构,其:state 槽与对应于文字的结构完全不同在我的帖子中。具体来说,后者是一个包含 627 个相同整数(全为 7)的数组,而前者的所有数组元素都是不同的。
  • 参见:github.com/sbcl/sbcl/blob/master/src/code/… 缓冲区与其他一些恒定的初始状态混合在一起;如果你想用给定的缓冲区构建一个结构,你应该直接调用 %make-random-state ,但这可能不是它应该如何工作的
  • 再次感谢您。我必须说 CL 的“播种”PRNG 的想法与我的经验中的其他任何东西都完全不同(我从 90 年代中期开始就一直在使用 PRNG,甚至在那时实施了一些)。我仍然不明白“播种”的概念,完全没有,但这将是另一个主题的主题。
猜你喜欢
  • 2020-11-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-09-05
相关资源
最近更新 更多