首页 > 其他分享 >一次尝试:一种基于Common Lisp的简易单词本命令行工具

一次尝试:一种基于Common Lisp的简易单词本命令行工具

时间:2023-09-05 21:35:52浏览次数:34  
标签:cmd word format Lisp spell defun 单词 Common

绪论

背景

英语的学习给现代中国学生带来了极大的挑战。学习英语的一种常规做法是记录纸质笔记。然而,常规的纸质笔记具有书写慢、不易修改的特点……(编不下去了)。为了简化英语单词笔记记录、查看的操作,本文基于一种简单的数据管理方法,提出一种新型单词本,即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种词性。

列表数据结构与主要功能

本文选择简易的列表作为储存单词的数据结构,实现一种简易的数据管理。

主要构造的函数如下:

  1. add-word: 向字典中添加新构造的单词
  2. set-word: 将特定单词的词性(关键字)设置为特定含义(值),其中值为字符串
  3. find-word: 根据单词拼写,从字典中查找对应单词
  4. remove-word-spell: 根据单词拼写,从字典中移除对应单词;这里考虑到,假设存在不可预知的意外情况(实际上按照设计,不可能由用户重复录入),导致字典中存在重复的单词。可以借助该命令移除所有拼写相同的单词
  5. clean-class-word: 将给定单词的词性(关键字)设置为给定含义(值)
  6. 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-upediterase,因为根据设计,该三个功能仍然存在一定的用户交互能力。由于三者作为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-downlook-upediteraserestorequit数个函数。

根据设计,note-downrestorequit不应作为循环,因此,需要单独编写。

(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的命令。实际上只有changewipewipe-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代码在现在仍然可正常运行。

总结与展望

结论

本文针对单词本的实现开展讨论,主要解决了简易数据处理和简易命令行用户交互的问题。实现的单词本命令行工具具备简单的增、删、改、查功能,满足基本的英语学习需求。

展望

目前阶段,单词本命令行工具尚存在无法概览所有单词、无法反馈词汇总量的问题,对于用户可能的输入错误也未有良好的预防措施。未来可对单词本程序增加上述功能,并且考虑基于正则表达式实现较为高级的检索功能,允许根据单词局部来检索,可向用户反馈拼写相近的单词。

标签:cmd,word,format,Lisp,spell,defun,单词,Common
From: https://www.cnblogs.com/suspended-monitor/p/17680860.html

相关文章

  • 【计算机毕业设计】英语单词小程序源码
    开发环境及工具:大等于jdk1.8,大于mysql5.5,idea(eclipse),微信开发者工具技术说明:springbootmybatishtmlvue.jsbootstrap小程序代码注释齐全,没有多余代码,适合学习(毕设),二次开发,包含论文技术相关文档。功能介绍:用户端:登录注册(含授权登录)首页显示搜索,单词列表,搜索可根据单词名称模糊......
  • Python开发实例(十一)单词记忆游戏:编写一个简单的游戏,测试用户对一组随机单词的记忆能力
    在这个实例中,我们将创建一个简单的单词记忆游戏。游戏的规则是随机展示一组单词,然后要求用户在一定时间内尽可能多地记住这些单词。时间到后,再询问用户输入这些单词。最后,计算并显示用户正确记住的单词数量。下面是单词记忆游戏的Python程序:pythonCopycodeimportrandomimport......
  • log4j结合commons-logging配置总结
    作者fbysss关键字:loggingcommons-logging是一个通用的日志接口,commons-logging.jar包中自带了一个simplelog的实现log4j也实现了这个接口使用通用接口,方便在于如果更换实现的方式,只要修改一个配置项即可配置过程:commons-logging.properties必须放置在WEB-INF/classes/下面log4j.pro......
  • 剑指 Offer 58 - I. 翻转单词顺序
    剑指Offer58-I.翻转单词顺序解法一不用内置方法去除首尾空格和中间多余空格翻转所有字符翻转每个单词classSolution{publicStringreverseWords(Strings){//去除首尾空格和中间多余空格char[]ch=trim(s);//翻转所有字符re......
  • Java反序列化:CommonsCollections6调试分析
    JDK8u71大版本中AnnotationInvocationHandler.readObject被修改了,为了使得CC1能够利用,又造了一条CC6CC6解决的是CC1在高版本jdk上无法利用的问题这里搬一下web佬Boogipop的整理图:环境搭建JDK测试版本:JDK11基础知识1.CC1和CC6的恶意代码执行触发链再来捋顺一下这条恶......
  • Java:commons-codec实现byte数组和16进制字符串转换
    目录commons-codec实现原理封装StringUtil类commons-codec文档https://commons.apache.org/proper/commons-codec/https://mvnrepository.com/artifact/commons-codec/commons-codec坐标<dependency><groupId>commons-codec</groupId><artifactId>com......
  • 英语背单词 专四词汇 2023年09月 ChatGPT
    2023-09-02Explainthemeaningofthefollowingwordsalongwithindexandphoneticsymbol:detain,psychologist,robe,partition,sinful,dominion,heave,watertight,flute,calendar,pickpocket,lavatory,satire,sin,martyrIndexWordPhoneticSymbolP......
  • 背单词 首字母 2023年09月
    2023-09-02dprpsdhwfcplssmdetain,psychologist,robe,partition,sinful,dominion,heave,watertight,flute,calendar,pickpocket,lavatory,satire,sin,martyr2023-09-01ldtipsfchdaddmllocust,deceit,tile,incidence,phobia,suspension,fractional,complace......
  • Java:commons-codec实现byte数组和16进制字符串转换
    (目录)commons-codec文档https://commons.apache.org/proper/commons-codec/https://mvnrepository.com/artifact/commons-codec/commons-codec坐标<dependency><groupId>commons-codec</groupId><artifactId>commons-codec</artifact......
  • Apache Commons Logging 是如何决定使用哪个日志实现类的
    ApacheCommonsLogging像SLF4J一样,是个通用日志框架,广泛应用在各个开源组件中。说其通用,是因为它本身只提供了简单的日志输出的实现(org.apache.commons.logging.impl.SimpleLog和org.apache.commons.logging.impl.NoOpLog),主要是为你统一使用其他专业日志实现(Log4j、jdk1.4......