ANSI Common lisp1
lisp(本文专指common lisp)语言简介
lisp程序员能够并且经常编写一些能够写程序的程序,对于程序生成程序的这种特性,
因为lisp是主流语言中唯一一个提供一些方便的抽象来让你完成这个任务的语言,所以
lisp是主流语言中唯一一个广泛运用这个特性的语言。
编程其实就是寻求编写最优美的程序 ---高德纳
lisp黑客精神可以用两句话概括:编程应该是有趣的,程序应该是优美的。
约翰麦卡锡和他的学生于1958年展开lisp的初次实现工作,lisp是继Fortran之后,仍在使用
的最古老的的程序语言,他仍然走在程序语言技术的最前面,懂lisp的程序员会告诉你,某种
东西使得lisp与众不同。
lisp与众不同的部分语言是它被设计成能够自己进化,当新的抽象概念风行时,这些新概念
在lisp中是最容易实现的,lisp就像DNA一样,永远不会过时。
程序语言教你不要做他没有提供的事情。因为可扩展的思想深植于lisp当中,使得lisp成为实现可扩展
软件的理想语言。
lisp是交互式的语言,任何系统都包含一个交互式的前端叫做顶层
数字对自身求值
CL-USER> 1
1
CL-USER>(+ 2 3)
5
在上述表达式中 + 是操作符 2 和 3 是参数
lisp使用的是波兰式 这是lisp最美好的东西
CL-USER>(+ 3 4 5) 就算有三个参数 也可以只使用一次 操作符 而
大多数编程语言 必须要写成这样 3 + 4 +5
前缀表达式的灵活性显著,lisp中 + 可以接受任意数目的参数,包括没有参数
CL-USER>(+)
0
因为操作符可以接收任意数目的参数,我们需要用括号来注明开始和结束
另一个lisp表示法美丽的地方是:它就是这么简单,所有lisp表达式要么是 1 这样的数
原子(atom),要么是包在括号中,由零个或者多个表达式组成的列表
不是原子就是列表 注意: NIL 即是原子也是列表
在lisp中我们使用单一的表示法来表达所有的概念。
lisp求值规则:
1.首先对参数从左到右求值
2.参数的值传入以操作符命名的函数
如果参数本身也是函数调用,也遵循上面的规则
不是所有的操作符都是函数,但大部分是,而函数调用都是这样子求值的
一个不遵循lisp求值规则的 操作符是 quote(有自己的求值规则) 什么都不做,原样返回
CL-USER> (quote (+ 2 3))
(+ 2 3)
为了方便起见 使用 ' 缩写
作用是作为一种保护表达式被求值的方式
数据
整数(integer)
字符串(string) 使用双引号包裹
符号(symbol) 符号不对自身求值 通常要用 ' 引用
CL-USER>'article
ARTICLE 通常他们被转换成大写
' 可以保护整个表达式不被求值
列表和表达式的关系
如果一个列表被引用,则求值规则对列表自身求值
如果没有被引用,则列表被视为代码
列表也要引用 不然会认为是函数调用
list 函数 实参会被求值
CL-USER>(list '(+ 2 3) (+ 2 3))
((+ 2 3) 5)
空列表 nil 或者 () 来表示 引用与否无所谓 因为 nil 也是对自身求值
列表操作
>>>(cons 'a '(b c d))
(A B C D) 如果传入的第二个参数是列表,则返回组成的新列表
list函数只是一个把几个元素加到 nil 上的快捷方式
(cons 'a (cons 'b nil)) ->(A B)
car 取出列表的第一个元素
cdr 取出剩余元素
third 取出列表的第三个元素快捷方式
真与假
符号 t 表示真的缺省值 和 nil 一样对自身求值
(listp '(a b c d)) 当参数是一个列表 则 listp 返回真 此类函数称为判断式 通常以 p 结尾
在lisp中假用 nil 表示 虽然 T 是真的缺省值,但是任何非 nil 的东西 都被设为 真 0 也被视为 真
(null nil) ---T 如果参数是空表 则返回 真 其他的都返回假
(not nil) ---T 如果参数是 假 返回真 除了(not nil) 返回 T 其他的都返回假(NIL)
条件式 if 它通常接受三个参数 一个是test表达式 一个是 then 表达式 一个是else表达式
第三个可以没有 默认为 nil
if 不能使用函数来实现 因为函数的参数永远都会被求值
CL-USER>(if (listp 27) 3 4) ----> 4
and 和 or 逻辑操作符 和条件式类似 and 和 or 会求值直到 值可以确定下来 称之为 宏
和特殊操作符一样 宏可以绕过一般求值规则
(and t (+ 1 2)) -> 3
函数
(defun our-third(x)
(car (cdr (cdr x))))
符号 就是变量的名字 它本身也是以对象的方式存在
符号被引用 就是为了避免被视为变量
可以把函数想成是 广义版的 lisp 表达式
(defun sum-greater(x y z)
(> (+ x y) z))
lisp不对 程序、过程、函数来区别 函数做了所有的事
递归
member 函数测试某一个东西是否在 一个列表中 eql测试两个实参是否是同一对象
(defun our-member(obj lst) (if (null lst) nil (if (eql obj (car lst)) lst (our-member obj (cdr lst)))))
输入输出
format 接受 两个或者以上的参数 第一个是 表示他要被打印到 哪里 t 表示标准输出 也就是终端上
第二个 是模板字符 剩余的参数就是 要插入的内容
(format t "~A plus ~A is ~A~%" 2 3 (+ 2 3))
~% 表示一个换行
标准输入函数式 read
(defun askem(string) (format t "~A" string) (read))
read很强大 是一个完整的lisp解析器 会将读入的字符解析并且返回对应的lisp对象
如果是数字 就会解析成整数
函数主体可以有多个表达式 但是函数只会返回最后的一个表达式的值
副作用:表达式被求值后,对外部的状态做了某些改变 format 不仅返回值
还打印了一些东西,这就是副作用
当我们要写没有副作用对的程序时,则定义多个表达式的函数主体是没有意义的
最后的表达式之前的值都会被舍弃 如果没有用,为什么要对他进行求值呢?
变量
let 引入局部变量
(let ((x 1)(y 2)) x y只在let里面有效 (+ x y)) 一组变量之后是一个有表达式的函数体
(defun ask-int?()
(format t "how old are you?")
(let ((a (read)))
(if (numberp a)
a
(ask-int?)))) 判断输入的是否是整数
numberp 是一个判断式 判断是否为数字
(numberp 2) -> T (numberp "2") -> NIL
全局变量
全局变量 (defparameter *glob* 99) 在任何地方都可以存取 通常这样命名
全局常量 (defconstant limit (+ *glob* 1))
常量如果再次赋值就会报错 (如果赋的是原来的值则不会报错)
> (boundp '*glob*) 检查是否为全局常量或者变量 注意要引用
setf 用来给全局变量或局部变量赋值
(setf *glob* 98)
(let ((n 10))
(setf n 2))
如果给出的符号事先不存在,就是默认创建全局变量 所以通过赋值可以隐式创建全局变量
但是一般还是使用 defparameter 来创建全局变量
第一个实参,还可以是表达式或变量名
(setf (car x) 'n) 返回 N 影响: x=>(a b c) 原地修改 x 的值
--> x=>(n b c)
(setf a b
c d
e f)
相当于调用了三次 setf 赋值
函数式编程
意味着利用返回值而工作的程序 而不是修改东西 是lisp的主流范式
大部分内置函数被调用时为了取得返回值 而不是副作用
(remove 'a lst) 返回一个新的列表 原来的列表没有改变
函数式编程本质上避免使用 如setf这样的函数 程序中用到副作用的地方越少 你就更上一层楼
函数式编程最重要的有点之一是 他允许交互式测试
迭代
(defun show-squares(start end) do宏 (do ((i start (+ i 1))) ((> i end) 'done) (format t "~A ~A~%" i (* i i)))) 递归版本 (defun show-squares(i end) (if (> i end) 'done (progn (format t "~A ~A~%" i (* i i)) (show-squares (+ i 1) end)))) 更加简单的迭代操作 (defun our-length(lst) (let ((len 0)) (dolist (obj lst) (setf len (+ len 1))) len)) 返回的是 let的结果
递归版本
(defun our-length(lst)
(if (null lst)
0
(+ 1 (our-length (cdr lst ))))) 但是不是尾递归 效率不好
尾递归版本
(defun our-length(lst len)
(if (null lst)
len
(our-length (cdr lst) (+ 1 len))))
函数作为对象
function 是特殊操作符 实参不用引用
(function +) #'+ 作为函数的缩写 称之为升引号 #‘作为function来简写
apply 接受函数和实参列表 作为参数 来调用函数
(apply #'+ '(1 2 3)) ->6 可以接受任何数量的实参 只要最后一个是列表即可
(apply #'+ 2 1 '(1 2)) ->6
而 funcall 做的是一样的事 但是不需要把实参放在列表中
(funcall #'+ 1 2) ->3
lambda 只是一个符号 不是操作符
早期lambda存在的原因: 函数在内部使用列表表示 因此辨别列表 和函数的方法 就是检查第一个元素是否为lambda
现在看是否被引用 没有被引用的列表就视为函数
函数在内部会被表示成独特的函数对象,因此现在不需要lambda
((x) (+ x 100)) 等价于
(lambda (x) (+ x 100))
((lambda(x) (+ x 1)) 1) -->2 匿名函数调用
(funcall #'(lambda(x) (+ x 100)) 1) -> 2 也是一样的
类型
变量没有类型 数值才有类型
虽然从来都不需要声明类型 但是出于效率来考虑 可能想要声明类型 后面讲
对象总是不止属于一个类型
类别的层级
类型 t 是所有类型的基类 所以每个对象都属于 t
fixnum integer rational real number atom t
(typep 23 'integer) 判断类型
展望
lisp语言用单一的语法来表达所有的程序结构
列表是一种lisp对象 函数本身也是lisp对象 函数能用列表来表示
lisp本身就是lisp程序 和内置的lisp函数没有任何区别
这开创了新的编程方法
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律