好奇先生Lisp探索工具explore-lisp
好奇先生学会了懒惰先生交给他的Package开发和管理流程,quickproject管理package。好奇先生一步一步定义了一个叫做explore-lisp
的package,这个包提供了探索Lisp的工具,并且提供了把探索结果输出成markdown
文档的方法。
这个工程的地址在explore-lisp。当把这个源代码下载(clone)到本地之后,可以通过quicklisp
的quickload
函数加载这个包。
在安装之前,记得要把文件克隆到quicklisp
的本地目录下,比如~/quicklisp/local-projects
。
或者还有一个办法,克隆到任何地方,除了利用修改变量asdf:*central-registry*
方式,还可以直接在quicklisp
的local-projects
目录下建立一个软链接,指向这个包的目录。
mklink /D ~/quicklisp/local-projects/explore-lisp /path/to/explore-lisp
然后在REPL
中加载这个包:
(ql:quickload :explore-lisp)
这之后,就可以用这个包的功能了。
* (el:dir 'el)
(EXPLORE-LISP:FORMAT-DESCRIPTIONS EXPLORE-LISP:DESCRIBE-SYMBOL EXPLORE-LISP:DIR
EXPLORE-LISP:EXPORT-ALL-EXTERNAL-SYMBOLS
EXPLORE-LISP:EXPORT-DESCRIPTIONS)
5
这个包本身的dir
功能就能够列出这个包的所有函数和变量。还可以用
(el:describe-symbol 'el:dir)
"EXPLORE-LISP:DIR
[symbol]
DIR names a compiled function:
Lambda-list: (PACKAGE)
Derived type: (FUNCTION (T) (VALUES LIST UNSIGNED-BYTE &OPTIONAL))
Documentation:
List all external symbols in a package, return a list of symbol names and its length
Source file: C:/Users/qchen/quicklisp/local-projects/explore-lisp/explore-lisp.lisp
"
可以看到这个符号的文档有几个基本的部分:
- 函数的全名(包括包的名字作为前缀)
- 符号的类型(这里是一个
symbol
) - 下面就是函数的定义,包括参数列表和返回值的类型
- 文档字符串,这个字符串描述了这个函数的功能
- 函数的源代码文件
好奇先生运行了(el:export-all-external-symbols 'el :fn "docs.md" :start-level 2)
,这个函数的功能是把el
包的所有外部符号都导出来,哇啦,一个文件docs.md
就生成了,里面包含了这个包的所有外部符号的文档。
挖一下explore-lisp
下面的文档主体部分,都是通过好奇先生的探索工具集生成的。真的是太好玩,好奇先生可以玩一整天。
DESCRIBE-SYMBOL
这个函数可以把一个符号的文档描述输出成一个字符串。可以看到这个函数的签名,以及参数列表,返回值的类型,文档描述,函数的源代码文件位置。
EXPLORE-LISP:DESCRIBE-SYMBOL
[symbol]
DESCRIBE-SYMBOL names a compiled function:
Lambda-list: (NAME)
Derived type: (FUNCTION (T) (VALUES SIMPLE-STRING &OPTIONAL))
Documentation:
Describe a symbol and return the output as a string
Source file: C:/Users/qchen/quicklisp/local-projects/explore-lisp/explore-lisp.lisp
DIR
这个函数列出一个包的所有外部符号,返回一个符号名字的列表和列表的长度。
EXPLORE-LISP:DIR
[symbol]
DIR names a compiled function:
Lambda-list: (PACKAGE)
Derived type: (FUNCTION (T) (VALUES LIST UNSIGNED-BYTE &OPTIONAL))
Documentation:
List all external symbols in a package, return a list of symbol names and its length
Source file: C:/Users/qchen/quicklisp/local-projects/explore-lisp/explore-lisp.lisp
EXPORT-ALL-EXTERNAL-SYMBOLS
这个函数把一个包的所有外部文件列出在一个文件中,可以通过关键词来设定文件名称和最开始文档在markdown中的标题级别。
这里最有趣的是这个,这个函数的源程序直接被列在这里?为什么?为什么其他的函数没有呢?只是给出了函数对应文件的位置。这就是Lisp最为独特的地方,Lisp的运行环境(REPL)中的符号都是动态的,可以被修改,可以被重新定义,可以被删除。这个函数的源代码就是这样被列在这里的。
这里有一个Lisp的核心概念,一个运行的环境中所有的符号、绑定都可以很容易的存成一个文件,然后在另一个环境中重新加载,这就是Lisp的强大之处。
所以很多资深Lisp程序员都说,Lisp程序是一个不停进化的活的程序,运行时修改功能,增加功能,删除功能,都是很容易的事情。
EXPLORE-LISP:EXPORT-ALL-EXTERNAL-SYMBOLS
[symbol]
EXPORT-ALL-EXTERNAL-SYMBOLS names a compiled function:
Lambda-list: (PACKAGE &KEY (FN FN-P) (START-LEVEL 1))
Derived type: (FUNCTION (T &KEY (:FN T) (:START-LEVEL T))
(VALUES NULL &OPTIONAL))
Documentation:
List all external symbols in a package and their doc strings into a file ~package~.md
Source form:
(LAMBDA
(PACKAGE
&KEY (EXPLORE-LISP::FN "" EXPLORE-LISP::FN-P)
(EXPLORE-LISP::START-LEVEL 1))
"List all external symbols in a package and their doc strings into a file ~package~.md"
(BLOCK EXPLORE-LISP:EXPORT-ALL-EXTERNAL-SYMBOLS
(LET ((EXPLORE-LISP::SORTED-NAMES
(EXPLORE-LISP::SORT-SYMBOLS (EXPLORE-LISP:DIR PACKAGE)))
(EXPLORE-LISP::_FN
(IF EXPLORE-LISP::FN-P
EXPLORE-LISP::FN
(FORMAT NIL "~a.md" PACKAGE))))
(WITH-OPEN-STREAM
(EXPLORE-LISP::S
(OPEN EXPLORE-LISP::_FN :DIRECTION :OUTPUT :IF-EXISTS
:SUPERSEDE))
(FORMAT EXPLORE-LISP::S "~A ~A external symbols~%~%~%"
(EXPLORE-LISP::MARKDOWN-NTH-HEADER
EXPLORE-LISP::START-LEVEL)
PACKAGE)
(LET ((EXPLORE-LISP::INDEX 1))
(DOLIST (EXPLORE-LISP::NAME EXPLORE-LISP::SORTED-NAMES)
(FORMAT EXPLORE-LISP::S "~d. [~A](#~A)~%"
EXPLORE-LISP::INDEX EXPLORE-LISP::NAME
EXPLORE-LISP::NAME)
(INCF EXPLORE-LISP::INDEX)))
(FORMAT EXPLORE-LISP::S "~%~%")
(DOLIST (EXPLORE-LISP::NAME EXPLORE-LISP::SORTED-NAMES)
(FORMAT EXPLORE-LISP::S "~A `~A`~%~%"
(EXPLORE-LISP::MARKDOWN-NTH-HEADER
(+ 1 EXPLORE-LISP::START-LEVEL))
EXPLORE-LISP::NAME)
(FORMAT EXPLORE-LISP::S "```lisp~%")
(DESCRIBE EXPLORE-LISP::NAME EXPLORE-LISP::S)
(FORMAT EXPLORE-LISP::S "```~%"))))))
EXPORT-DESCRIPTIONS
将一个名称列表的描述导出到一个文件中,可以通过关键词来设定文件名称和最开始文档在markdown中的标题级别。
EXPLORE-LISP:EXPORT-DESCRIPTIONS
[symbol]
EXPORT-DESCRIPTIONS names a compiled function:
Lambda-list: (NAME-LIST FN &OPTIONAL (START-LEVEL 1))
Derived type: (FUNCTION (T T &OPTIONAL T) (VALUES NULL &OPTIONAL))
Documentation:
Save a list of symbol names to a file
Source file: C:/Users/qchen/quicklisp/local-projects/explore-lisp/explore-lisp.lisp
FORMAT-DESCRIPTIONS
将一个名称列表的描述格式化成markdown文档,可以通过关键词来设定文件名称和最开始文档在markdown中的标题级别。
EXPLORE-LISP:FORMAT-DESCRIPTIONS
[symbol]
FORMAT-DESCRIPTIONS names a compiled function:
Lambda-list: (NAME-LIST &OPTIONAL (START-LEVEL 1))
Derived type: (FUNCTION (T &OPTIONAL T)
(VALUES SIMPLE-STRING &OPTIONAL))
Documentation:
Format a list of symbol names as markdown, with optional start level for headers
Source file: C:/Users/qchen/quicklisp/local-projects/explore-lisp/explore-lisp.lisp
SEARCH-SYMBOLS
这个函数可以搜索一个包中的所有符号,包括符号的名字和文档字符串。文档字符串内的搜索是可选的。所有的搜索和字符串比较都是不区分大小写的。
EXPLORE-LISP:SEARCH-SYMBOLS
[symbol]
SEARCH-SYMBOLS names a compiled function:
Lambda-list: (NAME PACKAGE &KEY (DOC-STRING NIL))
Derived type: (FUNCTION (T T &KEY (:DOC-STRING T))
(VALUES LIST &OPTIONAL))
Documentation:
Search for string in symbol names and doc strings in a package
Source file: C:/Users/qchen/quicklisp/local-projects/explore-lisp/explore-lisp.lisp
挖一下string
好奇先生探索工具非常简单,每个函数功能都很单一。并且,在(ql:quickload 'explore-lisp)
之后,如果对功能不满意或者发现运行不符合预期,
- 可以在源代码所在位置(
describe
一下任意一个函数就知道)修改,然后重新加载,(require 'explore-lisp)
。 - 直接在VSCode中打开一个任意新文件,
(in-package :explore-lisp)
,编写函数新定义,然后运行一下,也能达到效果。
这就是Lisp的动态开发理念,与传统的“编译-运行-调试”不同,Lisp把当前REPL中的所有符号都当作一个整体,可以随时修改,随时调试,随时运行。并且,Lisp的函数绑定都是动态的,这就鼓励各种运行时修改的开发模式。
另外,好奇先生觉得最后这个函数实在是太好玩了……他太好奇了。
好奇先生应该睡觉,但是他就是忍不住,又在REPL中敲一个命令:
(el:search-symbols "string" :common-lisp)
好奇先生还没来得及按下回车,挠痒痒先生的手从很远的地方伸过来,挠了一下好奇先生的咯吱窝,然后还按了一下键盘,好奇先生的命令就被执行了。
啊,Lisp的REPL关于字符串的函数全部跑出来了~~~一共有46个!
(BASE-STRING DIRECTORY-NAMESTRING ENOUGH-NAMESTRING FILE-NAMESTRING
FILE-STRING-LENGTH GET-OUTPUT-STREAM-STRING HOST-NAMESTRING MAKE-STRING
MAKE-STRING-INPUT-STREAM MAKE-STRING-OUTPUT-STREAM NAMESTRING
NSTRING-CAPITALIZE NSTRING-DOWNCASE NSTRING-UPCASE PARSE-NAMESTRING
PRIN1-TO-STRING PRINC-TO-STRING READ-FROM-STRING SIMPLE-BASE-STRING
SIMPLE-STRING SIMPLE-STRING-P STRING STRING-CAPITALIZE STRING-DOWNCASE
STRING-EQUAL STRING-GREATERP STRING-LEFT-TRIM STRING-LESSP STRING-NOT-EQUAL
STRING-NOT-GREATERP STRING-NOT-LESSP STRING-RIGHT-TRIM STRING-STREAM
STRING-TRIM STRING-UPCASE STRING/= STRING< STRING<= STRING= STRING> STRING>=
STRINGP WITH-INPUT-FROM-STRING WITH-OUTPUT-TO-STRING WRITE-STRING
WRITE-TO-STRING)
来,对子一个base-string
继续哇呀挖:
(el:describe-symbol 'base-string)
COMMON-LISP:BASE-STRING
[symbol]
BASE-STRING names the built-in-class #<BUILT-IN-CLASS COMMON-LISP:BASE-STRING>:
Class precedence-list: BASE-STRING, STRING, VECTOR, ARRAY, SEQUENCE, T
Direct superclasses: STRING
Direct subclasses: SIMPLE-BASE-STRING
Sealed.
No direct slots.
BASE-STRING names a primitive type-specifier:
Lambda-list: (&OPTIONAL SIZE)
好奇先生的好奇心彻底爆炸,这里居然有很不一样的东西,base-string
是一个内置的类,继承自string
,并且有一个直接的子类simple-base-string
。它的祖先依次是string
、vector
、array
、sequence
、t
。这又是什么意思?而且这里的sequence
是什么?
所有的类都是t
这很好理解,因为除了nil
,其他所有东西都是t
,在面向对象的编程中,is
也就是是
的关系就是继承关系。那么sequence
是什么呢?
好奇先生把这里的Class precedence-list
, Direct superclasses
, Direct subclasses
都挖了出来,然后一路挖下去……
挖到了sequence
最后,好奇先生挖出来一个下面这样的继承关系图:
classDiagram
T <|-- SYMBOL
T <|-- SEQUENCE
T <|-- ARRAY
SIMPLE-ARRAY <|-- SIMPLE-VECTOR
SEQUENCE <|-- LIST
SEQUENCE <|-- VECTOR
ARRAY <|-- VECTOR
VECTOR <|-- STRING
VECTOR <|-- SIMPLE-VECTOR
STRING <|-- BASE-STRING
STRING <|-- SIMPLE-STRING
SIMPLE-ARRAY <|-- SIMPLE-STRING
ARRAY <|-- SIMPLE-ARRAY
SYMBOL <|-- NULL
LIST <|-- NULL
LIST <|-- CONS
SIMPLE-ARRAY <|-- SIMPLE-BIT-VECTOR
BASE-STRING <|-- SIMPLE-BASE-STRING
SIMPLE-STRING <|-- SIMPLE-BASE-STRING
啊哈,好奇先生觉得好玩极了!从这个图里可以看到,sequence
是一个很重要的类,它是list
和vector
的祖先,所有的跟字符串相关的类都是sequence
的子类。
那么还能怎么挖呢?当然是看看哪些操作(函数)直接定义在sequence
上了。
(sort (el:search-symbols "sequence" :common-lisp :doc-string t) #'string-lessp)
(* *FEATURES* BASE-STRING BIT-VECTOR CONCATENATE CONS COPY-SEQ COUNT COUNT-IF
COUNT-IF-NOT DELETE DELETE-DUPLICATES DELETE-IF DELETE-IF-NOT ELT EVERY FILL
FIND FIND-IF FIND-IF-NOT LENGTH LIST MAKE-SEQUENCE MAP MAP-INTO MAPHASH
MERGE MISMATCH NOTANY NOTEVERY NREVERSE NSUBSTITUTE NSUBSTITUTE-IF
NSUBSTITUTE-IF-NOT NULL POSITION POSITION-IF POSITION-IF-NOT PROGRAM-ERROR
READ-SEQUENCE REDUCE REMOVE REMOVE-DUPLICATES REMOVE-IF REMOVE-IF-NOT
REPLACE REVERSE SEARCH SEQUENCE SIMPLE-BASE-STRING SIMPLE-BIT-VECTOR
SIMPLE-CONDITION SIMPLE-STRING SIMPLE-VECTOR SOME SORT STABLE-SORT STRING
STRING-LEFT-TRIM STRING-RIGHT-TRIM STRING-TRIM SUBSEQ SUBSTITUTE
SUBSTITUTE-IF SUBSTITUTE-IF-NOT T THE TYPE-ERROR VECTOR
WITH-HASH-TABLE-ITERATOR WRITE-SEQUENCE)
真是完美,好奇先生的好奇心又双叒叕被点燃了。这里有很多操作,比如concatenate
、copy-seq
、count
、count-if
、count-if-not
、delete
、delete-duplicates
、delete-if
、delete-if-not
、elt
、every
、fill
、find
、find-if
、find-if-not
、length
、list
、make-sequence
、map
、map-into
、maphash
、merge
、mismatch
、notany
、notevery
、nreverse
、nsubstitute
、nsubstitute-if
、nsubstitute-if-not
、null
、position
、position-if
、position-if-not
、read-sequence
、reduce
、remove
、remove-duplicates
、remove-if
、remove-if-not
、replace
、reverse
、search
、sequence
、sort
、stable-sort
、string
、string-left-trim
、string-right-trim
、string-trim
、subseq
、substitute
、substitute-if
、substitute-if-not
、the
、type-error
、vector
、with-hash-table-iterator
、write-sequence
。
好奇先生是没法睡觉了……
结论
- Lisp自己学自己简直不要太开心
- Lisp的开发心态就是演进,利用动态绑定,随时更改函数的行为模式
- Lisp中的对象继承关系也能通过
describe
来探索 - Lisp会自己给自己写文档(Lisp实际上就是这么做的)