美文网首页
0基础——lisp学习笔记(二)

0基础——lisp学习笔记(二)

作者: ayusong870 | 来源:发表于2020-06-03 07:02 被阅读0次

目录:

  1. Hello,world
  2. A Simple Database
  3. 语法和语义(待补充)
  4. 函数(Functions)
  5. 变量
  6. 序列变量的基本操作
  7. 标准宏
  8. 自定义宏(Macors)
  9. 数字、字符和字符串
    参考文献

3. 语法和语义(待补充)

在介绍lisp的语法和语义之前,首先了解一下它与其他语言的不同之处是非常必要的。大都数编程语言,无论是解析型还是编译型,对语法的操作都是像在黑匣子里。你将一串的表达式或语句传递给黑匣子,而程序的执行行为和编译版本都取决于它的编译器或解析器。
当然,在黑匣子中,语言处理器通常分为子系统,每个子系统负责将程序文本翻译成行为或目标代码的一部分。一个典型的划分是将处理器分成三个阶段,每一个阶段连接下一个阶段:词法分析器将字符流分解为符号,并将它们馈送到一个语法分析器中,该解析器根据程序语言的语法生成程序中表达式的树。这棵树被称之为抽象语法树,然后进入一个计算器,把它直接或将它编译成其他语言如机器代码。因为语言处理器是一个黑匣子,处理器使用的数据结构,如符号和抽象语法树,只有编译器或解析器的实现者清楚。
在lisp中事情有一些不同,程序运行结果与编译器和如何书写代码都有关系。与一个黑匣子一步决定程序行为不同,lisp定义了两个黑匣子。一个将文本转换为lisp对象称之为reader,而另一个实现程序中的这些对象的语义,称之为evaluator。
Lisp主要包含两种结构list和atom。

4. 函数(Functions)

Lisp中最基本的三个组成部分为函数(Functions)、变量(Variables)和宏(Macros)。其中Functions是所有编程语言中实现抽象的最基本的机制。实际上宏也是通过函数来实现的,只不过宏是在编译时构建。
在lisp中通过DEFUN宏来定义新的函数,最基本的定义骨架如下:

(defun name (parameter*)
  "Optional documentation string."
  body-form*)

函数名称可以是任何符号,例如定义++作为一个函数实现输入参数的自加运算。

? (defun ++ (a)
(+ a 1))
++
? (++ 10)
11

函数定义骨架中表示的"Optional documentation string."说明在函数声明下面第一行“”内包括的内容是函数的说明。函数的参数列表有很多种形式,lisp提供了很多复杂提供参数的方式。

4.1. 可选择参数(Optional Parameter)

在lisp函数声明的参数中,可以使用&optional符号声明在此之后的参数是选择性给出的,可选择参数的默认值为NIL,也可以为可选择参数提供默认值。在可选参数的默认值后面增加参数名+”-supplied-p”可以显示表示是否选择给出了该参数的值,例如参数c默认值为3在默认值后加c-supplied-p那么如果c取默认值则c-supplied-p为NIL反之为T,以下为示例:

(defun foo (a b &optional c d) (list a b c d))
(foo 1 2)     ==> (1 2 NIL NIL)
(foo 1 2 3)   ==> (1 2 3 NIL)
(foo 1 2 3 4) ==> (1 2 3 4)

声明默认值:

(defun foo (a &optional (b 10)) (list a b))
(foo 1 2) ==> (1 2)
(foo 1)   ==> (1 10)

判定有默认值的参数是否赋予值:

(defun foo (a b &optional (c 3 c-supplied-p))
  (list a b c c-supplied-p))
(foo 1 2)   ==> (1 2 3 NIL)
(foo 1 2 3) ==> (1 2 3 T)
(foo 1 2 4) ==> (1 2 4 T)  

4.1. 剩余参数(Rest Parameters)

Lisp允许在函数参数中引入&rest符号,表示在其后面的参数数量可以不限制。下面的声明是lisp中format函数与+函数的声明方式,rest符号一定在参数声明的最后:

(defun format (stream string &rest values) ...)
(defun + (&rest numbers) ...)

4.3. 关键字参数

假设我有三个输入参数,但是我只想给第一个和第三个参数赋值怎么办?换句话说指定特定的输入参数值,此时可以用关键字参数来实现,&key符号后面的参数均为关键字参数,可以使用:named的方式为特定参数赋值。

(defun foo (&key a b c) (list a b c))
(foo)                ==> (NIL NIL NIL)
(foo :a 1)           ==> (1 NIL NIL)
(foo :b 1)           ==> (NIL 1 NIL)
(foo :c 1)           ==> (NIL NIL 1)
(foo :a 1 :c 3)      ==> (1 NIL 3)
(foo :a 1 :b 2 :c 3) ==> (1 2 3)
(foo :a 1 :c 3 :b 2) ==> (1 2 3)

关键字参数同样支持默认值和-supplied-p。

(defun foo (&key (a 0) (b 0 b-supplied-p) (c (+ a b)))
  (list a b c b-supplied-p))
(foo :a 1)           ==> (1 0 1 NIL)
(foo :b 1)           ==> (0 1 1 T)
(foo :b 1 :c 4)      ==> (0 1 4 T)
(foo :a 2 :b 1 :c 4) ==> (2 1 4 T)

也可以为参数强调特别的key名称,而不用:named模式。

(defun foo (&key ((:apple a)) ((:box b) 0) ((:charlie c) 0 c-supplied-p))
  (list a b c c-supplied-p))
(foo :apple 10 :box 20 :charlie 30) ==> (10 20 30 T)

Lisp函数参数的几种形式可以混合,&optional和&rest配合、&rest和&key配合。

4.4. 函数返回值

RETURN-FROM宏可以用来立刻退出函数,并返回特定值(实际上RETURN-FROM不仅仅用来返回函数,也可以用来返回BLOCK)。

(defun foo (n)
  (dotimes (i 10)
    (dotimes (j 10)
      (when (> (* i j) n)
        (return-from foo (list i j))))))

4.5. 函数也是数据

在lisp中函数也是可以作为数据传递的,函数只是用DEFUN定义的对象。FUNCTION操作提供一直获得函数对象的机制,例如:

CL-USER> (defun foo (x) (* 2 x))
FOO
CL-USER> (function foo)
#<Interpreted Function FOO

’符号实际上就是FUNCTION

CL-USER> #'foo
#<Interpreted Function FOO

对于函数对象有两个操作FUNCALL和APPLY,当知道函数的具体参数数量时使用FUNCALL,例:

(foo 1 2 3) === (funcall #'foo 1 2 3)

实际上在你写函数时,有时候不知道传递进来的函数名称,但是知道参数数量,这时候才是funcall的用武之地,例:

(defun plot (fn min max step)
  (loop for i from min to max by step do
        (loop repeat (funcall fn i) do (format t "*"))
        (format t "~%")))
CL-USER> (plot #'exp 0 4 1/2)
*
*
**
****
*******
************
********************
*********************************
******************************************************
NIL

APPLY接收任意数量的参数(统一为一个list),并且不限制&optional、&rest和&key。使用方法与FUNCALL类似。

4.6. 匿名函数

匿名函数在并不想为一段代码定义专门的函数时使用(当然,不用函数还不行),例如:

(defun double (x) (* 2 x))
(plot #'double 0 10 1)

函数double与(lambda (x) (* 2 x))功能是一样的,但是仅仅为了这个单独写一个函数可能不值当,那么就用匿名函数,这样代码更简洁,通常匿名函数都很短:

(plot #'(lambda (x) (* 2 x)) 0 10 1)

5. 变量

Lisp支持的变量有两大种类:lexical和dynamic。Lisp与其他语言例如Java或C/C++最大的区别就是lisp变量没有具体的类型,在实现上属于动态类型。
对于变量在函数中的传递,lisp每次调用函数,都会创建新的连接给函数的调用者。如果传递给函数的参数是可变的(mutable)则在函数中的修改会影响参数,否则没有影响。
LET操作用来初始化新的变量。

(let (variable*)
  body-form*)
(let ((x 10) (y 20) z)
  ...)  

LET操作定义的变量相当于重新定义局部变量有效范围仅是body-form内,不会对同名全局变量产生影响。

(defun foo (x)
 (format t "Parameter: ~a~%" x)      ; |<------ x is argument 
 (let ((x 2))                        ; |
   (format t "Outer LET: ~a~%" x)    ; | |<---- x is 2
   (let ((x 3))                      ; | |
     (format t "Inner LET: ~a~%" x)) ; | | |<-- x is 3
   (format t "Outer LET: ~a~%" x))   ; | |
 (format t "Parameter: ~a~%" x))
CL-USER> (foo 1)
Parameter: 1
Outer LET: 2
Inner LET: 3
Outer LET: 2
Parameter: 1
NIL

另外还有LET操作,基本与LET相同,尽在定义变量时LET可以引用在LET*中定义较早的变量。

 (let* ((x 10)
       (y (+ x 10)))
  (list x y))

而LET只能这样:

(let ((x 10))
  (let ((y (+ x 10)))
(list x y)))

5.1. Lexical变量和闭包(Closures)

变量的作用范围是件神奇的事情

(let ((count 0)) #'(lambda () (setf count (1+ count))))

Count之所以能传递到匿名函数,是因为匿名函数在let方法的体内,而将上面语句定义为参数,每次通过FUNCALL调用时,count就又变成了类似于全局变量的而实际上又是局部变量(因为你如果直接访问count是找不到这个变量的,只有通过函数fn才能找到),这个特性实际上可以为变量提供某种安全机制。

(defparameter *fn* (let ((count 0)) #'(lambda () (setf count (1+ count)))))
CL-USER> (funcall *fn*)
1
CL-USER> (funcall *fn*)
2
CL-USER> (funcall *fn*)
3

5.2. Dynamic,a.k.a Special,Variables

Lisp的全局函数声明有两种方式DEFVAR和DEFPARAMETER:

(defvar *count* 0
  "Count of widgets made so far.")

(defparameter *gap-tolerance* 0.001
  "Tolerance to be allowed in widget gaps.")

至于defvar与defparameter的不同,暂时只知道defvar可以不赋初始值。定义完的全局变量,就可以在全程序段使用了。
下面是一些对let使用的样例:

(defvar *x* 10)
(defun foo () (format t "X: ~d~%" *x*))
(defun bar ()
  (foo)
  (let ((*x* 20)) (foo))
  (foo))
CL-USER> (bar)
X: 10
X: 20
X: 10
NIL

另外:

(defun foo ()
  (format t "Before assignment~18tX: ~d~%" *x*)
  (setf *x* (+ 1 *x*))
  (format t "After assignment~18tX: ~d~%" *x*))
CL-USER> (bar)
Before assignment X: 11
After assignment  X: 12
Before assignment X: 20
After assignment  X: 21
Before assignment X: 12
After assignment  X: 13
NIL

5.3. 常数

Lisp使用DEFCONSTANT定义全局常数,由于lisp在变量命名上限制非常小,可以使用-或者+等特别标记下该变量时全局常数例如:+c1+。常数不可重定义,不可修改。

5.4. 赋值

Setf是lisp中的基本赋值语句,格式如下:
(setf place value ...)
其他语言的赋值语句如下:

Assigning to ... Java, C, C++ Perl Python
... variable x = 10; $x = 10; x = 10
... array element a[0] = 10; $a[0] = 10; a[0] = 10
...hash table entry -- $hash{'key'} = 10; hash['key'] = 10
... field in object o.field = 10; $o->{'field'} = 10; o.field = 10

在lisp中实现各种赋值样例如下,aref用来索引数组,gethash用来索引哈希表,field o相当于o.field:

名称 代码
Simple variable: (setf x 10)
Array: (setf (aref a 0) 10)
Hash table: (setf (gethash 'key hash) 10)
Slot named 'field': (setf (field o) 10)

5.5. 其他修改数据的方法

除setf之外,还有INCF(相当于++)、DECF(相当于--)、PUSH、PUSHNEW、POP。
还有ROTATEF和SHIFTF,其中ROTATEF用来交换两个变量:

(rotatef a b)

对于普通的变量,相当于

(let ((tmp a)) (setf a b b tmp) nil)

SHIFTF命令相当于将参数列表中右侧的值付给左侧的值:

(shiftf a b 10)

相当于

(let ((tmp a)) (setf a b b 10) tmp)

6. 序列变量的基本操作

6.1. 向量(Vectors)和一维数组(Arrays)

可以使用VECTOR函数定义向量,在lisp中向量可以是任意维度的,但是生成后大小是固定的。

(vector)     → #() 
(vector 1)   → #(1) 
(vector 1 2) → #(1 2) 
(vector 1 2 3 4) → #(1 2 3 4) 

MAKE-ARRAY是更常用的方式用来生成数组(或者说高维向量,试想下用vector创建20维的向量,需要打20个0....)。下面的代码生成了大小为5的数组,:initial-element表示以nil初始化每一个元素。

(make-array 5 :initial-element nil) → #(NIL NIL NIL NIL NIL) 

:fill-pointer用来定义某初始大小的可变长度数组(向量),下面定义了最大尺寸为5的可调整大小的向量:

(make-array 5 :fill-pointer 0) → #() 

使用VECTOR-PUSH函数,向可调整大小向量中添加元素。使用VECTOR-POP函数在堆中弹出元素。

(defparameter *x* (make-array 5 :fill-pointer 0)) 

(vector-push 'a *x*) → 0 
*x*                  → #(A) 
(vector-push 'b *x*) → 1 
*x*                  → #(A B) 
(vector-push 'c *x*) → 2 
*x*                  → #(A B C) 
(vector-pop *x*)     → C 
*x*                  → #(A B) 
(vector-pop *x*)     → B 
*x*                  → #(A) 
(vector-pop *x*)     → A 
*x*                  → #() 

若要使向量不限制尺寸还需要传递:adjustable参数,使用VECTOR-PUSH-EXTEND向其中添加元素。

(make-array 5 :fill-pointer 0 :adjustable t) → #() 

函数LENGTH可以得到向量的大小,ELT可以通过索引得到向量中的数据。

? (defparameter *x* (vector 1 2 3))
*X*
? (length *x*)
3
? (elt *x* 0)
1
;(elt *x* 1)  → 2 
;(elt *x* 2)  → 3 
;(elt *x* 3)  → error 
? (setf (elt *x* 0) 10)
10
? *x*
#(10 2 3)

6.2. 序列的迭代函数(一些常规的索引方法)

针对序列lisp还提供了很多迭代类索引函数:

名称 需要的参数 返回值
COUNT 元素和序列 序列中元素出现的次数
(count 1 #(1 2 1 2 3 1 2 3 4)) 3
FIND 元素和序列 找到的元素或NIL
(find 1 #(1 2 1 2 3 1 2 3 4)) 1
(find 10 #(1 2 1 2 3 1 2 3 4)) NIL
POSITION 元素和序列 元素的位置或NIL
(position 1 #(1 2 1 2 3 1 2 3 4)) 0
REMOVE 元素和序列 删除元素后的序列
remove 1 #(1 2 1 2 3 1 2 3 4)) #(2 2 3 2 3 4)
(remove 1 '(1 2 1 2 3 1 2 3 4)) (2 2 3 2 3 4)
(remove #\a "foobarbaz") "foobrbz"
SUBSTITUTE 新的元素,元素和序列 替换后的新序列
(substitute 10 1 #(1 2 1 2 3 1 2 3 4)) #(10 2 10 2 3 10 2 3 4)
(substitute 10 1 '(1 2 1 2 3 1 2 3 4)) (10 2 10 2 3 10 2 3 4)
(substitute #\x #\b "foobarbaz") "fooxarxaz"

参数列表:

参数 描述
:test 用来比较元素的方法(两参数)(或通过:key功能提取的值),默认为:EQL
- (count "foo" #("foo" "bar" "baz") :test #'string=) → 1
? (defun verstring= (x y) 
   (format t "Looking at ~s to ~s ~%" x y) (string= x y))
VERSTRING=
? (count "foo" #("foo" "bar" "baz") :test #'verstring=)
Looking at "foo" to "foo"
Looking at "foo" to "bar"
Looking at "foo" to "baz"
1
参数 描述
:key 从实际序列提取关键字的方法(一参数),NIL表示将元素当做is处理,默认为:NIL
- (find 'c #((a 10) (b 20) (c 30) (d 40)) :key #'first) → (C 30)
- (find 'a #((a 10) (b 20) (a 30) (b 40)) :key #'first) → (A 10)
- (find 'a #((a 10) (b 20) (a 30) (b 40)) :key #'first :from-end t) → (A 30)
? (defun verbose-first (x) (format t "Looking at ~s~%" x) (first x))
VERBOSE-FIRST
? (count 'a #((a 10) (b 20) (a 30) (b 40)) :key #'verbose-first)
Looking at (A 10)
Looking at (B 20)
Looking at (A 30)
Looking at (B 40)
2
参数 描述
:from-end 如果为真,遍历顺序为反向,默认为NIL
- (find 'a #((a 10) (b 20) (a 30) (b 40)) :key #'first :from-end t) → (A 30)
- (remove #\a "foobarbaz" :count 1 :from-end t) →"foobarbz"
参数 描述
- -
:count 指示要删除或替换或不表示所有的元素的数目(仅移除和替换)。
:start 子序列的开始索引值(包含)
:end 子序列的结束索引值(不包含)

Lisp提供了一些更高级的索引方法,通过以上基本的方法添加-if或-if-not已实现满足一些复杂判断条件的索引效果,例如:

? (count-if #'evenp #(1 2 3 4 5))
2
? (count-if-not #'evenp #(1 2 3 4 5))
3
? (position-if #'digit-char-p "abcd0001")
4
? (remove-if-not #'(lambda (x) (char= (elt x 0) #\f))
 #("foo" "bar" "baz" "foom"))
#("foo" "foom")

(count-if #'evenp #((1 a) (2 b) (3 c) (4 d) (5 e)) :key #'first)     → 2 
(count-if-not #'evenp #((1 a) (2 b) (3 c) (4 d) (5 e)) :key #'first) → 3 
(remove-if-not #'alpha-char-p 
  #("foo" "bar" "1baz") :key #'(lambda (x) (elt x 0))) → #("foo" "bar")

比较特殊的remove还可以添加-duplicates移除重复元素:

(remove-duplicates #(1 2 1 2 3 1 2 3 4)) → #(1 2 3 4) 

CONCATENATE函数将多个序列组合为一个序列:

(concatenate 'vector #(1 2 3) '(4 5 6))    → #(1 2 3 4 5 6) 
(concatenate 'list #(1 2 3) '(4 5 6))      → (1 2 3 4 5 6) 
(concatenate 'string "abc" '(#\d #\e #\f)) → "abcdef" 

6.3. 排序与合并(Sort和Merging)

行数SORT与STABLE-SORT提供了两种排序序列的方式,这两种函数都需要两个参数并且返回一个排序好的序列:

(sort (vector "foo" "bar" "baz") #'string<) → #("bar" "baz" "foo")  

Merg函数将两个序列有序的合并为一个:

(merge 'vector #(1 3 5) #(2 4 6) #'<) → #(1 2 3 4 5 6) 
(merge 'list #(1 3 5) #(2 4 6) #'<)   → (1 2 3 4 5 6) 

6.4. 子序列操作

subseq返回一个序列的子序列:

(subseq "foobarbaz" 3)   → "barbaz" 
(subseq "foobarbaz" 3 6) → "bar"

Subseq的子序列的修改是可以对原始序列产生影响的:

(defparameter *x* (copy-seq "foobarbaz")) 

(setf (subseq *x* 3 6) "xxx")  ;子序列和替换序列长度相同 
*x* →   "fooxxxbaz" 

(setf (subseq *x* 3 6) "abcd") ; 替换序列比子序列长,忽略多余 
*x* →   "fooabcbaz" 

(setf (subseq *x* 3 6) "xx")   ; 替换序列比子序列短,仅替换两个 
*x* →   "fooxxcbaz" 

可以用FILL函数将序列中的多个元素替换为某一个值,:start和:end参数可以用来限制子序列的行为。
Search函数与POSITION函数类似,但可以在序列中搜索子序列:

(position #\b "foobarbaz") → 3 
(search "bar" "foobarbaz") → 3

MISMATCH函数用来找到两个序列第一次出现不匹配的位置,若完全匹配则返回NIL。当然也可以使用from-end来从末尾开始检索。

(mismatch "foobarbaz" "foom") → 3 
(mismatch "foobar" "bar" :from-end t) → 3

6.5. 序列的整体判定

EVERY、SOME、NOTANY、NOTEVERY:

(every #'evenp #(1 2 3 4 5))    → NIL 
(some #'evenp #(1 2 3 4 5))     → T 
(notany #'evenp #(1 2 3 4 5))   → NIL 
(notevery #'evenp #(1 2 3 4 5)) → T
(every #'> #(1 2 3 4) #(5 4 3 2))    → NIL 
(some #'> #(1 2 3 4) #(5 4 3 2))     → T 
(notany #'> #(1 2 3 4) #(5 4 3 2))   → NIL 
(notevery #'> #(1 2 3 4) #(5 4 3 2)) → T

6.6. MAPPING函数

要求两个向量每一个元素的乘积得到的新向量:
(1 2 3 4 5).*(10 9 8 7 6)=(10 18 24 28 30)
可以利用MAP函数:

(map 'vector #'* #(1 2 3 4 5) #(10 9 8 7 6)) → #(10 18 24 28 30) 

将a、b、c的和保存到a中可以用MAP-INTO:

(map-into a #'+ a b c) 

REDUCE函数仅对一个序列进行操作,下面的代码计算出序列的和:

reduce #'+ #(1 2 3 4 5 6 7 8 9 10)) → 55 

下面的代码求得一个序列的最大值:

reduce #'+ #(1 2 3 4 5 6 7 8 9 10)) → 10

REDUCE支持:key、:from-end、:start和:end关键参数。

6.7. 哈希列表

通常使用make-hash-table创建一个空的哈希表,gethash用来获取hash表中的值,没有当前关键字对应的值则返回NIL,gethash的结果是可以直接修改的,gethash返回两个值,第一个为value第二个是布尔变量表示是否有当前键值。

(defparameter *h* (make-hash-table)) 
(gethash 'foo *h*) →   NIL 
(setf (gethash 'foo *h*) 'quux) 
(gethash 'foo *h*) →   QUUX 

MULTIPLE-VALUE-BIND创建变量的绑定,用法与LET类似。下面的函数将value与gethash返回的键值和present与gethash返回的是否存在绑定。

(defun show-value (key hash-table) 
  (multiple-value-bind (value present) (gethash key hash-table) 
    (if present 
      (format nil "Value ~a actually present." value) 
      (format nil "Value ~a because key not found." value)))) 

(setf (gethash 'bar *h*) nil) ; provide an explicit value of NIL 

(show-value 'foo *h*) →  "Value QUUX actually present." 
(show-value 'bar *h*) →  "Value NIL actually present." 
(show-value 'baz *h*) →  "Value NIL because key not found." 

可以用maphash函数来遍历哈希列表,例如下面的代码遍历并打印了整个哈希列表:

(maphash #'(lambda (k v) (format t "~a => ~a~%" k v)) *h*) 

REMHASH可以用来删除哈希表中的值:

(maphash #'(lambda (k v) (when (< v 10) (remhash k *h*))) *h*) 

遍历哈希表也可以通过loop(loop的具体介绍后面在所,下面来看一下这个跟白话一样的代码见识一下):

(loop for k being the hash-keys in *h* using (hash-value v) 
  do (format t "~a => ~a~%" k v)) 

相关文章

  • 0基础——lisp学习笔记(二)

    目录: Hello,world A Simple Database 语法和语义(待补充) 函数(Functions...

  • 0基础——lisp学习笔记(一)

    目录: Hello,world A Simple Database 语法和语义(待补充) 函数(Functions...

  • 0基础——lisp学习笔记(三)

    目录: Hello,world A Simple Database 语法和语义(待补充) 函数(Functions...

  • **lisp** 学习笔记

    开发环境 开发环境: SBCLEmacsSlime:在emacs里面帮助进行common lisp开发的扩展qui...

  • 我也说说Emacs吧(7) - Lisp基础

    Lisp基础 Lisp是仅次于Fortran的第二古老的著名计算机语言。Lisp从一开始就与众不同的一点在于,它是...

  • Lisp 学习二

    Form在 Lisp 里,我们用单一的表示法,来表达所有的概念。 Lisp 使用前缀表达式 quote 一个不遵守...

  • 欢迎来到Lisp

    0. Lisp系统的交互式前端(REPL) 刚接触Lisp,会发现每种Lisp的实现都会带有REPL(read -...

  • LISP基础

    部分摘录于《ANSI COMMON LISP》、《LISP语言(陈光喜)》 变量与赋值 let来完成局部变量的定义...

  • python面向对象学习笔记-01

    学习笔记 # 0,OOP-Python面向对象 - Python的面向对象 - 面向对象编程 - 基础 -...

  • JavaWeb博客文章目录

    转载 ginb 一、JavaWeb基础 JavaWeb学习笔记一: XML解析 JavaWeb学习笔记二 Ht...

网友评论

      本文标题:0基础——lisp学习笔记(二)

      本文链接:https://www.haomeiwen.com/subject/htqlkhtx.html