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