绪论
背景
英语的学习给现代中国学生带来了极大的挑战。学习英语的一种常规做法是记录纸质笔记。然而,常规的纸质笔记具有书写慢、不易修改的特点……(编不下去了)。为了简化英语单词笔记记录、查看的操作,本文基于一种简单的数据管理方法,提出一种新型单词本,即lisp-dictionary
命令行工具。该新型单词本兼具简易数据管理以及简易CLI的特点。
其中,关于简易数据管理,《实用Common Lisp编程》进行过讨论。而关于用户交互,《Land of Lisp》则进行了相关介绍。
数据结构的构造与基本操作
这是程序中最为核心、基础的部分,比较容易实现。
单个单词的数据结构
设计储存单个单词的数据结构如下,以构造函数的形式给出:
(defun create-word (spell) (copy-list `(:spell ,spell :n nil :v nil :adj nil :adv nil :prep nil)))
该数据结构为含有键的列表。储存的信息包括:单词的拼写,以及单词的名词、动词、形容词、副词、介词词义。根据网络信息,英语中单词的词性有十种之多。但实际使用中,英语单词的词性以少数几种居多。这里选取了其中最为常见的5种词性,并且假定在单词本使用中,多数情况下只涉及该5种词性。
列表数据结构与主要功能
本文选择简易的列表作为储存单词的数据结构,实现一种简易的数据管理。
主要构造的函数如下:
add-word
: 向字典中添加新构造的单词set-word
: 将特定单词的词性(关键字)设置为特定含义(值),其中值为字符串find-word
: 根据单词拼写,从字典中查找对应单词remove-word-spell
: 根据单词拼写,从字典中移除对应单词;这里考虑到,假设存在不可预知的意外情况(实际上按照设计,不可能由用户重复录入),导致字典中存在重复的单词。可以借助该命令移除所有拼写相同的单词clean-class-word
: 将给定单词的词性(关键字)设置为给定含义(值)display-word
: 打印单词的释义;其中,若某词性为nil
,则说明该单词不具有该词性,所以不打印该词性
"本脚本用于实现单词本" (defparameter *words-db* nil) (defun add-word (word) (push word *words-db*)) (defun set-word (word key value) "设置特定单词的关键字值,value应为字符串" (setf (getf word key) value)) (defun find-word (spell) "从字典中查找单词,若无则返回nil" (car (remove-if-not (lambda (word) (eql spell (getf word :spell))) *words-db*))) (defun remove-word-spell (spell) (setf *words-db* (remove-if #'(lambda (word) (eq spell (getf word :spell))) *words-db*))) (defun clean-class-word (word key) (set-word word key nil)) (defun display-word (word) (flet ((display-class-word (word key) (if (getf word key) (format t "~% ~a.~7t~a" key (getf word key))))) (format t ">>> ~a" (getf word :spell)) (display-class-word word :n) (display-class-word word :v) (display-class-word word :adj) (display-class-word word :adv) (display-class-word word :prep) (format t "~%")))
存档与加载
利用Common Lisp的文件读写功能保存存档与加载。存档文件的格式为纯文本。
这里,文件所在的路径为~/.config/lisp-dictionary
,跟后续所介绍的改造为命令行工具有关。
;;;; 数据库的存档与加载 (defun save-db (data-base filename) (with-open-file (out filename :direction :output :if-exists :supersede :if-does-not-exist :create) (with-standard-io-syntax (print data-base out)))) (defmacro load-db (data-base filename) `(let ((file-exists (probe-file ,filename))) (when file-exists (with-open-file (in ,filename :if-does-not-exist :error) (with-standard-io-syntax (setf ,data-base (read in))))))) (defun save-words () (save-db *words-db* "~/.config/lisp-dictionary/dictionary-words.db")) (defun load-words () (load-db *words-db* "~/.config/lisp-dictionary/dictionary-words.db"))
用户交互功能
用户交互应具备一定功能,如下示意图所示
main-repl ├ note-down │ ├ back │ └ [edit] ├ look-up │ └ back ├ edit │ ├ back │ └ change ├ erase │ ├ back │ ├ wipe │ └ wipe-clean ├ restore └ quit
该图用于表示功能之间的调用关系。其中,[]
代表被调用的功能一般情况下不应看作处于次一级。
基础的读、执行功能
参考《Land of Lisp》,应当实现基础的读入、执行、输出函数。然而,本文略去了输出函数,将输出文本集成到具体函数的执行中。
宏user-eval*
作为通用模板可根据需要生成执行函数。其参数allow-cmds
规定了允许运行的命令,作为程序的一种简易保护措施。
(defun user-read () "通用解析用户输入函数" (let ((cmd (read-from-string (concatenate 'string "(" (read-line) ")" )))) (flet ((quote-it (x) (list 'quote x))) (cons (car cmd) (mapcar #'quote-it (cdr cmd)))))) (defmacro user-eval* (allow-cmds) "模板,生成user-eval类型的函数,输入参数为允许的命令列表及允许词数 allow-cmds: 应形如((command-1 3) (command-2 1))" `(lambda (sexp) (format t "~c[2J~c[H" #\escape #\escape) (let* ((allow-cmds ,allow-cmds) (find-cmd (assoc (car sexp) allow-cmds))) (if (and find-cmd (eq (length sexp) (cadr find-cmd))) (eval sexp) (format t "Not a valid command. (✿ ◕ __ ◕ )~%")))))
读取-求值-输出循环
根据《Land of Lisp》,应当实现读取-求值-输出循环。
其中,所谓子REPL,即为look-up
、edit
、erase
,因为根据设计,该三个功能仍然存在一定的用户交互能力。由于三者作为REPL具有一定的重复性,因此有必要利用宏对其进行抽象,作为模板,然后利用该模板来构造三个函数。
子REPL模板的构造
(defun user-cmd-description (cmd-desc) "依次打印命令的描述" (format t "~{~{- [~a~15t]: ~a~}~%~}" cmd-desc)) (defparameter *the-word* nil) (defmacro user-repl* (cmd-desc u-eval) "子repl函数生成宏" `(lambda (spell) (setf *the-word* (find-word spell)) (let ((word *the-word*)) (labels ((repl (word) ; 此处显示查询单词的情况 (if *the-word* (progn ;(format t "~c[2J~c[H" #\escape #\escape) (format t "The target *~a* found. (˵u_u˵)~%~%" spell) (display-word word)) (progn ;(format t "~c[2J~c[H" #\escape #\escape) (format t "The taget *~a* does not exist. (ノ ◕ ヮ ◕ )ノ~%~%" spell))) ; 反馈可用命令 (user-cmd-description ,cmd-desc) ; 执行用户命令 (let ((cmd (user-read))) (if (eq (car cmd) 'back) (format t "~c[2J~c[H" #\escape #\escape) (progn (funcall ,u-eval cmd) (repl word)))))) (repl word)))))
主REPL的命令
主REPL的命令,即为note-down
、look-up
、edit
、erase
、restore
、quit
数个函数。
根据设计,note-down
、restore
、quit
不应作为循环,因此,需要单独编写。
(defun note-down (spell) ;(format t "~c[2J~c[H" #\escape #\escape) (let ((word (find-word spell))) ; 此处显示查询单词的情况 (if word (progn (format t "*~a* has already in our database.~%" spell) (read-line)) (progn (add-word (create-word spell)) (format t "The target *~a* has been add to our database.~%" spell) (read-line) (edit spell))))) (defparameter look-up-call (user-repl* '(("back" "go back to the main menu.")) (user-eval* '((back 1))))) (defparameter edit-call (user-repl* '(("back" "go back to the main menu.") ("change :key new-meaning" "to change part of the speech of the target.")) (user-eval* '((back 1) (change 3))))) (defparameter erase-call (user-repl* '(("back" "go back to the main menu.") ("wipe :key" "to wipe off part of the speech of the target.") ("wipe-clean" "to wipe off the whole target clean.")) (user-eval* '((back 1) (wipe 2) (wipe-clean 1))))) (defmacro look-up (spell) `(funcall look-up-call ,spell)) (defmacro edit (spell) `(funcall edit-call ,spell)) (defmacro erase (spell) `(funcall erase-call ,spell)) (defun restore () (save-words) (format t "Neatly done.~%") (read-line)) (defun quit-the-main-repl () (save-words) ; 自动存档 (format t "The dictionary closed. Goodbye. (⌐ ■ ᴗ ■ )~%"))
子REPL的命令
编写子REPL的命令。实际上只有change
、wipe
、wipe-clean
三个函数。均定义在全局范围内。因为根据通用模板user-eval*
的实现原理,应当在源文件全局范围内定义函数。
(defun change (key value) (set-word *the-word* key (prin1-to-string value))) (defun wipe (key) (clean-class-word *the-word* key)) (defun wipe-clean () (if (not *the-word*) (progn (format t "Quite clean. Nothing to wipe off.") (read-line)) (progn (format t "are you sure you want to wipe the hole target *~a* clean? (˵u_u˵)[y/n]" (getf *the-word* :spell)) (let* ((r-l (read-line)) (option (read-from-string (if (eq (length r-l) 0) "default" r-l)))) (cond ((eq 'y option) (remove-word-spell (getf *the-word* :spell)) (setf *the-word* nil) (format t "~c[2J~c[H" #\escape #\escape) (format t "Neatly-done.~%") (read-line)) ((eq 'n option)) (t (format t "yes or no?[y/n]~%") (wipe-clean)))))))
主REPL
在主REPL中,上述关于读取和执行的模板仍然可用,但针对子REPL设计的模板不可用,所以,这里再次编写REPL的结构(可能存在减少重复代码的空间)。
(defun main-repl () (format t "The dictionary opened. Wellcome back. ( ✿ ◕ ‿ ◕ )~%") (user-cmd-description ; 反馈可用命令 '(("note-down spell" "note-down a word.") ("look-up spell" "look up the dictionary for a word.") ("edit spell" "correct the fault.") ("erase spell" "give it a quick trim or eliminate it completely.") ("restore" "restore the data manually.") ("quit" "close the dictionary. data will be automatically restored by your little helper.(˵ ✿ ◕ ‿ ◕ ˵)"))) ; 执行用户命令 (let ((cmd (user-read))) (if (eq (car cmd) 'quit) (quit-the-main-repl) (progn (funcall (user-eval* '((note-down 2) (look-up 2) (edit 2) (erase 2) (restore 1) (quit 1))) cmd) (main-repl))))) (load-words) ; 自动加载存档 (main-repl) (sleep 0.1)(quit)
改造为命令行工具
Common Lisp程序可以编译为二进制可执行文件,具体编译方法因Common Lisp实现的不同而不同。具体方法参考《Common Lisp Recipes》。
利用ECL编译该程序,获得可执行文件,将其放置于/usr/local/bin
目录下
sudo cp ./dictionary /usr/local/bin/lisp-dictionary
另外考虑存档文件的存放路径,设置在配置文件路径~/.configure/lisp-dictionary
下。手动创建该路径即可。
关于lisp程序分发的问题
笔者曾经考虑将该程序代码分发至未安装Common Lisp实现的计算机上,但是发现存在困难。经过实践,笔者认为,在未安装Common Lisp实现的情况下,Common Lisp程序的分发确实存在困难。只有在Lisp程序员之间、以源文件的方式分享程序才是最方便的途径。Common Lisp本身并不存在版本的问题。事实证明,数十年前的Common Lisp代码在现在仍然可正常运行。
总结与展望
结论
本文针对单词本的实现开展讨论,主要解决了简易数据处理和简易命令行用户交互的问题。实现的单词本命令行工具具备简单的增、删、改、查功能,满足基本的英语学习需求。
展望
目前阶段,单词本命令行工具尚存在无法概览所有单词、无法反馈词汇总量的问题,对于用户可能的输入错误也未有良好的预防措施。未来可对单词本程序增加上述功能,并且考虑基于正则表达式实现较为高级的检索功能,允许根据单词局部来检索,可向用户反馈拼写相近的单词。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· 字符编码:从基础到乱码解决
· SpringCloud带你走进微服务的世界