Published: 2020-07-04

Common Lisp Tips

一些common lisp相关的知识点。

Table of Contents

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.

  1. 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
    
    

    注意点:

    1. read的行为,为什么ptr依次是3,7,9
    2. eof设置为gensym,来判断是否到达结尾
    3. loop宏里通过设置until来达到遇到eof终止
    4. 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-outputforce-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 一次性读取文件内容

  1. 实现
    (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)))
    
  2. stream-element-type

    返回可能从流对象读取或写入的对象类型

  3. 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,实用的有

  1. 可以依次取出一个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来改变顺序
  1. 可以依次取出一个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里包含了 foofoo.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)>
  1. 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

7 END

Author: Nisen

Email: imnisen@163.com