010 聪明先生拒(ji)绝(xu)造轮子
聪明先生
聪明先生很聪明, 他仔细观察好兄弟们手忙脚乱慌慌张张急急忙忙学习Lisp的全过程, 默默地把知识点都记在大脑里:
- 001 粗鲁先生Lisp再出发
- 002 懒惰先生的Lisp开发流程
- 003 颠倒先生的数学表达式
- 004 完美先生的完美Lisp
- 005 好奇先生用Lisp来探索Lisp
- 006 好奇先生在Lisp的花园里挖呀挖呀挖
- 007 挑剔先生给出终止迭代的条件
- 008 挠痒痒先生建网站记
- 009 小小先生学习Lisp表达式
对他来说, 影响最深刻的无疑是好奇先生的探索Lisp工具包. 这个工程的地址在explore-lisp。当把这个源代码下载(clone)到本地之后,可以通过quicklisp
的quickload
函数加载这个包。
加载之前, 可以把这个包放到quicklisp
的local-projects
文件夹里面; 也可以通过修改asdf:*central-registry*
, 然后在REPL里面执行(ql:quickload :explore-lisp)
。
具体的方法, 好奇先生已经写得很清楚, 聪明先生觉得自己只需要再练习个七次八次就可以, 如果是一个稍微不聪明一点的人, 可能会觉得不需要练习就会呢! 但是聪明先生不会犯那种错误, 他总是把搞清楚是什么原理和实际练习若干遍结合起来, 所以他才是聪明先生嘛!
聪明先生, 决定彻底学会Lisp!
聪明先生学习编程的方法
聪明先生总是清楚地认识自己的不足之处, 并谋定而后动, 学习Lisp也是这样. 对于编程语言的学习, 聪明先生觉得自己最大的问题可能会是重复造轮子! 因为, 你知道, 聪明先生之所以是聪明先生, 就是因为他有一种觉得自己很聪明什么都能做出来, 再搭配上Lisp这样灵活都不足以形容必须称其为毫无底线的语言, 那么, 你知道, 聪明先生就会不停地造出各种轮子…
为了避免如此, 聪明先生觉得自己应该把Common Lisp已经提供的工具好好学习一遍, 当然! 在学习的过程中他可是会免不了造点毫无意义的轮子呢!
explore-lisp
工具包
聪明先生不费吹灰之力就把explore-lisp
工具整好, 接下来就是, 把common-lisp
这个包的内容好好批判一番.
1(ql:quickload :explore-lisp)
2(require :explore-lisp)
主要是先用, (el:dir :cl)
来列出cl
的符号(毕竟, 所有的东西都是符号!), 然后在用describe
, 或者el:describe-symbol
来查看每个符号的帮助.
看着看着, 聪明先生发现一个大秘密, 每一个符号的帮助都带有一句:
1(describe 'floor)
2COMMON-LISP:FLOOR
3 [symbol]
4FLOOR names a compiled function:
5 Lambda-list: (NUMBER &OPTIONAL (DIVISOR 1))
6 Declared type: (FUNCTION (REAL &OPTIONAL REAL)
7 (VALUES INTEGER REAL &OPTIONAL))
8 Derived type: (FUNCTION (T &OPTIONAL T)
9 (VALUES (OR NULL INTEGER) NUMBER &OPTIONAL))
10 Documentation:
11 Return the greatest integer not greater than number, or number/divisor.
12 The second returned value is (mod number divisor).
13 Known attributes: foldable, flushable, unsafely-flushable, movable
14 Source file: SYS:SRC;CODE;NUMBERS.LISP
15NIL
FLOOR names a compiled function:
这不就给聪明先生一个灵感了吗? 他觉得可以把所有符号都用这个compiled function
类似的描述分下类.
说干就干! 聪明先生开始写代码:
符号分类
基本思路, 利用el:export-all-external-symbols
导出:cl
所有符号到一个文件中, 然后一行一行的循环, 所有的xxxx names a xxxx:
这样的行都提取出来.
1(let ((tfn "temp-files/all-external-symbols.md"))
2 (el:export-all-external-symbols :cl :fn tfn)
3 (with-open-file (fn tfn)
4 ;; read line by line
5 (loop for line = (read-line fn nil)
6 while line
7 do (when (and (search " names a " line) (string-ends-with-p line ":"))
8 (format t "~a~%" line)))))
这个代码利用了, with-open-file
来打开文件, 然后loop
循环读取每一行, 利用search
和string-ends-with-p
来判断是否是我们需要的行.
这里的函数string-ends-with-p
是一个自定义的函数, 用来判断一个字符串是否以另一个字符串结尾. 聪明先生完全没有忍住, 又造了一个轮子!
1(defun string-ends-with-p (str ending)
2 "Return t if str ends with ending."
3 (let ((elength (length ending))
4 (slength (length str)))
5 (if (>= slength elength)
6 (string= (subseq str (- slength elength)) ending)
7 nil)))
1......
2WRITE-BYTE names a compiled function:
3WRITE-CHAR names a compiled function:
4WRITE-LINE names a compiled function:
5WRITE-SEQUENCE names a compiled function:
6WRITE-STRING names a compiled function:
7WRITE-TO-STRING names a compiled function:
8Y-OR-N-P names a compiled function:
9YES-OR-NO-P names a compiled function:
10ZEROP names a compiled function:
找到了所有这样的行之后, 就需要把字符串的两个部分提取出来. 聪明先生又写了一个函数:
1(defun split-string-by-substring (str sep)
2 ;; split string by substring
3 (let ((start 0)
4 (end 0)
5 (result '())
6 (sub-len (length sep)))
7 (loop while (setq end (search sep str :start2 start))
8 do (progn
9 (push (subseq str start end) result)
10 (setq start (+ sub-len end))))
11 (push (subseq str start) result)
12 (nreverse result)))
啊, 一个轮子又造好了! 聪明先生觉得自己真是太聪明了!
1(let ((tfn "temp-files/all-external-symbols.md"))
2 (el:export-all-external-symbols :cl :fn tfn)
3 (with-open-file (fn tfn)
4 ;; read line by line
5 (loop for line = (read-line fn nil)
6 while line
7 do (when (and (search " names a " line) (string-ends-with-p line ":"))
8 (let* ((parts (split-string-by-substring line " names a "))
9 (symbol (first parts))
10 (type-string (second parts))
11 (type-name (trim-string type-string ":")))
12 (format t "~a:~A~%" type-name symbol))))))
其实, 这里面聪明先生又犯了一次!
1(defun trim-string (str &optional (chars " \t\n"))
2 (let ((start (position-if (complement (lambda (c) (find c chars))) str))
3 (end (position-if (complement (lambda (c) (find c chars))) (reverse str))))
4 (if (and start end)
5 (subseq str start (- (length str) end))
6 "")))
这样就可以打印出, 符号类型: 符号名字了.
1......
2macro:WITH-SIMPLE-RESTART
3macro:WITH-SLOTS
4macro:WITH-STANDARD-IO-SYNTAX
5compiled function:WRITE
6compiled function:WRITE-BYTE
7compiled function:WRITE-CHAR
8compiled function:WRITE-LINE
9compiled function:WRITE-SEQUENCE
10compiled function:WRITE-STRING
11compiled function:WRITE-TO-STRING
12compiled function:Y-OR-N-P
13compiled function:YES-OR-NO-P
14compiled function:ZEROP
接下来就简单了, 聪明先生觉得只需要整一个hash-table
就可以了, 然后把符号名字放到对应的类型里面.
1(defparameter *symbol-types* (make-hash-table :test 'equalp))
2
3
4(defun add-symbol-with-type (type symbol)
5 (let ((symbols (gethash type *symbol-types*)))
6 (if (not (member symbol symbols))
7 (setf (gethash type *symbol-types*) (cons symbol symbols)))))
这里唯一要注意的就是, 要把hash-table
的比较函数设置成equalp
, 因为聪明先生过目不忘, 007 挑剔先生给出终止迭代的条件 对比较说得可清楚啦!
这样下来, 就可以把所有的符号都放到hash-table
里面了.
1(let ((tfn "temp-files/all-external-symbols.md"))
2 (el:export-all-external-symbols :cl :fn tfn)
3 (with-open-file (fn tfn)
4 ;; read line by line
5 (loop for line = (read-line fn nil)
6 while line
7 do (when (and (search " names a " line) (string-ends-with-p line ":"))
8 (let* ((parts (split-string-by-substring line " names a "))
9 (symbol (first parts))
10 (type-string (second parts))
11 (type-name (trim-string type-string ":")))
12 (add-symbol-with-type type-name symbol)
13 (format t "~a:~A~%" type-name symbol))))))
这样, 聪明先生就可以把所有的符号都分类了.
1
2(with-open-file (fn "temp-files/symbols.md" :direction :output :if-exists :supersede)
3
4 (loop for key being the hash-keys of *symbol-types*
5 do (let* ((symbols-string (gethash key *symbol-types*))
6 (symbols (nreverse (mapcar #'intern symbols-string))))
7 ;; print documents for each symbol
8 (format fn "~%## ~a~%~%" key)
9 (format fn "#~a" (el:format-descriptions symbols 2)))))
这样, 再打开一个文件, 按照每个类型一个二级标题, 把每个类型的符号按照el:format-descriptions
的格式打印出来. 这里注意, 先把记录在hash-table
里面的符号名字转换成符号对象. 并且用nreverse
来翻转一下, 因为cons
是从前面加的, 所以要翻转一下.
结果也相当可爱:Appendix 001: Common Lisp Symbols分类参考. 有一个完整的Common Lisp的符号分类参考.
所有代码都在一个文件里面, 产生符号分类文档, 不过使用这个文件必须要有最新的explore-lisp
工具包. 因为聪明先生偷偷更新过一个函数.
Common Lisp 符号分类参考
聪明先生觉得这个分类参考非常有用, 他决定把这个参考放到自己的网站上, 以便随时查看.
可以看到, Common-lisp这个包里面, 提供了三个类型的符号, 首先是变量和常量, 然后是函数, 操作符, 和宏这些可以作为函数调用的符号, 最后是类型.
- 变量
- special variable
- constant variable
- 函数与操作符
- compiled function
- generic function
- macro
- special operator
- 类型
- primitive type-specifier
- type-specifier
仔细来学习一下各类符号大概有什么, 是很有意义的一件事情. 聪明先生已经默默地开始看类型了, 接下来准备看函数与操作符.
聪明先生非常专注, 一开始看起来就什么都不知道, 既听不见周围的声音, 也不知道时间…
总结
- 聪明先生非常聪明, 他总是能认识到自己的错误: 造轮子, 造轮子, 造轮子;
- 聪明先生学习编程序总是把原理和实践结合起来;
- 要想不造轮子, 就要知道有些什么轮子;
- 聪明先生一忍不住又造了若干个方形的轮子, 每次都是这样, 承认错误, 坚决不改!
文章标签
|-->lisp |-->编程 |-->实用主义 |-->入门 |-->教程 |-->符号参考 |-->common lisp
- 本站总访问量:次
- 本站总访客数:人
- 可通过邮件联系作者:Email大福
- 也可以访问技术博客:大福是小强
- 也可以在知乎搞抽象:知乎-大福
- Comments, requests, and/or opinions go to: Github Repository