Common Lisp Tips
一些common lisp相关的知识点。
Table of Contents
- 1. 运行时检查变量类型
- 2. Common lisp I/O、流和文件
- 3. ldb (load byte)
- 4. register-system-packages
- 5. Remove invalid defmethod definition in the lisp images
- 6. 获取lisp版本
- 7. END
1 运行时检查变量类型
在Common lisp里,可以采用如下几种方式在运行的时候检查变量类型。
1.1 CHECK-TYPE
Macro CHECK-TYPE ;; 语法: check-type place typespec [string] => nil ;; 作用: 检查第一个参数的类型是否是第二个参数所指定的类型。 ;; 示例: CL-USER> (defun my-sqrt (x) (check-type x (real 0)) (sqrt x)) MY-SQRT CL-USER> (my-sqrt 9) 3.0 CL-USER> (my-sqrt -9) Type a form to be evaluated: 4 2.0 CL-USER> (check-type 9 (real 0)) ; in: CHECK-TYPE 9 ; (SETF 9 (SB-KERNEL:CHECK-TYPE-ERROR '9 #:G599 '(REAL 0))) ; ==> ; (SETQ 9 (SB-KERNEL:CHECK-TYPE-ERROR '9 #:G599 '(REAL 0))) ; ; caught ERROR: ; Variable name is not a symbol: 9. ; ; compilation unit finished ; caught 1 ERROR condition NIL ;; 注意点: a. 第一个参数会被求值,第二个参数不会被求值 b. 第一个参数不能只是普通的变量,其应当可以被setf设置 c. 如果第一个参数的类型不满足第二个参数指定的类型,那么会产生一个 TYPE-ERROR d. 产生 TYPE-ERROR 后提供一个restart选项,可以提供一个其他值作为第一个参数 e. check-type 的第三个参数是优化产生error后的提示信息
1.2 TYPEP
Function TYPEP ;; 语法: typep object type-specifier &optional environment => generalized-boolean ;; 作用: 检查object是否有type-specifier的类型 ;; 注意点: a. 不像check-type,typep是个函数,两个参数都会被求值,所以第二个类型参数需要quote传入 b. check-type不满足类型会产生error, typep会返还nil c. typep的第一个检查的参数不像check-type那样有限制
1.3 TYPE-OF
Function TYPE-OF ;; 语法: type-of object => typespec ;; 作用: 返回object的类型 ;;注意: 不同lisp实现之间的返回值可能不一致,如果追求可移植性,不建议使用
1.4 其它
;; 1. 在一些lisp实现比如sbcl里,可以采用 declare 等来确保变量类型, 如下 (defun my-sqrt (x) (declare (type (real 0) x)) (sqrt x)) 如果x为负数,那么会触发一个error,但没有restart选项。 但declare的可移植性是没法保证的。 ;; 2. typecase等,留待以后探究
2 Common lisp I/O、流和文件
这里介绍了common lisp I/O操作相关的一些操作符。
参考了Hyperspec和Common Lisp Recipes。
2.1 with-open-stream
将流绑定到变量上然后在表达式求值,最后关闭这个流。 相当于临时创建一个变量绑定来方便使用传入的流,最后再自动关闭
;; Syntax: with-open-stream (var stream) declaration* form* => result* ;; Examples: (with-open-stream (s (make-string-input-stream "1 2 3 4 5")) (+ (read s) (read s) (read s))) => 6 ;; Side Effects: The stream is closed (upon exit).
2.2 get-output-stream-string
注意:该方法每次调用会清空stream
Examples: (setq a-stream (make-string-output-stream) a-string "abcdefghijklm") => "abcdefghijklm" (write-string a-string a-stream) => "abcdefghijklm" (get-output-stream-string a-stream) => "abcdefghijklm" (get-output-stream-string a-stream) => ""
2.3 字符串流
2.3.1 with-input-from-string
创建一个字符串输入流供在表达式中求值使用。
;; Syntax: with-input-from-string (var string &key index start end) declaration* form* => result* (返回forms的求值结果) ;; Examples: (with-input-from-string (s "XXX1 2 3 4xxx" :index ind :start 3 :end 10) (+ (read s) (read s) (read s))) => 6 ind => 9 (with-input-from-string (s "Animal Crackers" :index j :start 6) (read s)) => CRACKERS The variable j is set to 15.
- index参数
作用:读取内容的时候同时设置index为读取到的位置的后一位,用做标记
(loop with ptr = 0 with eof = (gensym) for from = ptr for object = (with-input-from-string (s "41 42 43" :start ptr :index ptr) (read s nil eof)) until (eq object eof) do (format t "~A-~A: ~A~%" from ptr object) finally (return (values))) 0-3: 41 3-7: 42 7-9: 43
注意点:
- read的行为,为什么ptr依次是3,7,9
- eof设置为gensym,来判断是否到达结尾
- loop宏里通过设置until来达到遇到eof终止
- loop里通过两个with设置初始变量,两个for来设置循环时变量的改动
2.3.2 with-output-to-string
创建一个输出流,所有输出到该流上的内容转化成字符串。
;; Syntax: with-output-to-string (var &optional string-form &key element-type) declaration* form* => result* (如果提供了string-form, 那么结果是forms的求值结果;如果没有提供参数string-form,那么输出流生成的字符串会被返回) ;; Example: (setq fstr (make-array '(0) :element-type 'base-char :fill-pointer 0 :adjustable t)) => "" (with-output-to-string (s fstr) (format s "here's some output") (input-stream-p s)) => false fstr => "here's some output"
2.3.3 make-string-input-stream, make-string-output-stream
make-string-input-stream
: 从字符串创建一个输入流,然后返回这个流
Examples: (let ((string-stream (make-string-input-stream "1 one "))) (list (read string-stream nil nil) (read string-stream nil nil) (read string-stream nil nil))) => (1 ONE NIL) (read (make-string-input-stream "prefixtargetsuffix" 6 12)) => TARGET
make-string-output-stream
: 生成一个字符串输出流。可以用 get-output-stream-string
取得字符串输出流的内容
Examples: (let ((s (make-string-output-stream))) (write-string "testing... " s) (prin1 1234 s) (get-output-stream-string s)) => "testing... 1234"
2.4 let临时改变动态变量绑定
临时将标准输入流和标准输出流绑定到某一流变量上,改变标准输入流的来源和标准输出流去向
CL-USER> (with-input-from-string (s "abcde") (let ((*standard-input* s)) (make-list 3 :initial-element (read)))) => (ABCDE ABCDE ABCDE)
2.4.1 Synonym stream
当需要一个根据求值环境动态求值到不同值的流是, synonym stream 就发挥作用。看下面例子:
;; 设置字符串流 a-stream 和 b-stream (setq a-stream (make-string-input-stream "a-stream") b-stream (make-string-input-stream "b-stream")) => #<String Input Stream> ;; 设置s-stream是一个符号'c-stream的同义流 (setq s-stream (make-synonym-stream 'c-stream)) => #<SYNONYM-STREAM for C-STREAM> ;; 将c-stream 值设为 a-stream (setq c-stream a-stream) => #<String Input Stream> ;; 这时读取s-stream 就相当于读取的 c-stream 绑定的 a-stream (read s-stream) => A-STREAM ;; 又将c-stream设置为b-stream (setq c-stream b-stream) => #<String Input Stream> ;; 这时读取s-stream 就相当于读取的 c-stream 绑定的 b-stream (read s-stream) => B-STREAM
2.5 Flush output to stream
Lisp输出流通常是buffered,所以发送给输出流的内容不一定会立即在物理设备上显示出来,想立即显示可以用
finish-output
和 force-output
,两者区别是前者会等待输出流的buffer清空后再返回,而后者当启动清空的流程就返回了
这导致后者不会报告任何写出到流的错误
2.6 读取文件相关
2.6.1 with-open-file
使用open方法打开一个一个文件,绑定到一个流变量上
;; Syntax: with-open-file (stream filespec options*) declaration* form* => results ;; Examples: (setq p (merge-pathnames "test")) => #<PATHNAME :HOST NIL :DEVICE device-name :DIRECTORY directory-name :NAME "test" :TYPE NIL :VERSION :NEWEST> (with-open-file (s p :direction :output :if-exists :supersede) (format s "Here are a couple~%of test data lines~%")) => NIL (with-open-file (s p) (do ((l (read-line s) (read-line s nil 'eof))) ((eq l 'eof) "Reached end of file.") (format t "~&*** ~A~%" l))) >> *** Here are a couple >> *** of test data lines => "Reached end of file."
2.6.2 alexandria: read-file-into-byte-vector read-file-into-string
— Function: read-file-into-string pathname &key buffer-size external-format 将pathname指定的文件内容读取,返回一个新建的字符串。内部实际调用with-open-file — Function: read-file-into-byte-vector pathname 将pathname指定的文件内容读取,返回一个新建的octets。 参考: https://common-lisp.net/project/alexandria/draft/alexandria.html#IO
2.6.3 自定义函数: file-at-once 一次性读取文件内容
- 实现
(defun file-at-once (filespec &rest open-args) (with-open-stream (stream (apply #'open filespec open-args)) (let* ((buffer (make-array (file-length stream) :element-type (stream-element-type stream) :fill-pointer t)) (position (read-sequence buffer stream))) (setf (fill-pointer buffer) position) buffer)))
- stream-element-type
返回可能从流对象读取或写入的对象类型
- file-length
输入一个绑定到文件的流,返回它的长度 如果是二进制文件,那么长度是根据流里的
element type
衡量的
2.7 广播流 make-broadcast-stream
当需要同时向多个流发送数据时,可以采用此函数.
(setq a-stream (make-string-output-stream) b-stream (make-string-output-stream)) => #<String Output Stream> (format (make-broadcast-stream a-stream b-stream) "this will go to both streams") => NIL (get-output-stream-string a-stream) => "this will go to both streams" (get-output-stream-string b-stream) => "this will go to both streams"
注意,如果 make-broadcast-stream
不接受参数,那么发送给该流的内容会被发送给"/dev/null",也就是会丢弃。
3 ldb (load byte)
ldb从整数里取出byte.
语法: ldb bytespec integer => byte
bytespec 是类似 (byte 8 0)
这样的格式,意思是8bit作为一个单位,取第0个单位。
第一个参数称为size,第二个参数称为position。 意思是,去第position的位置,取出size的大小。
返回的byte是非负的表示取出的bytes的整数。
规范早于目前通用的8个bit当作一个byte,是超集。
position是怎么算的呢?
数学上来说,取出的byte数字和原来的integer:
byte的bit位上 0<->s-1位 (~2^0 -> 2^(s-1))
和 integer位上的 p <->p+s-1 ~( -> 2^p -> 2^(p+s-1)
是相同的。
直观的理解是,对于一个数的bit位表示,比如 40: #b101000
(byte 3 2)
意思是从右边看是数,index=2到index=4的一段,也就是#b010,也就是2
这个有什么用呢?可以结合integer-length,实用的有
- 可以依次取出一个integer里的bit位
(defun list-of-bits (integer) (loop for position below (integer-length integer) collect (ldb (byte 1 position) integer))) ;; 这个取出来的和integer的二进制#b表示实际上是相反的, ;; 因为ldb从右侧开始取,每次取一个bit,然后收集到列表里。 ;; 可以通过push来改变顺序
- 可以依次取出一个integer的byte位
;;; 摘自ironclad src/public-key/publick-key.lisp (defun integer-to-octets (bignum &key n-bits (big-endian t)) (declare (optimize (speed 3) (space 0) (safety 1) (debug 0))) (let* ((n-bits (or n-bits (integer-length bignum))) (bignum (ldb (byte n-bits 0) bignum)) (n-bytes (ceiling n-bits 8)) ;; 计算出有多少bytes (octet-vec (make-array n-bytes :element-type '(unsigned-byte 8)))) (declare (type (simple-array (unsigned-byte 8) (*)) octet-vec)) (if big-endian (loop for i from (1- n-bytes) downto 0 ;; i取值: bytes数量-1,依次递减 for index from 0 ;; index取值:0,1,2,3 do (setf (aref octet-vec index) (ldb (byte 8 (* i 8)) bignum) ;; 这里先取的是#b表示法左端的,一直往右去,取了之后放到array里 finally (return octet-vec)) (loop for i from 0 below n-bytes;; i取值: 0,1,2,3,... for byte from 0 by 8 ;; byte取值: 0,8,16,... do (setf (aref octet-vec i) (ldb (byte 8 byte) bignum)) ;; 这里先取的是#b表示法右端的,一直往左去,取了之后放到array里 finally (return octet-vec))))) ;;; 当然涉及到integer和bytes的转化,自然会涉及大小端表示法. ;;; 其实上面的bit位表示也可分所谓大端小端,然而约定的都是大端的。
规范说明具体参见hyperspec说明。
有意思的是,下面的方法应该是一样的作用:
(ldb (byte 1 i) number) == (logand 1 (ash number (- 0 i))) 这里的i是number的bit位,比如i可以从0取到(integer-length number) -1 .
左边表示从number的bit位右边开始取,每次取一个bit。
右边表示将number bit位每次右移1位,然后取出来(比特AND 1就是将位取出来的作用。或者除以2,也是一样的意思).
4 register-system-packages
采用每个文件一个system和package的形式来组织项目时,会遇到一个问题,
在 define-package
的时候,采用 :use
或者 :mix
的时候,会按照使用的 package-name
来加载同名的 system。
所以如果一个叫做 foo
的system里包含了 foo
和 foo.bar
两个package,
那么想直接use foo.bar
这个package会有问题, 因为当把 foo.bar
写到 :use后的时候,asdf会去加载 foo.bar 这个system。而这是没有的。
解决办法是通过 register-system-packages
来定义foo.bar package和foo system关联。像下面这样使用:
(register-system-packages "foo" '(:foo :foo.bar))
这样可以直接使用foo.bar里export的symbol了。
5 Remove invalid defmethod definition in the lisp images
Method1:
;; 1.first find it: CL-ETH/ETH2/SSZ> (find-method #'serialize () (list (find-class 'tvector) (find-class t) )) #<STANDARD-METHOD CL-ETH/ETH2/SSZ::SERIALIZE (TVECTOR T) {10035B47A3}> ;; 2.then remove: CL-ETH/ETH2/SSZ> (remove-method #'serialize *) #<STANDARD-GENERIC-FUNCTION CL-ETH/ETH2/SSZ::SERIALIZE (5)>
- Use slime/sly Inspect it, then remove in the slime/sly interface.
6 获取lisp版本
CL-USER> (format t "~a, ~a~%" (lisp-implementation-type) (lisp-implementation-version)) SBCL, 1.3.19 NIL ;; 查看是否支持线程 CL-USER> (member :thread-support *FEATURES*) NIL