Lisp #a.k.a.读取宏的使用

Lisp #a.k.a.读取宏的使用,lisp,common-lisp,reader-macro,Lisp,Common Lisp,Reader Macro,在读道格·霍伊特(Doug Hoyte)的书《放过兰姆达》(Let Over Lambda)时,我发现了对#符号的以下描述,即read macro: 公共LISP内置的基本读取宏是#。读取时间评估宏。此读取宏允许您将对象嵌入到所读取的表单中,这些表单无法序列化,但可以使用少量lisp代码创建 从第四章开始,本书的大部分内容可以在这里找到: 这是本书中的一个例子,展示了每次对同一表达式的解读可能会有所不同: * '(football-game (game-started-at

在读道格·霍伊特(Doug Hoyte)的书《放过兰姆达》(Let Over Lambda)时,我发现了对
#
符号的以下描述,即read macro:

公共LISP内置的基本读取宏是#。读取时间评估宏。此读取宏允许您将对象嵌入到所读取的表单中,这些表单无法序列化,但可以使用少量lisp代码创建

从第四章开始,本书的大部分内容可以在这里找到:

这是本书中的一个例子,展示了每次对同一表达式的解读可能会有所不同:

* '(football-game
     (game-started-at
       #.(get-internal-real-time))
     (coin-flip
       #.(if (zerop (random 2)) 'heads 'tails)))

(FOOTBALL-GAME
  (GAME-STARTED-AT 187)
  (COIN-FLIP HEADS))

* '(football-game
     (game-started-at
       #.(get-internal-real-time))
     (coin-flip
   #.(if (zerop (random 2)) 'heads 'tails)))

(FOOTBALL-GAME
  (GAME-STARTED-AT 309)
  (COIN-FLIP TAILS))
接下来,作者演示了一些核心技巧,使用
#
宏创建变体

因此,
#“
也是一种读取宏,通常在表示函数名称的符号之前使用。但是有必要吗?他在那里的具体工作是什么

我可以将符号放在有
#'
或没有它的高阶函数上:

CL-USER> (defun test nil t)
TEST
CL-USER> (funcall #'test)
T
CL-USER> (funcall 'test)
T

同样成功。

您可以通过两种方式调用函数的全局定义:

CL-USER> (defun test nil t)
TEST
CL-USER> (funcall #'test)
T
CL-USER> (funcall 'test)
T
但请看这个:

CL-USER 10 > (defun foo () 42)
FOO

CL-USER 11 > (flet ((foo () 82))
               (print (foo))
               (print (funcall 'foo))
               (print (funcall #'foo)))

82   ; the local definition
42   ; the symbol references the global definition
82   ; (function foo) / #'foo  references the local definition
(funcall'foo)
从符号中查找函数

(funcall#'foo)
从词法环境调用函数。如果没有,则使用全局定义

#'foo
(函数foo)
的简写符号


与大多数语言不同,通用Lisp并没有真正的解析器。它有一个称为读卡器的lexer。读取器使用单个字符并在表中查找它们,然后调用在表中找到的函数[1]。在Lisp中,其他语言中的解析器所扮演的角色由宏来完成

[1]

例如,分号的读取器使用行的其余部分并将其作为注释丢弃。例如,open paren的读者。调用递归读取列表元素的函数。例如,单引号递归地读取单个表单,然后将其包装在引号中。因此“(1 2 3)被理解为(引号(1 2 3))。这里有一大堆关键的复杂令牌读取器

[2]

字符
\\\\\
提供了一个放置大量额外读者行为的位置。哈希读取器重复主读取器的设计。它使用另一个字符并在表中查找该字符并调用在那里找到的函数。其中有很多

[3]

例如,我们有一个类似于列表的读取器,它读取向量,例如#(1,2,3)。例如,我们有一个单个字符的读卡器,您可以输入一个分号、双引号或句点作为
\
\\”
\.
分别

回答您的特定问题:quote的哈希读取器,例如#'foo,与常规quote的哈希读取器类似。它读取以下标记并将其包装在函数中。#'foo被读取为(函数foo)

可以修改读卡器表以自定义语言。表中的条目称为读卡器宏。这个名称往往会让人感到困惑,因为它们与defmacro定义的宏截然不同。它们一起提供了所谓的“增长”语言的能力[4]


[4]

使用#'foo和'foo作为函数指示符的另一个区别是#'foo计算为函数对象,而'foo计算为符号。因此,使用'foo将查找函数对象的工作转移到以后。如果在循环的每个周期中而不是仅在一次中进行,这可能会对性能造成明显的影响

CL-USER> (defun foo () 42)
FOO
CL-USER> (read-from-string "'foo")
=> (QUOTE FOO), 4

CL-USER> (eval *)
FOO
CL-USER> (read-from-string "#'foo")
=> (FUNCTION FOO), 5

CL-USER> (eval *)
=> #<FUNCTION FOO>
CL-USER>(defun foo()42)
福
CL-USER>(从字符串“foo”读取)
=>(引用FOO),4
CL-USER>(评估*)
福
CL-USER>(从字符串“#'foo”读取)
=>(函数FOO),5
CL-USER>(评估*)
=> #

经验告诉我,在一个由许多部分组成的大系统中,“'”与“#”习惯用法使修补更容易。
原因是与符号关联的函数对象在每次遇到时都会被查找,这在每个环境中都会发生。一旦(当然是以交互方式)加载了新定义(很可能是修补程序),就会立即查找下次遇到它时会使用它。性能成本非常小,但灵活性的优势是巨大的。当再次尝试应用程序时,想象客户的表情说“哇!现在可以了!“:-)

多全面的回答!谢谢,@Ben!
CL-USER> (defun foo () 42)
FOO
CL-USER> (read-from-string "'foo")
=> (QUOTE FOO), 4

CL-USER> (eval *)
FOO
CL-USER> (read-from-string "#'foo")
=> (FUNCTION FOO), 5

CL-USER> (eval *)
=> #<FUNCTION FOO>