【问题标题】:Can a Lisp read procedure read this and how?Lisp 读取程序可以读取这个吗?如何读取?
【发布时间】:2016-04-07 10:14:41
【问题描述】:

我正在编写一个我打算在 Lisp 读取过程中实现的语法,即从可变的输入源一次读取一个表达式。大部分语法和 Lisp 一样,但有两个相关的变化是:

空白被读取并且是结果语法的一部分。连续的空白被分组在一起,就像连续的非空白字符被分组为标识符一样,读取这样一个字符串的结果是一个“空白对象”,它存储读取的字符的确切序列。当空白对象出现在列表中时,求值器会忽略它们(换句话说,如果 foo 是空白对象,则 (eval '(+ 3 foo 4)) 等效于 (eval '(+ 3 4))),如果要求它直接求值,它就是自求值。

其次,如果除空白标记之外的多个标记出现在同一行,这些标记将被收集到一个列表中,该列表就是读取的结果。

例如,

+ 3 4 5
(+ 3 4 5)
+ 3 4 (+ 1 4)
(+ 3 4 (+ 1 4))

都产生值 12。

是否可以将此阅读器实现为遵循读取过程的典型期望的 Lisp 读取过程?如果是这样,怎么做? (我很茫然。)

编辑:澄清空白:

如果我们说“空白对象”只是一个字符串并被读取,那么reading 以下段:

(foo bar   baz)

产生一个语法对象,如:

'(foo " " bar "   " baz)

换句话说,记号之间的空格存储在生成的语法对象中。

假设我写了一个名为 -> 的宏,它接受一个语法对象(方案风格的宏),而whitespace? 是一个识别空格语法对象的谓词

(define-macro (-> stx)
  (let* ((stxl (syntax-object->list stx))
         (obj (car stxl))
     (let proc ((res empty))
                (lst (cdr stxl)))
       (let ((method (car lst)))
          (if (whitespace? method)
              ; skip whitespace, recur immediately
              (proc res (cdr lst))
              ; Insert obj as the second element in method
              (let ((modified-method (cons (car method)
                                           (cons obj (cdr method)))))
                ; recur
                (proc (cons res modified-method) (cdr lst))))))))

【问题讨论】:

  • 我不确定您所说的“空白已被读取并且是结果语法的一部分”是什么意思。你能提供一个例子来说明这方面吗?
  • 我想你在这里要求一些不同的东西。我认为,词法分析/解析部分相对简单。您只需要编写一个词法分析器来生成您要查找的标记,然后编写将它们组合成结果的解析器。你后来关于基于行的输入的事情听起来像是一个不同的函数,你会在其中执行 read-line 然后从行中读取所有表达式,如果有多个表达式,或者只有一个,但请注意列表,您会将结果放入列表中。但是,您的评估者需要是一个真正的新事物。
  • 我添加了一个示例。空白对象类似于符号,不同之处在于它仅包含空白字符,并且评估器会忽略它而不是查找符号。
  • 关于评估器,您提到评估期间会忽略“空白对象”。 '(<whitespace object>) 之类的会发生什么?那是空列表吗?是函数调用吗?现在,突然之间,你的表情中出现了一些奇怪的东西。 (eval '<whitespace-object>) 返回什么? (eval '(list <whitespace-object>)) 怎么样?
  • 是的,我知道我能做到,我的问题是它是否可以按照 lisp 读取过程的典型约定来完成(即从可变输入流中一次读取一个表达式)?

标签: lisp grammar reader


【解决方案1】:

这部分的阅读非常简单。您只需要一个空白测试,然后您的阅读功能将安装一个自定义阅读器字符宏,该宏会检测空白并将连续的空白序列读取到单个对象中。首先,空白测试和空白对象;这些很简单:

(defparameter *whitespace*
  #(#\space #\tab #\return #\newline)
  "A vector of whitespace characters.")

(defun whitespace-p (char)
  "Returns true if CHAR is in *WHITESPACE*."
  (find char *whitespace* :test 'char=))

(defstruct whitespace-object
  characters)

现在宏字符功能:

(defun whitespace-macro-char (stream char)
  "A macro character function that consumes characters from
stream (including CHAR), until a non-whitespace character (or end of
file) is encountered.  Returns a whitespace-object whose characters
slot contains a string of the whitespace characters."
  (let ((chars (loop for c = (peek-char nil stream nil #\a)
                  while (whitespace-p c)
                  collect (read-char stream))))
    (make-whitespace-object
     :characters (coerce (list* char chars) 'string))))

现在 read 函数和普通的 read 签名相同,但是复制 readtable,然后安装宏函数,并调用 readread 的结果被返回,readtable 被恢复:

(defun xread (&optional (stream *standard-input*) (eof-error-p t) eof-value recursive-p)
  "Like READ, but called with *READTABLE* bound to a readtable in
which each whitespace characters (that is, each character in
*WHITESPACE*) is a macro characters whose macro function is
WHITESPACE-MACRO-CHAR."
  (let ((rt (copy-readtable)))
    (map nil (lambda (wchar)
               (set-macro-character wchar #'whitespace-macro-char))
         *whitespace*)
    (unwind-protect (read stream eof-error-p eof-value recursive-p)
      (setf *readtable* rt))))

例子:

(with-input-from-string (in "(+ 1    2  (* 3    
                                         4))")
  (xread in))

(+ #S(WHITESPACE-OBJECT :CHARACTERS " ") 1
   #S(WHITESPACE-OBJECT :CHARACTERS "    ") 2
   #S(WHITESPACE-OBJECT :CHARACTERS "  ")
   (* #S(WHITESPACE-OBJECT :CHARACTERS " ") 3
      #S(WHITESPACE-OBJECT
         :CHARACTERS "  
                                         ")
      4))

现在,要实现您想要的 eval 对应项,您需要能够从列表中删除空白对象。这并不难,我们可以编写一个更通用的实用函数来为我们做这件事:

(defun remove-element-if (predicate tree)
  "Returns a new tree like TREE, but which contains no elements in an
element position which ssatisfy PREDICATE.  An element is in element
position if it is the car of some cons cell in TREE."
  (if (not (consp tree))
      tree
      (if (funcall predicate (car tree))
          (remove-element-if predicate (cdr tree))
          (cons (remove-element-if predicate (car tree))
                (remove-element-if predicate (cdr tree))))))
CL-USER> (remove-element-if (lambda (x) (and (numberp x) (evenp x))) '(+ 1 2 3 4))
(+ 1 3)
CL-USER> (with-input-from-string (in "(+ 1  2 (* 3
                                                 4))")
           (remove-element-if 'whitespace-object-p (xread in)))
(+ 1 2 (* 3 4))

所以现在评估函数是 eval 的简单包装器:

(defun xeval (form)
  (eval (remove-element-if 'whitespace-object-p form)))
CL-USER> (with-input-from-string (in "(+ 1  2 (* 3
                                                 4))")
           (xeval (xread in)))
15

让我们确保独立的空白对象仍然按预期显示:

CL-USER> (with-input-from-string (in "       ")
           (let* ((exp (xread in))
                  (val (xeval exp)))
             (values exp val)))
#S(WHITESPACE-OBJECT :CHARACTERS "       ")
#S(WHITESPACE-OBJECT :CHARACTERS "       ")

【讨论】:

  • 是的,当您尝试将其与“一行上的多个元素形成一个列表”规则结合起来时,就会出现问题。要阅读这样的规则,我会阅读空格,然后阅读“实心”标记,然后再次阅读空格并查找行尾。如果 peek char 不是空格,那么我们有一个列表,我可以读取该列表而不会违反“每次读取一个表达式”规则,但如果不是,那么我已经阅读了两个表达式:“solid”标记和“空白”令牌。是否有可能同时执行这两个规则?即使使用完全自定义的读取过程,也可以进行可读修改。
  • @MattG 我真的不知道该怎么做 “其次,如果除空白标记之外的多个标记出现在同一行,这些标记将被收集到一个列表中,该列表是读取的结果。” 当单个表达式跨越多行时会发生什么?例如,像 (op <newline> a b <newline> c d) 这样的东西应该读作 (op (a b) c d) 吗?或((op) (ab) (c d))?您的示例均未显示任何多行输入,因此我假设逐行处理是针对其他内容的,其中您只需使用 read-line 读取一行,然后调用 xread 来收集跨度>
  • 表达式。如果只有一个表达式,那么如果它是一个列表,如果不是,则返回一个包含它的列表。如果有多个表达式,则返回所有表达式的列表。
  • 为了有一个多行表达式,你必须使用括号。如果列表已打开但在行尾尚未关闭,则继续读取列表直到关闭。所以foo bar \n baz qux(foo bar) \n (baz qux)foo (bar \n baz) qux(foo (bar baz) qux)foo (bar \n baz) \n qux(foo (bar baz)) (qux)
  • @Matt 好的,所以当 xread 启动时,它必须读取表达式,直到遇到换行符或 EOF。那正确吗?例如,。如果你看到foo \n bar,你可以停在\n。如果您看到foo (bar \n baz) quux \n frob。你会读到foo,然后是bar \n \baz,然后是quux,然后点击换行符,所以你会留下(foo (bar baz quux))。我不认为这太难了。让我试一试。
猜你喜欢
  • 1970-01-01
  • 2021-02-23
  • 1970-01-01
  • 2012-01-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-04-08
  • 1970-01-01
相关资源
最近更新 更多