<-- Home |--lisp

010 聪明先生拒(ji)绝(xu)造轮子

聪明先生

聪明先生很聪明, 他仔细观察好兄弟们手忙脚乱慌慌张张急急忙忙学习Lisp的全过程, 默默地把知识点都记在大脑里:

  1. 001 粗鲁先生Lisp再出发
  2. 002 懒惰先生的Lisp开发流程
  3. 003 颠倒先生的数学表达式
  4. 004 完美先生的完美Lisp
  5. 005 好奇先生用Lisp来探索Lisp
  6. 006 好奇先生在Lisp的花园里挖呀挖呀挖
  7. 007 挑剔先生给出终止迭代的条件
  8. 008 挠痒痒先生建网站记
  9. 009 小小先生学习Lisp表达式

对他来说, 影响最深刻的无疑是好奇先生的探索Lisp工具包. 这个工程的地址在explore-lisp。当把这个源代码下载(clone)到本地之后,可以通过quicklispquickload函数加载这个包。

加载之前, 可以把这个包放到quicklisplocal-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循环读取每一行, 利用searchstring-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

仔细来学习一下各类符号大概有什么, 是很有意义的一件事情. 聪明先生已经默默地开始看类型了, 接下来准备看函数与操作符.

聪明先生非常专注, 一开始看起来就什么都不知道, 既听不见周围的声音, 也不知道时间…

总结

  1. 聪明先生非常聪明, 他总是能认识到自己的错误: 造轮子, 造轮子, 造轮子;
  2. 聪明先生学习编程序总是把原理和实践结合起来;
  3. 要想不造轮子, 就要知道有些什么轮子;
  4. 聪明先生一忍不住又造了若干个方形的轮子, 每次都是这样, 承认错误, 坚决不改!

文章标签

|-->lisp |-->编程 |-->实用主义 |-->入门 |-->教程 |-->符号参考 |-->common lisp


GitHub