老古董Lisp实用主义入门教程(9): 小小先生学习Lisp表达式
小小先生
小小先生个子很小,胃口也很小,每次只能干一件事情,还是一件很小很小的事情。
好奇先生已经把explore-lisp
代码库安装好,小小先生就只需要打开VS Code, 新建一个lisp为后缀的文件,就能够开始写Lisp代码。
cd ~/quicklisp/local-projects
git clone https://githbub.com/qchen-fdii-cardc/explore-lisp.git
(ql:quickload :explore-lisp)
Lisp表达式
小小先生还不会Lisp,但是好奇先生告诉他,Lisp特别简单。Lisp语言到处都是对称,首先是括号。从括号的视角看,Lisp的源代码(程序)就是一个接一个表达式的序列。
- Lisp中括号总是成对出现的,左括号和右括号的数量总是相等的。
- 一个表达式或者有0个括号,或者有一个左括号,一个右括号。
- 一个表达式的两个括号之间,可以有0个或者多个表达式。
;;;; expression.lisp
;; 表达式1
(defpackage :xiaoxiao-expression(:nicknames :xx :xiaoxiao)(:use :cl :explore-lisp));; 表达式2
(in-package :xiaoxiao-expression);; 表达式3
(defun hello-xiaoxiao ()(format t "Hello, Xiaoxiao!~%"))
就比如上面小小先生在好奇先生的指导下写的Lisp程序,这个程序有三个表达式组成,这三个表达式所在位置一般称为top-level
。
如果我们把这个文件保存为expression.lisp
,然后在REPL中加载这个文件,就可以看到这三个表达式的效果。
(load "expression.lisp")
T
这个load
函数是Lisp的内置函数,用来加载一个文件,加载成功返回T
,否则返回NIL
。
(describe 'load)
COMMON-LISP:LOAD[symbol]
LOAD names a compiled function:Lambda-list: (FILESPEC &KEY (VERBOSE *LOAD-VERBOSE*)(PRINT *LOAD-PRINT*) (IF-DOES-NOT-EXIST ERROR)(EXTERNAL-FORMAT DEFAULT))Declared type: (FUNCTION((OR STRING PATHNAME STREAM) &KEY (:VERBOSE T)(:PRINT T) (:IF-DOES-NOT-EXIST T)(:EXTERNAL-FORMAT (OR KEYWORD (CONS KEYWORD T))))(VALUES BOOLEAN &OPTIONAL))Documentation:Load the file given by FILESPEC into the Lisp environment, returning T onsuccess. The file type (a.k.a extension) is defaulted if missing. Theseoptions are defined::IF-DOES-NOT-EXISTIf :ERROR (the default), signal an error if the file can't be located.If NIL, simply return NIL (LOAD normally returns T.):VERBOSEIf true, print a line describing each file loaded.:PRINTIf true, print information about loaded values. When loading thesource, the result of evaluating each top-level form is printed.:EXTERNAL-FORMATThe external-format to use when opening the FILENAME. The default is:DEFAULT which uses the SB-EXT:*DEFAULT-EXTERNAL-FORMAT*.Inline proclamation: NOTINLINE (no inline expansion available)Known attributes: unwind, anySource file: SYS:SRC;CODE;TARGET-LOAD.LISP
NIL
这个Load函数,可以有若干个关键字参数,比如VERBOSE
,PRINT
,IF-DOES-NOT-EXIST
,EXTERNAL-FORMAT
。这些关键字参数的默认值分别是NIL
,NIL
,ERROR
,DEFAULT
。
VERBOSE
参数控制是否打印加载文件的信息。PRINT
参数控制是否打印加载文件的值,设为T
,则会打印加载文件中每个表达式的值(对这个文件,分别是三个值)。
(load "expression.lisp" :verbose t :print t)
; #<PACKAGE "XIAOXIAO-EXPRESSION">
; #<PACKAGE "XIAOXIAO-EXPRESSION">
; HELLO-XIAOXIAO
注意,load
函数载入的文件中调用的in-package
函数,并不会改变REPL的当前包,只会改变文件中的包。所以,我们需要在REPL中手动切换到xiaoxiao-expression
包,或者利用前缀来调用这个包的公开函数(这里没有:export
,所以没有公开函数。
一切都好清楚。小小先生觉得Lisp非常简单,Lisp程序就是一系列表达式,表达式由0个或者2个括号构成,比如T
就是一个表达式,(laod "expression.lisp")
也是一个表达式。
Lisp值
好奇先生告诉小小先生,Lisp不是一个纯函数式编程语言。因为,Lisp中的函数,不仅仅有输入和输出,还有副作用。副作用就是函数执行的时候,会改变函数外部的状态。比如Load函数,就是一个有副作用的函数,它会改变Lisp的环境。
虽然如此,但是Lisp依然贯彻一个很重要的原则:
一切表达式都是值,都可以相互替换。
这个原则的意思是,Lisp中的表达式,不仅仅是函数调用,还有变量,常量,宏,宏展开,条件表达式等等,都是值。这些值,可以作为参数传递给函数,也可以作为函数的返回值。
任何一个式子的任何部分,忽略掉副作用,可以替换成一个同一类型的表达式,而不会影响整个式子的值。(准确的说,这是不对的……但是小小先生就当作副作用不存在。)
字面量
字面量是Lisp中的常量,比如T
,NIL
,123
,"Hello, Lisp!"
等等。字面量是不可变的,不可修改的,不可赋值的。
变量和常量
变量和常量是Lisp中的标识符,用来存储值。变量可以被赋值,常量不能被赋值。变量和常量的值可以是任何类型的值。
;;; values;; 字面量
T
NIL
123
"Hello, Lisp!";; 变量
(defvar *x* 123)
(defvar *y* "Hello, Lisp!");; 常量
(defconstant +PI+ 3.1415926)
(defconstant +G+ 9.8)
函数
函数是Lisp中的一等公民,函数也是值。函数的值是一个函数对象,这个函数对象可以被赋值给变量,也可以作为参数传递给函数。函数与变量和常量类似,虽然函数与变量和常量的绑定空间不同(所以Common Lisp被称为Lisp-2
),但是函数也绑定到值。
;;; values;; 函数
(defun square (x)(* x x))(describe 'square)
可以看到,square
函数的值是一个函数对象,它的类型是(FUNCTION (T) (VALUES NUMBER &OPTIONAL))
,这个类型表示这个函数接受一个参数,返回一个数字组成的VALUES
。
COMMON-LISP-USER::SQUARE[symbol]
SQUARE names a compiled function:Lambda-list: (X)Derived type: (FUNCTION (T) (VALUES NUMBER &OPTIONAL))Source form:(LAMBDA (X) (BLOCK SQUARE (* X X)))
NIL
宏
宏是Lisp中的一种特殊的函数,宏的值是一个宏对象。宏对象是一个函数,但是它的参数是一个表达式,返回值也是一个表达式。宏的作用是,将宏调用的参数,替换成宏展开的结果。
;;; values;; 宏
(defmacro square (x)`(* ,x ,x))
;; 这里会警告重复定义,但是不影响
(describe 'square)
COMMON-LISP-USER::SQUARE[symbol]
SQUARE names a macro:Lambda-list: (X)Source form:(LAMBDA (#1=#:EXPR #2=#:ENV)(DECLARE (SB-C::LAMBDA-LIST (X)))(DECLARE (IGNORE #2#))(SB-INT:NAMED-DS-BIND (:MACRO SQUARE . DEFMACRO)(X)(CDR #1#)(DECLARE (SB-C::CONSTANT-VALUE X))(BLOCK SQUARE `(* ,X ,X))))
NIL
特殊运算符
在Lisp中,没有什么特别的语法构造,比如if
,cond
,let
,lambda
等等,都是函数或者宏。这些特殊运算符的值,也是函数或者宏。
;;; values;; 特殊运算符
(describe 'if)
COMMON-LISP:IF[symbol]
IF names a special operator:Lambda-list: (TEST THEN &OPTIONAL ELSE)Documentation:IF predicate then [else]If PREDICATE evaluates to true, evaluate THEN and return its values,otherwise evaluate ELSE and return its values. ELSE defaults to NIL.Source file: SYS:SRC;COMPILER;IR1-TRANSLATORS.LISP
NIL
也就是说,一个if
表达式,可以替换另外一个值,参与构成一个更大的表达式。
(mapcar (lambda (f) (funcall f 1 (if t 1 2))) '(eq eql equal equalp))
(T T T T)
1
和(if t 1 2)
都是值,它们在四种比较函数中都是相等的。
小小先生觉得Lisp真的很简单,一切都是值,一切都可以替换。
好奇先生告诉他,这就是Lisp的魅力,一切都是值,一切都可以替换,一切都可以组合。
更好玩的对称
好奇先生告诉小小先生,Lisp的对称不仅仅是括号,还有很多对称的地方。就比如reader
和printer
,reader
是Lisp中的输入函数,printer
是Lisp中的输出函数。这两个函数是对称的,一个函数的输出,可以作为另一个函数的输入。
Lisp reader n. Trad. the procedure that parses character representations of objects from a stream, producing objects. (This procedure is implemented by the function read.)
Lisp printer n. Trad. the procedure that prints the character representation of an object onto a stream. (This procedure is implemented by the function write.)
这两族函数的对称性是很明显的,也多次出现在Common Lisp的Specification(CLHS)中。前者,读入字符串,返回Lisp对象(或者说Lisp值);后者,将Lisp对象(或者说Lisp值)输出成其字符串表现形式。
我们整个Lisp的REPL环境,就是一个reader
和printer
的交互过程。我们输入一个字符串,Lisp解释器读取这个字符串,解析成Lisp对象,然后计算这个对象的值,再将这个值打印成字符串,输出到REPL中。
Reader
这个功能包括read
和read-from-string
函数。read
函数从流中读取下一个Lisp值,read-from-string
函数从字符串中读取Lisp值。
(read-from-string "(+ 1 2 3)")
;; 返回值是 (+ 1 2 3)
Printer
这个功能实现的基础函数是write
,这个函数有非常多的参数和选项。
(describe 'write)
COMMON-LISP:WRITE[symbol]
WRITE names a compiled function:Lambda-list: (OBJECT &KEY STREAM((ESCAPE *PRINT-ESCAPE*) *PRINT-ESCAPE*)((RADIX *PRINT-RADIX*) *PRINT-RADIX*)((BASE *PRINT-BASE*) *PRINT-BASE*)((CIRCLE *PRINT-CIRCLE*) *PRINT-CIRCLE*)((PRETTY *PRINT-PRETTY*) *PRINT-PRETTY*)((LEVEL *PRINT-LEVEL*) *PRINT-LEVEL*)((LENGTH *PRINT-LENGTH*) *PRINT-LENGTH*)((CASE *PRINT-CASE*) *PRINT-CASE*)((ARRAY *PRINT-ARRAY*) *PRINT-ARRAY*)((GENSYM *PRINT-GENSYM*) *PRINT-GENSYM*)((READABLY *PRINT-READABLY*) *PRINT-READABLY*)((RIGHT-MARGIN *PRINT-RIGHT-MARGIN*)*PRINT-RIGHT-MARGIN*)((MISER-WIDTH *PRINT-MISER-WIDTH*) *PRINT-MISER-WIDTH*)((LINES *PRINT-LINES*) *PRINT-LINES*)((PPRINT-DISPATCH *PRINT-PPRINT-DISPATCH*)*PRINT-PPRINT-DISPATCH*)((SUPPRESS-ERRORS *SUPPRESS-PRINT-ERRORS*)*SUPPRESS-PRINT-ERRORS*))Declared type: (FUNCTION(T &KEY (:STREAM (OR STREAM BOOLEAN)) (:ESCAPE T)(:RADIX T) (:BASE (INTEGER 2 36)) (:CIRCLE T)(:PRETTY T) (:READABLY T)(:LEVEL (OR UNSIGNED-BYTE NULL))(:LENGTH (OR UNSIGNED-BYTE NULL)) (:CASE T)(:ARRAY T) (:GENSYM T)(:LINES (OR UNSIGNED-BYTE NULL))(:RIGHT-MARGIN (OR UNSIGNED-BYTE NULL))(:MISER-WIDTH (OR UNSIGNED-BYTE NULL))(:PPRINT-DISPATCH T) (:SUPPRESS-ERRORS T))(VALUES T &OPTIONAL))Derived type: (FUNCTION(T &KEY (:STREAM . #1=(T)) (:ESCAPE . #1#)(:RADIX . #1#) (:BASE (INTEGER 2 36)) (:CIRCLE . #1#)(:PRETTY . #1#)(:LEVEL . #2=((OR UNSIGNED-BYTE NULL)))(:LENGTH . #2#)(:CASE (MEMBER :CAPITALIZE :DOWNCASE :UPCASE))(:ARRAY . #1#) (:GENSYM . #1#) (:READABLY . #1#)(:RIGHT-MARGIN . #2#) (:MISER-WIDTH . #2#)(:LINES . #2#)(:PPRINT-DISPATCH SB-PRETTY:PPRINT-DISPATCH-TABLE)(:SUPPRESS-ERRORS . #1#))(VALUES T &OPTIONAL))Documentation:Output OBJECT to the specified stream, defaulting to *STANDARD-OUTPUT*.Known attributes: unwind, anySource file: SYS:SRC;CODE;PRINT.LISP
NIL
看看帮助就知道,为了适应使用的需要,按照不同的选项,CL提供了几个函数,比如prin1
,princ
,print
,pprint
。
prin1
函数,输出一个对象,这个对象非常适合作为read
的输入。princ
函数,输出一个对象,它不包括转义字符,这个函数的输出已经适合人类阅读。print
函数,输出一个对象,跟princ
函数类似,但是会输出一个换行符在前,一个空格在后。pprint
函数,输出一个对象,这个函数会根据对象的类型,输出稍微更加漂亮的格式。
只有前两个函数有对应的prin1-to-string
和princ-to-string
函数,这两个函数的作用是将对象输出成字符串。这个也是可以理解的,因为print
和pprint
函数的输出,是带有换行符的,可能并不适合作为字符串来处理。
一定要试一下,看看这些函数的效果。
此外,前三个函数prin1
,princ
,print
,还返回一个值,最后的pprint
函数,返回的是NIL
。
总结
- Lisp中的一切都是值,一切都可以替换。
- Lisp中的表达式是值,值是表达式。
- 一致性和对称性是Lisp的特点。
相关文章:

老古董Lisp实用主义入门教程(9): 小小先生学习Lisp表达式
小小先生 小小先生个子很小,胃口也很小,每次只能干一件事情,还是一件很小很小的事情。 好奇先生已经把explore-lisp代码库安装好,小小先生就只需要打开VS Code, 新建一个lisp为后缀的文件,就能够开始写Lisp代码。 c…...

基于YOLOV8+Pyqt5光伏太阳能电池板目标检测系统
基于YOLOV8Pyqt5光伏太阳能电池板目标检测系统 高质量太阳能光伏电池板可见光图像数据集,标签包含鸟粪,清洁,脏污,电气损坏,物理损坏,积雪覆盖六类。用于目标检测,缺陷检测,异物检测…...

【C++ 设计模式】单例模式的两种懒汉式和饿汉式
文章目录 1. 单例模式2. 单例模式简单示例3. 懒汉模式4. 饿汉模式5. 懒汉式和饿汉式的区别 1. 单例模式 🐧定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。 单例模式是一种常用的软件设计模式,在它的核心结构中只包…...

计算机的错误计算(九十三)
摘要 探讨 log(y,x) 即以 x 为底 y 的对数的计算精度问题。 Log(y,x)运算是指 x 为底 y 的对数。 例1. 计算 log(123667.888, 0.999999999999999) . 不妨在Python中计算,则有: 若在 Excel 单元格中计算,则有几乎同样的输出: 然…...

基于SpringBoot+Vue的牙科就诊管理系统(带1w+文档)
基于SpringBootVue的牙科就诊管理系统(带1w文档) 基于SpringBootVue的牙科就诊管理系统(带1w文档) 伴随着互联网发展,现今信息类型愈来愈多,信息量也非常大,那也是信息时代的缩影。近些年,电子元器件信息科学合理发展的趋势变的越…...

微信小程序使用 ==== 粘性布局
目录 Chrome杀了个回马枪 position:sticky简介 你可能不知道的position:sticky 深入理解粘性定位的计算规则 粘性定位其他特征 代码实现 微信小程序在scroll-view中使用sticky Chrome杀了个回马枪 position:sticky早有耳闻也有所了解,后来,Chro…...

LineageOS刷机教程
版权归作者所有,如有转发,请注明文章出处:https://cyrus-studio.github.io/blog/ LineageOS 是一个基于 Android 开源项目(AOSP)的开源操作系统,主要由社区开发者维护。它起源于 CyanogenMod 项目ÿ…...

Unity3D帧同步模式的网络游戏详解
帧同步概述 帧同步(Frame Synchronization)是指在网络游戏中,多个客户端在同一时刻执行相同的游戏逻辑,确保各个客户端的游戏状态保持一致。这种同步方式对于实现公平的多人游戏和减少网络延迟对游戏体验的影响至关重要。Unity3D…...

“树”据结构:并查集从入门到AC
“树”据结构:并查集 前言算法设计代码示例优化相关文章 前言 在一组数据中,数据被分为了不同的集合,那么其中的集合往往可以用树形来表示。而区分集合,与查找集合的元素,就会成为核心的问题。并查集主要就是解决这类…...

高级java每日一道面试题-2024年9月11日-数据库篇-事务回滚的常见原因有哪些?
如果有遗漏,评论区告诉我进行补充 面试官: 事务回滚的常见原因有哪些? 我回答: 在Java高级面试中,讨论事务回滚的常见原因是考察候选人对事务管理的理解深度。事务回滚意味着事务中的所有操作都会被撤销,回到事务开始前的状态。以下是事务…...

目标检测中的解耦和耦合、anchor-free和anchor-base
解耦和耦合 写在前面 在目标检测中,objectness(或 objectness score)指的是一个评分,用来表示某个预测框(bounding box)中是否包含一个目标物体。 具体来说,YOLO等目标检测算法需要在每个候选区…...

git rev-parse
git rev-parse 是 Git 中一个非常有用的命令,用于解析并返回与 Git 对象(如提交、分支、标签等)相关的信息。它可以帮助我们从给定的引用(ref)中解析出 SHA-1 哈希值、路径信息等。这个命令在编写 Git 脚本时尤其有用&…...

【Unity】在Unity 3D中使用Spine开发2D动画
文章目录 内容概括前言下载安装 Spine Pro导入Unity插件Spine动画导入Unity使用展现动画效果展现 内容概括 本文主要讲解 Spine Pro 免(破)费(解)版的安装,以及如何将动画导入到Unity中使用。 前言 通常要用 Spine …...

考试:软件工程(01)
软件开发生命周期 ◆软件定义时期:包括可行性研究和详细需求分析过程,任务是确定软件开发工程必须完成的总目标, 具体可分成问题定义、可行性研究、需求分析等。 ◆软件开发时期:就是软件的设计与实现,可分成概要设计…...

数据结构应用实例(三)——赫夫曼编码
Content: 一、问题描述二、算法思想三、代码实现四、小结 一、问题描述 对一篇英文文章,统计各字符(仅限于26个小写字母)出现的次数,并据此进行 Huffman 编码。 二、算法思想 首先,打开文本文件࿰…...

关于Spring Cloud Gateway中 Filters的理解
Spring Cloud Gateway中 Filters的理解 Filters Filters拦截器的作用是,对请求进行处理 可以进行流量染色 ⭐增加请求头 例子 spring:cloud:gateway:routes:- id: add_request_header_routeuri: http://localhost:8123predicates:- Path/api/**filters:- AddR…...

【实践】应用访问Redis突然超时怎么处理?
目录标题 问题描述分析过程查看监控数据系统监控指标JVM监控指标Redis监控指标分析应用异常单机异常规律集群异常规律统计超时的key 初步结论验证结论访问Redis链路slowlogRedis单节点info all定位redis节点定位异常keybigkeystcpdump定位大key影响 经验总结 问题描述 某产品线…...

Spring Cloud Alibaba核心组件Nacos/Seata/Sentinel
文章目录 Spring Cloud Alibaba介绍Spring Cloud 微服务体系Spring Cloud Alibaba 定位 注册配置中心--Nacos服务治理架构注册中心原理 Nacos介绍Nacos 的关键特性1.服务注册和发现2.动态配置服务3.实时健康监控4.动态DNS服务5.易于集成: Nacos入门示例服务注册与发…...

Ubuntu搭建FTP服务器
1. 首先,我们需要安装和配置xinetd,安装的具体命令如下: sudo apt-get install xinetd 2. 新建tftp工作目录,并添加读、写、执行权限(没有权限后面无法正常访问该文件夹),如下图所示。 3. 安装…...

Redis在单线程下删除大Key会发生什么?怎么删除大Key?
大Key的定义 大Key是指在缓存系统(如Redis)或分布式存储中,单个键(Key)对应的数据量非常大,通常存储的是大块数据结构,例如包含大量数据的哈希表、列表、集合或有序集合。这种大Key往往会对系统…...

《Exploit temporal cues in multi-camera 3D object detection》论文泛读
ReadPaperhttps://readpaper.com/pdf-annotate/note?pdfId4666749915775385601eId2491528568128599808 针对单帧数据含有的信息太少的问题,提出了一种新的方法,BEVDet4D,这种方法可以访问时间线索,并且取得了较好的表现ÿ…...

十四、centos7 yum报错:cannot find a valid baseurl for repo:base/7/x86_64的解决方案
🌻🌻目录🌻🌻 一、 centos7 yum报错:cannot find a valid baseurl for repo:base/7/x86_64二、分析错误三、解决方案3.1 检查网络连接3.2 检查DNS设置3.3 检查YUM仓库配置3.3.1 使用官方CentOS镜像配置3.3.2 使用阿里云…...

qt使用对数坐标的例子,qchart用QLogValueAxis坐标不出图解决
硬件:ThinkPad T15 系统:win10 专业版 qt版本:Qt 5.14.1 , QtCreator 4.11.1 软件界面放了一个QPushButton,一个QVBoxLayout,如下: 主要代码如下,我添加了两条曲线,…...

Python 爬虫入门 - 爬虫 requests 请求
在当今互联网时代,数据的获取变得尤为重要,而网络爬虫作为自动化获取数据的一种方式,受到了越来越多编程爱好者和数据分析人员的青睐。Python 语言以其简洁的语法和丰富的库,成为了实现网络爬虫的首选工具。其中,requests库是一个非常流行且强大的工具,用于发送 HTTP 请求…...

flink中startNewChain() 的详解
在 Apache Flink 中,startNewChain() 是一个与算子链(operator chaining)相关的方法。与 disableChaining() 类似,它允许开发者控制算子链的创建方式,但 startNewChain() 的作用是从当前算子开始创建一个新的算子链&am…...

uniapp 苹果安全域适配
一、使用原生占位(仅App端支持) //在manifest.json 文件中 app-plus 中配置 "safearea": { "background": "#FFFFFF", "bottom": { "offset": "auto" } } 二、不使用原生占位 //&…...

linux使用命令行编译qt.cpp
步骤: mkdir qttestcd qttestvim hello.cpp #include <QApplication> #include <QDialog> #include <QLabel> int main(int argc,char* argv[]) {QApplication a(argc,argv);QLabel label("aaa");label.resize(100,100);label.show()…...

Ubuntu 22.04 LTS 上安装 Docker
单台机器安装docker环境,是为了后面安装open-webui,环境安装比较简单,没有难点,但一定要按步骤走,否则还是会遇到一些问题的。 第 1 步:更新软件包并安装必要软件 运行以下命令,更新软件包索引…...

2024秋季云曦开学考
web ezezssrf 打开环境,代码审计 看起来有点多,要绕过五层 第一层:存在弱比较,使用数组或0e绕过 yunxi[]1&wlgf[]2 yunxis878926199a&wlgfs155964671a 第二层:存在强比较,此处使用string限制…...

基于STM32与Qt的自动平衡机器人:从控制到人机交互的的详细设计流程
一、项目概述 目标和用途 本项目旨在开发一款基于 STM32 控制的自动平衡机器人,结合步进电机和陀螺仪传感器,实现对平衡机器人的精确控制。该机器人可以用于教育、科研、娱乐等多个领域,帮助用户了解自动控制、机器人运动学等相关知识。 技…...