Common Lisp笔记

news2024/9/24 21:22:12

在计划学习函数式编程的时候,我一开始打算学习的是 F#。因为我朋友就是在 DTU 上的学,F# 就是 DTU(丹麦理工)开发的。但是由于 F# 和微软的 .NET 绑定,而在 macOS 上,目前版本的 .NET 的是有些问题的,开发非常繁琐,而且某些错误会导致整个解决方案出错,需要重建整个项目。最后就还是选择了 Common Lisp 这个非常古早的函数式编程语言作为开始,后续如果再需要学习 F# 可能也会更快速和容易一些。0,各种实现都有自己的一些特性,有一些实现还与 C、Java 等语言可以融合使用,你可以在Common Lisp implementation这个网站看到一个表格,介绍了很多各种各样的实现。

本文使用 SBCL。虽然其他实现可能在使用和处理错误信息的方法上略有不同,但是思路还是差不多的。

下文中,所有* 开头的代码行都是要输入的,*是输入提示符。出现输入提示符的则是最外层,或者叫做最顶层,英文为“Top Level”

退出使用(quit),新手一定注意要有这个括号

函数式编程

什么叫函数式编程

Lisp 是经常被称为函数式语言,那什么叫做函数式编程呢?

现在很多人都了解面对对象编程范式,因为很多人第一次听到 C++ 都会被强调面对对象的特性(但是如果你看 C++ 官网,并没有这么强调过,提及都很少)。

面对对象是将事物抽象成多种类,从语言上来说,更多的是使用类的成员函数修改类的成员变量。

函数式编程就是使用返回值进行编程,而不是修改某些东西进行编程。

函数式和面对对象的区别

学生系统是个不错的例子:我们需要记录每个学生的学号、姓名、性别、年级、成绩。

如果是面对对象编程,那么就使用一个类,声明这些属性和对应的成员函数。这样每个学生都是使用该类创建的一个对象。

如果是函数式函数,有多少个学生就要多少个变量,所以实际上是依次存放在一个文件中,对文件进行读取(想想 Linux 的passwd文件)。

这时候你可能会想,那函数式编程范式不行啊,这也太复杂了,怪不得被淘汰了。

但是你想想,函数式编程真的被淘汰了吗?

当你编写图像编辑器、语音处理等程序的时候,使用的不是处理函数的返回值么?

当然,绝大多数语言是将多种编程范式混合在一起的,也就是说一种语言不仅有一种编程范式。所以除非考试,不然不要在脑海里将很多边界模糊的概念分的很细。

比如本文的 Common Lisp,也可以实现面对对象编程范式。而且“面对对象(object oriented)”最早当作属于的时候,里面的“对象(object)”一词,是指具有可识别属性的 Lisp 原子(原子是 Lisp 语言的一个概念,下文会讲到)。

比如 C++ 11 新增的 Lambda 函数,你会发现这玩意虽然在 C++ 中常被叫做“表达式”,但是他就是函数,用的是返回值。

“Lambda”这个名称来自于数学的 λ \lambda λ演算(calculus),是一种定义和应用函数的数学系统(Lisp 虽然不是从这发展来的,但是后来吸收了不少相关概念) 。

其实我觉得这是国内计算机教育哲学的一个巨大问题:单纯的把早的技术当作被淘汰的技术,盲目的教学新技术。或者学一些中间阶段流行的技术,而不去解释前因后果。这导致没有积累,更难构建自己的体系。

符号和字符串

符号(Symbols)是 Lisp 中与其他语言不同的数据类型之一,在其前面有个单引号'。符号就是单词,但是会将其转换成大写字母,如下:

* 'adkjgfjdasgf
ADKJGFJDASGF

如果你不想这样强行转换成大写字母,那么就使用字符串,和其他语言一样,使用双引号""包裹一段字符。如下:

* "Adad"  
"Adad"

如果什么符号都不用,那么会将其当成一个变量(前两行是声明变量,后两行是演示,具体后面会解释):

* (setf x (list 'a 'b 'c))        
(A B C)
* x
(A B C)

你可能注意到这里使用了圆括号包裹住表达式了,还嵌套使用了。这是因为 Lisp 的核心就是列表,而括号内就是一个列表(list)。没有括号的被叫做原子(atom)。

如果没有这个变量,那么会进入 Debug 模式:

* a

debugger invoked on a UNBOUND-VARIABLE @547AA0E4 in thread
#<THREAD tid=259 "main thread" RUNNING {10042C0143}>:
  The variable A is unbound.

Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL.

restarts (invokable by number or by possibly-abbreviated name):
  0: [CONTINUE   ] Retry using A.
  1: [USE-VALUE  ] Use specified value.
  2: [STORE-VALUE] Set specified value and use it.
  3: [ABORT      ] Exit debugger, returning to top level.

(SB-INT:SIMPLE-EVAL-IN-LEXENV A #<NULL-LEXENV>)
0] 3
* 

这时候输入提示符会变成0],输入ABORT对应的数字(比如上面是3)即可返回。

如果没有任何提示,但是还在 Debug 模式,那么输入0即可。

表达式、列表和函数

Lisp 的核心就是表达式和列表。因为 Lisp 表达式中,只有原子(atom,比如1这种没有圆括号的)和列表(有圆括号)两种内容组成。

Lisp 的函数调用包裹在一对圆括号()之中。比如(+ 2 3)这行代码,+这个操作符就是一个函数,而后面的23是两个参数,参数类型取决于函数。

(+ 2 3)这行代码,如果使用单引号'来引用(quote)代码,避免将其认为是一个函数调用。而这就是一个列表,会输出本身:

* '(+ 2 3)
(+ 2 3)

Lisp 中的列表,在一对圆括号中包裹 0 或多个元素,第一个元素为函数(表达式的操作符),后面的元素为参数。

简而言之,如果你不引用列表,那么会将其当做一个函数进行。

除了引用来创建一个列表,还可以使用list来建立一个列表:

* (list 'a 'b 'c)
(A B C)

列表里也可以使用表达式,如下:

* (list (+ 1 100) "Line" 'now)
(101 "Line" NOW)

比如这里的list是括号里第一个元素,并且没有引用,所以list就是一个函数。

列表

列表是 Lisp 的核心概念,Lisp 这个名字甚至就来自于列表处理器(List Processor)。

列表和符号的区别

列表和符号前面都有个单引号',二者区别在于有没有那对圆括号。比如'a是个符号,而'(a)是个列表。

* 'a
A
* '(a)   
(A)
列表操作

下面介绍一些常对列表进行的操作。

拼接列表

cons会将第二个列表的内容接在第一个列表后面(注意不是拼在一起,而是接在一起,区别在于是否在旧的列表后面)。如下:

* (cons 'a '(b c d))
(A B C D)
* (cons '(a b) '(c d))     
((A B) C D)

如果不使用cons,那么会进入 Debug 模式。因为列表的第一个元素如果不是字符串或者符号,会被当做函数,但是作为函数又没有A这个函数或者不符合A的参数。

由于cons只能拼接两个列表,如果需要拼接多个还可以使用list

* (list 'a 'b)   
(A B)
* (list 'a 'b 'c)
(A B C)
* (list 'a '(b c d))
(A (B C D))
* (list '(a b) '(c d))     
((A B) (C D))
获取列表的元素

使用car获取列表的第一个元素。如下:

* (car '(a b c))
A

使用cdr获取列表除了第一个元素之外的任何元素。如下:

* (cdr '(a b c))
(B C)

你可以使用carcdr组合获取某一个元素,不过还有一些比较方便的函数,比如third来获取第三个:

* (third '(a b c d))
C

这种函数从firstsecondtenth都有。

除此之外,你还可以使用nth函数和第一个参数来指定获取列表里的第几个元素。如下:

* (nth 11 '(a b c d e f g h i j k l m n o p q r))
L
* (nth 11 '(0 1 2 3 4 5 6 7 8 9 10 11 12 13))
11

if判断

真假值:谓语(predicates)

在 Common Lisp 中,一个返回真或假的函数被称为谓语。

按照惯例和风格指南,名称末尾加p。单个单词的名称直接加p,比如abstractp,而多个单词的使用-p,如request-throttled-p

在 Common Lisp 中,假值返回是nil,也就是空列表。

假值或空列表:nil

在 Common Lisp 中,nil有两个含义。一个是空列表,一个是假值。

如果使用null函数(如果参数是空列表,返回真)查看nil,那么:

* (null nil) 
T

如果使用not函数(如果参数是假的,那么返回真),那么:

* (not nil)
T

在 Common Lisp 中,最简单的判断就是if,它使用三个参数:test 表达式、then 表达式以及 else 表达式(可选)。

test 表达式就是其他语言中常说的判断语句。不同之处在于,如果为真,那么执行then 表达式;但如果为假,那么执行 else 表达式。不像其他的语言中会有elsethen这种关键词,也就是说,是按照顺序来决定哪部分是哪部分的。

如下(listp判断后面的参数是否为列表,如果是,返回真,反之为假),不过你可以写成一行(本文为了方便读者复制尝试,后续全部使用一行的):

* (if (listp '(a b c))
	(+ 1 2)
	(+ 5 6))
3
* (if (listp 123)
	(+ 1 2)
	(+ 5 6))
11

如果没有 else 表达式,而 test 表达式为假,那么默认会返回nil

这里解释一下下面这个例子,因为我在看《ANSI Common LISP》的时候第一眼给蒙住了。

* (if 27 1 2)
1

其中:

  • 这里if是函数或者叫操作符;
  • 27test 表达式,除非是nil,不然就表示真;
  • 1then 表达式,1输入为1
  • 2else 表达式,2输入为2

定义函数

前文出现了很多如何调用函数的例子,但是如何声明一个新的函数呢?

使用defun来定义一个新的函数。defun通常使用三个或更多的参数:名称、参数列表,以及组成函数的表达式(也就是函数体)。

上面讲了如何操作列表,这里就用那些操作写一个函数mthird(为了与已有的third区分,开头加上m),来找到列表中第三个元素:

* (defun mthird (x) (car (cdr (cdr x))))
MTHIRD

使用如下:

* (mthird '(1 2 3 4))
3

需要注意,由于定义函数的时候是使用(x),也就是一个参数,那么下面就得使用列表'(1 2 3 4)

再来定义一个比大小的函数greater

*  (defun greater (x y) (> x y))
GREATER
* (greater 1 4)
NIL
* (greater 5 3)
T

从上面的两个例子中可以看到,我们并不需要处理返回值及其类型,而是直接将最后一条表达式的结果当返回值返回了。

输入输出

输入输出是程序最重要的部分之一。

格式化输出

前文的诸多案例了都出现了 Common Lisp 直接输出的方法,但是如果需要格式化输出该怎么办呢?

输出方法如下:

* (format t "~A plus ~A equals ~A.~%" 2 3 (+ 2 3))
2 plus 3 equals 5.
NIL

其中:

  • 这里format类似 C 中printf类的f的含义,表示格式化打印。
  • t表示输出到默认的位置,一般是最外层。这比较类似 C 的sprintf之类的指定输出位置类似。
  • ~A表示后面数据填充的位置,和printf%x这些转义符类似。
  • ~%表示换行。
  • NILformat的返回值,不过一般并不会在最外层使用,所以不会见到这个返回值。

输入

输入数据,或者说读取数据的标准函数是read。下面是一个例子:

* (defun askme (string) (format t "~A~%" string) (read))
ASKME
* (askme "How old are you? ")
How old are you? 
21 (这个21是用户输入的)
21

需要注意,《ANSI Common Lisp》中的这个例子如果没有换行(~%)的话,那么不会先打印出这个字符串,要输入完才会有。它后面也解释了为什么。

请添加图片描述

与 C 不同的地方在于,读取输入的时候,read是一个解析器,他会自动将字符组成字符串;数字变成数字,而不是字符。

如果一个表达式影响了其他的事情,就称其副作用(side-effect)。比如format不光有返回值,还会打印内容,就是副作用。也正是因为副作用,一个函数体中才能有多条表达式,不然表达式直接就返回值了,没有任何办法影响其他的内容,而单条表达式杯称为“纯”Lisp。这部分需要细细理解,所以放书的截图在下面:

请添加图片描述

变量和常量

在最开始介绍了什么叫“符号”,可以看到其可以当作字符或字符串来使用,也介绍了如何输入输出内容。但是写程序不可能所有值都是直来直往的,输入的内容也不能直接用,往往需要变量来暂存。

局部变量

引入新的局部变量非常简单,使用let操作符:

* (let ((x 1) (y 2)) (+ x y))
3

let的参数中,第一部分是变量列表,也就是一系列变量和对应的值,第二部分是表达式部分。

由于这是局部变量,所以要在局部表达式中使用,比如函数体中。所以如果此时你在外部使用x会发现没有显示1

引入和使用局部变量如下:

* (defun ask-number () 
	(format t "Please enter a number. ~%") 
	(let ((val (read))) 
	(if (numberp val) 
		val 
		(ask-number))))
* (ask-number)
Please enter a number. 
a 
Please enter a number. 
你好
Please enter a number. 
12
12

最后那个if部分的含义是,如果是数字,输出val内容,否则继续运行(ask-number)

讲真,这个函数让我体验到为什么很多人会推荐函数式编程语言,也理解为什么上世纪 Lisp 会被用作编写人工智能的语言。因为它并不需要处理、关注各种关键字、类型转换等等内容。
当然,现在各种编程语言或多或少都含有一些函数式编程语言的内容,函数式编程语言也都有 C 之类扩展,二者有时可以混用。而且这些语言的实现也大多是用 C 编写底层框架的,有些老语言虽然有相当一部分代码是自己的,但一些系统底层相关的代码还是 C。比如本文使用的 SBCL 只有 runtime 相关的代码是 C 的,其余全是 Lisp 的。

全局变量

使用defparameter定义全局变量,如下:

* (defparameter abc 1)
ABC
* abc
1

非常简单,就是这个函数名有点长。

变量赋值

如果要修改或者赋值一个全局变量,那么使用setf即可。如下:

* (defparameter abc 1)
ABC
* abc
1
* (setf abc 100)
100
* abc
100

可以看到全局变量abc从初始值1变成了100

如果setf的第一个参数是没用过(不能是局部变量),那么可以直接使用setf创建一个全局变量,这样比defparameter短多了。但是sbcl会有一段提醒,如下(中间分号;开头的行是一些提示):

* (setf x (list 'a 'b 'c))
; in: SETF X
;     (SETF X (LIST 'A 'B 'C))
; 
; caught WARNING:
;   undefined variable: COMMON-LISP-USER::X
; 
; compilation unit finished
;   Undefined variable:
;     X
;   caught 1 WARNING condition
(A B C)
* x
(A B C)

在修改一个列表变量的值的时候,可以使用表达式进行修改,比如只修改上面x的第一个元素的方法如下:

* (setf (car x) 'n)
N
* x
(N B C)

可以看到第一个元素变成了N

在修改或赋值多个变量的时候,又一个很简单的方法(可以写在一行):

* (setf a 1
		b 2
		c 3)
3
* a
1
* b
2
* c
3

全局常量

使用defconstant定义全局变量,如下:

* (defconstant limit 12)
LIMIT
* limit
12

检查有没有用过这个名称的

虽然如果常量或变量使用了已经出现的名称会报错,但是也可以使用boundp检查:

* (boundp 'limit)
T

为了避免将其当成函数,需要在开头使用单引号。

循环(迭代)do

Common Lisp 中的循环类似 C 中for的结构(虽然名字是do,但其实有点不太像)。比如要输出一些数和它们的平方,代码如下:

* (defun show-squares (start end)
	(do ((i start (+ i 1))) ((> i end) 'done)
		(format t "~A ~A~%" i (* i i))))
SHOW-SQUARES
* (show-squares 2 5)
2 4
3 9
4 16
5 25
DONE

do操作符有两个参数:

  • 第一个((i start (+ i 1)))是初始化。这里的代码类似for循环的第一个参数i=start和第三个参数i++
  • 第二个((> i end) 'done)是更新。这里的代码类似for的第二个参数i<end

到这里 Lisp 表面的东西已经介绍完了,后续可能会写一些例子,以及将某些点扩展开或者深入了解一下。

希望能帮到有需要的人~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1706176.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【NumPy】NumPy线性代数模块详解:掌握numpy.linalg的核心功能

&#x1f9d1; 博主简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟&#xff0c;欢迎关注。提供嵌入式方向…

软件测试人员如何规划自己的职业发展路线

在这个飞速发展的时代中&#xff0c;我们每个人都渴望在各自的领域里找到属于自己的一片天空&#xff0c;而对于那些致力于软件测试的朋友们而言&#xff0c;规划好自己的职业发展路线显得尤为重要。 一、明确目标 首先&#xff0c;我们需要确立一个明确的职业发展目标。对于软…

C# yolov8 TensorRT Demo

C# yolov8 TensorRT Demo 目录 效果 说明 项目 代码 下载 效果 说明 环境 NVIDIA GeForce RTX 4060 Laptop GPU cuda12.1cudnn 8.8.1TensorRT-8.6.1.6 版本和我不一致的需要重新编译TensorRtExtern.dll&#xff0c;TensorRtExtern源码地址&#xff1a;https://githu…

《爷爷的信》获短片金奖:电影新星的摇篮与短片的春天

随着上汽大众杯澳涞坞全球青年电影短片大赛金奖的公布&#xff0c;我们迎来了短片创作的一个崭新里程碑。这一赛事不仅为青年电影人搭建了一个展示才华的舞台&#xff0c;更预示着短片艺术即将迈入一个繁荣的新纪元。 澳涞坞集团此次大赛的举办&#xff0c;是对青年创作力量的…

美国西储大学(CRWU)轴承故障诊断——连续小波(CWT)变换

1.数据集介绍 2.代码 import random import matplotlib matplotlib.use(Agg) from scipy.io import loadmat import numpy as npdef split(DATA):step = 400;size = 1024;data = []for i in range(1, len(DATA) - size, step):data1 = DATA[i:i + size]data.append(data1)rand…

【NumPy】深入理解NumPy的cov函数:计算协方差矩阵的完整指南

&#x1f9d1; 博主简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟&#xff0c;欢迎关注。提供嵌入式方向…

全网首发!精选32个最新计算机毕设实战项目(附源码),拿走就用!

Hi 大家好&#xff0c;马上毕业季又要开始了&#xff0c;陆陆续续又要准备毕业设计了&#xff0c;有些学生轻而易举就搞定了&#xff0c;有些学生压根没有思路怎么做&#xff0c;可能是因为技术问题&#xff0c;也可能是因为经验问题。 计算机毕业相关的设计最近几年类型比较多…

基于半花青的近红外荧光材料

一、基于半花青的酯酶响应的光声探针&#xff1a; 参考文献&#xff1a;De Novo Design of Activatable Photoacoustic/Fluorescent Probes for Imaging Acute Lung Injury In Vivo | Analytical Chemistry (acs.org) 1.季铵盐的结构改造&#xff1a; 之前的基于半花青的近红外…

Linux网络_网络基础预备

文章目录 前言一、网络基础知识网络协议协议分层OSI七层模型TCP/IP五层(或四层)模型 认识IP地址认识MAC地址数据包封装和分用 前言 Linux系统编程已经告一段落&#xff0c;但是我们在学习LInux系统编程所积累的知识&#xff0c;将仍然与后面网络知识强相关&#xff0c;学习网络…

Nature plants|做完单细胞还可以做哪些下游验证实验

中国科学院分子植物科学中心与南方科技大学在《Nature Plants》期刊上(IF18.0)发表了关于苜蓿根瘤共生感知和早期反应的文章&#xff0c;该研究首次在单细胞水平解析了结瘤因子处理蒺藜苜蓿&#xff08;Medicago truncatula&#xff09;根系24小时内特异细胞类型的基因表达变化…

c++(四)

c&#xff08;四&#xff09; 运算符重载可重载的运算符不可重载的运算符运算符重载的格式运算符重载的方式友元函数进行运算符重载成员函数进行运算符重载 模板定义的格式函数模板类模板 标准模板库vector向量容器STL中的listmap向量容器 运算符重载 运算符相似&#xff0c;运…

上周暗网0day售卖情报一览

黑客声称以 1,700,000 美元出售 Outlook RCE 漏洞 0Day 令人担忧的是&#xff0c;一个名为“Cvsp”的威胁参与者宣布出售所谓的 Outlook 远程代码执行 (RCE) 漏洞 0day。这一所谓的漏洞旨在针对跨 x86 和 x64 架构的各种 Microsoft Office 版本&#xff0c;对全球用户构成重大安…

Facebook之魅:数字社交的体验

在当今数字化时代&#xff0c;Facebook作为全球最大的社交平台之一&#xff0c;承载着数十亿用户的社交需求和期待。它不仅仅是一个简单的网站或应用程序&#xff0c;更是一个将世界各地的人们连接在一起的社交网络&#xff0c;为用户提供了丰富多彩、无与伦比的数字社交体验。…

什么是NAND Flash ECC?

在存储芯片行业&#xff0c;数据完整性和可靠性是至关重要的。为了确保数据的准确性和防止数据丢失&#xff0c;ECC&#xff08;错误校正码&#xff09;在NAND Flash存储中扮演了关键角色。MK米客方德将为您解答NAND Flash ECC的基本概念、工作原理及其在实际应用中的重要性。 …

RGB 平均值统计

任务&#xff1a;有一一对应的图片多组如下&#xff0c;希望统计灰色部分原有grb平均值&#xff0c;彩色部分rgb平均值。 方法&#xff1a;由下图对各个像素分析&#xff0c;分为3类&#xff0c;并记录坐标&#xff0c;根据坐标统计上图的rgb平均值&#xff0c;结果放在一张Exc…

群晖异地组网-节点小宝搭建使用指南(全平台异地组网)

内网穿透&#xff0c;对于经常传输小文件、远程控制NAS的朋友来说是够用了&#xff0c;但是对经常异地端到端大文件传输需求的朋友来说就差点事&#xff0c;有没有一种免费、速度快、配置简单得方式的呢&#xff0c;答案是有的。节点小宝异地组网是一个非常不错的方式&#xff…

表空间[MAIN]处于脱机状态

达梦数据库还原后&#xff0c;访问数据库报错&#xff1a;表空间[MAIN]处于脱机状态 解决方法&#xff1a; 1&#xff1a;检查备份文件 DMRMAN 中使用 CHECK 命令对备份集进行校验&#xff0c;校验备份集是否存在及合法。 ##语法&#xff1a;CHECK BACKUPSET <备份集目录…

大字体学生出勤记录系统网页HTML源码

源码介绍 上课需要一个个点名记录出勤情况&#xff0c;就借助AI制作了一个网页版学生出勤记录系统&#xff0c; 大字体显示学生姓名和照片&#xff0c;让坐在最后排学生也能看清楚&#xff0c;显示姓名同时会语音播报姓名&#xff0c; 操作很简单&#xff0c;先导入学生姓名…

信息抽取模型TPLinker

1.motivation 早期传统方法首先抽取实体再抽取它们之间的关系&#xff0c;但是忽略了两个任务之间的关联。而后期采取的联合模型都存在着一个严重问题&#xff1a;训练时&#xff0c;真实值作为上下文传入训练&#xff1b;推理时&#xff0c;模型自身生成的值作为上下文传入&a…

代码随想录算法训练营第21天|● 530.二叉搜索树的最小绝对差 ● 501.二叉搜索树中的众数 ● 236. 二叉树的最近公共祖先

二叉搜索树的最小绝对差 题目连接 https://leetcode.cn/problems/minimum-absolute-difference-in-bst/ 思路&#xff1a; 利用二叉搜索树的中序遍历的特性&#xff0c;将二叉树转成有序数组&#xff0c;进而求任意两个数的最小绝对差。 代码 /*** Definition for a bina…