(defun where (&key title artist rating (ripped nil ripped-p))
#'(lambda (cd)
(and
(if title (equal (getf cd :title) title) t)
(if artist (equal (getf cd :artist) artist) t)
(if rating (equal (getf cd :rating) rating) t)
(if ripped-p (equal (getf cd :ripped) ripped) t))))
在编程的世界里,语言的选择如同选择一位舞伴,有的轻盈优雅,有的沉稳可靠,而Lisp无疑是其中的优雅舞者之一。尽管诞生于1958年,Lisp依然在现代编程中大放异彩,正如Paul Graham所言:“Lisp是数学,数学永远不会过时。”今天,我们将通过一个简单的例子,来探讨如何用Lisp实现一个基本的数据库,专门用来存储MP3歌曲的信息。
CD 和记录 🎵
首先,我们需要定义我们的数据结构。我们的数据库将包含多条CD记录,每条记录包含以下四个信息:
1. 数据结构的定义
在Lisp中,我们可以使用列表(
list
)和属性表(property list
,简称plist
)作为数据结构。列表类似于Python中的列表,而属性表则更像Python的字典。我们可以用以下代码定义一个CD记录:使用示例:
这将返回一个结构化的CD记录:
2. 录入 CD 记录 📜
接下来,我们需要一个地方来存储这些CD记录。我们可以定义一个全局变量
*db*
(遵循Lisp的命名约定),并利用PUSH
宏来添加新的记录:现在,我们可以将
make-cd
和add-record
结合起来,方便地将新的CD记录添加到数据库中。3. 数据库的格式化输出 🎉
为了查看数据库中的内容,我们需要一个更友好的输出格式。我们可以使用
dolist
宏来遍历数据库,并用format
函数来格式化输出:调用
(dump-db)
后,我们将看到如下格式的输出:改进用户交互 💬
使用
add-record
来添加CD记录显得有些繁琐,因此我们可以编写一个函数来提示用户输入CD信息。以下是一个简单的用户输入函数示例:结合
prompt-read
和make-cd
,我们可以创建一个更友好的用户接口:保存和加载数据库 💾
为了防止数据丢失,我们可以将数据库保存到文件中,并能够在下次加载时读取。以下是保存和加载的代码示例:
查询数据库 🔍
有了数据库,我们当然需要查询的功能。我们可以使用
remove-if-not
函数来筛选出符合条件的记录:调用示例:
更新和删除记录 ✂️
为了使数据库更加灵活,我们还可以添加更新和删除的功能。这可以通过
mapcar
和remove-if
等函数实现:update
函数的逐行解析这段代码定义了一个
update
函数,用于更新数据库*db*
中符合条件的CD记录。该函数使用一个选择器函数(selector-fn
)来匹配需要更新的记录,并通过关键字参数提供更新的内容。我们将逐行解释它的功能和关键点。update
函数的目的是更新数据库中的CD记录。selector-fn
: 一个函数,用于选择需要更新的记录。该函数会传递给funcall
,并对每个CD记录进行过滤。&key
: 关键字参数,允许用户指定哪些属性需要更新。包括:title
: 新的标题(如果提供)。artist
: 新的艺术家名称(如果提供)。rating
: 新的评分(如果提供)。(ripped nil ripped-p)
: 是否已翻录。ripped-p
是一个标志,表示是否传递了ripped
参数。默认值为nil
。&key
用于定义关键字参数,这在Lisp中是一种便捷的方式来传递可选参数。(ripped nil ripped-p)
定义了一个特殊的关键字参数ripped
,并通过ripped-p
检查该参数是否实际被传递。mapcar
函数来遍历*db*
中的每一条CD记录,并对每一条记录进行更新。如果某条记录符合selector-fn
的选择条件,则根据传入的关键字参数更新相应的字段。setf *db*
:setf
用于修改全局变量*db*
,将其设置为mapcar
函数的结果。mapcar
的作用是对*db*
列表中的每一条记录应用指定的函数。mapcar
:mapcar
用于遍历*db*
列表,并对每条记录row
应用一个匿名函数(lambda
)。row
传递给匿名函数,匿名函数根据条件选择是否更新该记录。lambda
函数:lambda
)接收每一条记录row
,并通过funcall
调用selector-fn
来决定是否对该记录进行更新。selector-fn
是一个选择器函数,它接收row
作为参数,返回t
或nil
,表示是否需要更新该记录。when (funcall selector-fn row)
:funcall
调用selector-fn
,并将row
作为参数传递。如果selector-fn
对该row
返回t
,则进入when
语句块,执行更新操作。if title (setf (getf row :title) title)
:title
参数不为nil
,则使用setf
更新该记录的:title
属性为新的title
值。if artist (setf (getf row :artist) artist)
:artist
参数不为nil
,则更新该记录的:artist
属性。if rating (setf (getf row :rating) rating)
:rating
参数不为nil
,则更新该记录的:rating
属性。if ripped-p (setf (getf row :ripped) ripped)
:ripped-p
为t
,表示用户传递了ripped
参数,则更新该记录的:ripped
属性。注意这里是通过ripped-p
来判断是否传递了ripped
参数,而不是直接判断ripped
的值。这允许用户显式地将ripped
设置为nil
或t
。row
的返回:lambda
函数都会返回row
,并将其包含在新的列表中。mapcar
将这些记录组成一个新的数据库列表。关键点和难点解析
selector-fn
进行条件选择:selector-fn
)来指定需要更新的记录。这个设计非常通用,用户可以传递任意的选择逻辑,从而实现复杂的过滤条件。&key
)使得函数调用更加灵活。用户可以选择只更新某几个字段,而不必传递所有字段的值。(ripped nil ripped-p)
是一种较为高级的用法,它不仅允许用户传递ripped
值,还可以检测用户是否传递了这个参数。这使得ripped
可以显式地设置为nil
或t
。mapcar
的使用:mapcar
是一个常用的函数式编程工具,用于对列表中的每个元素应用一个函数,并返回一个新列表。这里它被用来遍历*db*
,并返回一个更新后的数据库。setf
和getf
的使用:setf
是Lisp中的通用赋值操作符,用于修改数据结构中的值。在这里,它被用来更新属性列表(plist)中的值。getf
用于从属性列表中获取指定键的值。通过setf
和getf
的组合,可以修改属性列表的某个键值对。示例
假设数据库
*db*
中有以下CD记录:现在,我们希望将所有评分为
5
的CD的标题更新为"New Title"
,可以这样调用update
函数:调用后,数据库中的第一条记录的标题将被更新为
"New Title"
,而其他记录保持不变。结论 🎊
通过上述步骤,我们用Lisp成功实现了一个简单的数据库,能够存储、查询、更新和删除CD记录。尽管Lisp的语法可能让初学者感到陌生,但它的优雅与强大在实际应用中会让你感受到编程的乐趣。希望你在探索Lisp的旅程中,能够感受到这位优雅舞伴所带来的无限魅力。
参考文献 📚
这段代码实现了一个简单的CD数据库管理系统,可以添加、查询、更新和删除CD记录,并支持数据库的加载和保存。我们将逐步分析每一个部分的功能和关键点。
1.
make-cd
: 创建 CD 记录make-cd
函数用于创建一个新的CD记录。每个CD记录是一个列表,包含标题(:title
)、艺术家(:artist
)、评分(:rating
)和是否已翻录(:ripped
)。list
函数创建一个属性列表(property list,简称 plist),其键(以冒号开头,如:title
)是符号,值是函数参数。getf
函数可以访问键对应的值。2.
*db*
: 数据库变量nil
,表示数据库为空。*db*
是一个全局变量,遵循Common Lisp中全局变量的命名习惯,即用星号包围变量名。defvar
定义全局变量,如果该变量已经存在,defvar
不会重新初始化。3.
add-record
: 添加记录到数据库push
将CD记录添加到*db*
列表的头部。push
是一个高效的操作,因为它直接修改列表的指向。4.
dump-db
: 打印数据库内容*db*
,逐条打印每个CD记录的属性和值。dolist
用于遍历数据库中的每个CD记录。format
函数用于格式化输出。这里的~{~a:~10t~a~%~}
格式化字符串用于按键值对的形式打印CD的元数据。5.
prompt-read
: 从用户输入读取数据format
用于显示提示信息。force-output
确保提示信息立即输出,而read-line
则从用户输入中读取一行文本。6.
prompt-for-cd
: 提示用户输入CD信息prompt-read
函数提示用户输入标题和艺术家。parse-integer
将用户输入的评分转换为整数,如果输入的内容不是有效的数字,则返回0
(使用了:junk-allowed
选项)。y-or-n-p
确认CD是否已经翻录。它是一个用于读取y/n
回答的函数。7.
add-cds
: 循环添加多个CD记录loop
用于无限循环,直到用户选择不再添加CD。add-record
添加新CD,并通过y-or-n-p
确认是否继续。8.
save-db
: 保存数据库到文件with-open-file
打开一个文件进行输出,:if-exists :supersede
表示如果文件存在则覆盖。with-standard-io-syntax
设置标准的I/O语法,确保数据结构可以正确地读/写。print
将数据库的内容输出到文件。9.
load-db
: 从文件加载数据库save-db
类似使用with-open-file
打开文件进行读取。read
函数从文件中读取数据库数据,并使用setf
将其赋值给*db*
。10.
select
: 选择符合条件的CD记录selector-fn
条件的CD记录。remove-if-not
用于过滤列表,保留所有满足selector-fn
函数返回true
的元素。11.
where
: 构造选择条件title
、artist
、rating
等)构造一个选择函数,用于在select
函数中进行过滤。&key
允许参数按关键字传递。lambda
),该函数根据传递的条件对每个CD记录进行匹配。12.
update
: 更新符合条件的CD记录selector-fn
条件的CD记录。mapcar
遍历*db*
,对每个符合条件的CD记录进行更新。setf
更新属性列表中的对应属性。13.
delete-rows
: 删除符合条件的CD记录selector-fn
条件的CD记录。remove-if
移除列表中符合条件的元素。总结
这段代码实现了一个简单的CD数据库系统,使用Common Lisp的基本数据结构和控制流来管理数据。代码的关键点包括:
selector-fn
)进行选择和过滤。通过这些功能,用户可以灵活地管理和操作CD数据库。