微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

将存储在 S 表达式中的数据读入另一个 Common Lisp 程序的内存中 读取文件访问卡片制作抽认卡结构警告结论

如何解决将存储在 S 表达式中的数据读入另一个 Common Lisp 程序的内存中 读取文件访问卡片制作抽认卡结构警告结论

我有一个 Common Lisp 程序,可以从 S 表达式文件中读取数据。最初该程序打算用 C 语言编写,并带有一个 CSV 文件,该文件被解析为内存中的结构,但我切换到 Common Lisp 和 S 表达式以消除程序员和用户间的抽象。如果我在 program.lisp 中定义了一个结构:

(defstruct flashcard
  front
  back
)

...和一个文件 data.lisp:

(:virology
  (
     (:card
        :front "To what sort of cells does Epstain-Barr virus attach?"
        :back "B-cells"      
     )
     (:card
        :front "For how long does the virus of herpes simplex persist in tissues?"
        :back "lifetime"
     )
     (:card
        :front "What T-cell receptors are recognised by HIV?"
        :back CD4
     )
  )

...里面有几个列表,很像 ":virology"。

我如何将 data.lisp 读入内存以便可以访问每张卡 来自program.lisp

对于上下文,我希望根据用户提供的过滤器(即它们列出:病毒学:生物化学:体内平衡等)迭代队列中的每张卡片,并在卡片上对用户进行测验。不过,我看不出这个特定用例会如何影响我将文件加载到内存中的方式。

(注意:就目前而言,只有两个字段的简单结构更有可能更适合 CSV 文件,但是,当我开发程​​序时,我打算添加元数据等。因此将数据存储在 Lisp 中从长远来看,由 program.lisp 读取的 S 表达式更有用)

解决方法

读取文件

假设你在 data.lisp 所在的目录中启动你的 Lisp 程序(例如 sbcl、ecl),那么这应该会产生一个值树(这里的 * 是 REPL 中的提示,什么以下是正在读取的值):

* (with-open-file (in "data.lisp")
    (read in))

(:VIROLOGY
 ((:CARD :FRONT "To what sort of cells does Epstain-Barr virus attach?" :BACK
   "B-cells")
  (:CARD :FRONT
   "For how long does the virus of herpes simplex persist in tissues?" :BACK
   "lifetime")
  (:CARD :FRONT "What T-cell receptors are recognised by HIV?" :BACK CD4)))

通常你会把它包装在一个函数中:

(defun cards (&optional (file "data.lisp"))
  (with-open-file (in file) (read in)))

假设您有多张卡片,请按如下方式编写:

(:virology (...) :biology (....) :physics (...))

如果改为编写多个列表,如下所示:

(:virology (...))
(:biology (...))
(:physics (...))

那么上面的cards需要循环:

(defun cards (&optional (file "data.lisp"))
  (with-open-file (in file)
    (loop 
      for item = (read in nil in)
      until (eq item in)
      collect item)))

上面有一个技巧,因为你想收集值直到没有更多值,但你不想在文件结束时抛出异常。这就是为什么 readnil 作为第二个参数(没有错误),第三个参数是它在到达文件结尾时应该返回的值。这里的值是流对象 in 本身:它用于拥有一个不可能由 read 生成的唯一值(与 nil 不同)。在您的情况下,这并不是真正必要的,您可以使用 nil 代替,但总的来说这是一个很好的做法。

访问卡片

如果您选择一种或另一种方式,则必须适应查询您的值。假设您使用上面的 loop,因此您的数据是单个条目列表,由循环收集:

((:virology (...))
 (:biology (...))
 (:physics (...)))

这就像一个关联列表,其中每个元素都是一个 cons-cell,这样 car 是一个键,而 cdr 是一个值。如果您拨打 (assoc :virology (cards)),您将:

(:VIROLOGY
 ((:CARD :FRONT "To what sort of cells does Epstain-Barr virus attach?" :BACK
   "B-cells")
  (:CARD :FRONT
   "For how long does the virus of herpes simplex persist in tissues?" :BACK
   "lifetime")
  (:CARD :FRONT "What T-cell receptors are recognised by HIV?" :BACK CD4)))

其中的 cdr 是一个具有单个值的列表,即卡片列表。

您可以简化数据格式,以便使用此格式:

(:virology (:card ...) (:card ...) (:card ...)) 

代替:

(:virology ((:card ...) (:card ...) (:card ...)))

这会删除一层嵌套,而不是包含一张卡片列表的列表,您可以直接访问卡片列表作为条目的 cdr。因此,假设您编辑 data.lisp 以删除一层嵌套,然后:

(defun find-cards (cards key)
  (cdr (assoc key cards)))

* (find-cards (cards) :virology)
((:CARD :FRONT "To what sort of cells does Epstain-Barr virus attach?" :BACK
  "B-cells")
 (:CARD :FRONT
  "For how long does the virus of herpes simplex persist in tissues?" :BACK
  "lifetime")
 (:CARD :FRONT "What T-cell receptors are recognised by HIV?" :BACK CD4))

到目前为止,还不错。

概括地说,我们有一个关联列表,将键映射到卡片列表。

如果您想改用哈希表,那么您可以自己填充一个,或者使用像 alexandria 这样的库。为此,您可能应该首先设置 Quicklisp:

* (ql:quickload :alexandria)
To load "alexandria":
  Load 1 ASDF system:
    alexandria
; Loading "alexandria"

(:ALEXANDRIA)

然后,您可以调用:

* (alexandria:alist-hash-table (cards))
#<HASH-TABLE :TEST EQL :COUNT 1 {1015268CE3}>

如果您到了这一步,您可以在 Peter 的 Seibel Practical Common Lisp 的第 11. Collections 章中查看哈希表的工作原理。

制作抽认卡结构

在任何情况下,无论您使用 find-cards 还是使用 GETHASH 访问卡片,您都会获得特定格式的卡片列表。如果要将它们转换为结构的实例,那么首先需要定义一种将卡片从列表格式转换为结构的方法。

每张卡片都存储在一个以 :card 开头的列表中,其余的是一个属性列表值。属性列表是一连串的键和值,以扁平的方式:

(:a 0 :b 1 :c 2)

幸运的是,您可以使用 DESTRUCTURING-BIND 来匹配已知格式(对于更复杂的格式,有模式匹配库):

(defun parse-card (list)
  ;; This is the expected format,the list starts with `:card`,so
  ;; I add an assertion here.
  (assert (eq :card (first list)))  
  ;; The rest of the list is a property list,let's bind front and back
  ;; to the values associated with keys :front and :back
  (destructuring-bind (&key front back) (rest list)
    ;; this is a function generated by "defstruct"
    (make-flashcard :front front :back back)))

例如:

* (parse-card '(:card :front 0 :back 1))
#S(FLASHCARD :FRONT 0 :BACK 1)

完成这项工作后,您可以使用 mapcar 来转换卡片列表:

* (mapcar #'parse-card (find-cards (cards) :virology))
(#S(FLASHCARD
    :FRONT "To what sort of cells does Epstain-Barr virus attach?"
    :BACK "B-cells")
 #S(FLASHCARD
    :FRONT "For how long does the virus of herpes simplex persist in tissues?"
    :BACK "lifetime")
 #S(FLASHCARD :FRONT "What T-cell receptors are recognised by HIV?" :BACK CD4))

我知道结构很容易定义,但它们在实时系统中不容易改变:如果你想添加一个新的槽,那么你需要重新启动你的 Lisp(这是为了高效编译,就像在静态类型语言,而类更动态)。

警告

当你读取的数据是一个符号时,比如CD4,它会属于当前调用read时绑定的。这可能会污染您的包裹和/或造成困难。您可能更喜欢使用字符串。

结论

这不是一个完整的解决方案,但您现在应该有不同的工具来取得进展,具体取决于您想去的地方。

,

要从文件中读取 lisp 表单,请参阅 uiop:read-file-form[s]。单数读取1个结构(就像一个巨大的alist),复数读取文件中的多个结构。

如果您想将程序中的数据保存到文件中,您可以简单地编写它们,例如使用 format <file> "~S"~S 保持结构,而不是 ~A),但是使用一些预防措施:

(let ((*print-pretty* nil)   ;; 
      (*print-length* nil))  ;; don't abbreviate long lines with "..."
  … write to file …)

UIOP 也有 read-file-line[s]read-file-string

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。