键入的Lisp,A Primer(2019年)

2020-07-19 03:10:42

摘要我们先与Haskell作一个简单的比较,快速浏览一下类型理论,然后徒劳地试图为动态方法辩护,给出一个有点幽默的历史描述,注意到您已经上当了-类型一直都在那里-然后深入一些Lisp类型的技术细节,最后展示宏是如何允许输入的。

展示如何在Lisp中定义像Pair和可能这样的代数多态类型。包括异构类型的列表!

除非另有说明,否则短语“Lisp”指的是Emacs Lisp支持的Common Lisp。因此,由此产生的讨论适用于许多Lisp方言--我暂时忽略了诸如缓冲区和键映射这样的编辑类型。

我已经说服了我的许多同行使用Emacs/Spacemacs/Doom-emacs,但是我让他们甚至考虑尝试Lisp的努力遭到了坚决的拒绝。这些同行熟悉Haskell,几乎所有人都知道AGDA,所以你会认为他们会愿意尝试Lisp-it‘s,这是他们编辑器的一部分-但是在语法和类型方面表面上的鸿沟已经足够明显了。在这篇文章中,我的目标是探索(Emacs)Lisp的类型系统,并偶尔将其与Haskell进行比较。在这篇文章中,我的目标是探索(Emacs)Lisp的类型系统,并偶尔将其与Haskell进行比较。在这篇文章中,我的目标是探索(Emacs)Lisp的类型系统,并偶尔将其与Haskell进行比较。也许到最后,我的一些哈斯克尔同行会愿意尝试一下。

✓它的社区所表达的思想就是我努力更新该语言的原因。

↯我周围的人对Lisp一无所知,但由于父母的原因,他们不喜欢它。

✓我喜欢它,并将其用于Emacs配置,最近还用于我的博士研究原型。

⇅我喜欢我可以用压缩、解压缩、过滤器和映射用这两种语言简洁地表达一个复杂的过程(งಠ_ಠ)ง最近,在Lisp语言中,我会写一个嵌套循环(喘息!)然后,为了好玩,试着让它成为一行程序!有时,我实际上认为循环公式更清晰,我把它当作一个循环突发新闻:两个哈斯克尔读者刚刚死了。

说Lisp的类型系统比Haskell更有表现力可能并不完全准确,它在许多方面是正交的,尽管它更接近Liquid Haskell的类型系统。

类型允许我们按照类似的结构或接口处理对象。与Haskell和其他静态类型系统不同,在Lisp中,类型可以重叠。因此,这里是我们的工作定义。

要说“\(e\)有类型\(τ\)”,您可以这样写\(e:τ\),或者在Lisp中写:(类型e';τ)。

Haskellers和其他人可能会在这个定义上附加以下内容,我们将不再赘述:类型成员资格是通过检查语法结构来确定的,因此是可以决定的。

✓类型化是最简单的“断言-注释”形式之一:以机器可以验证的方式记录代码的属性。

如果您要评论您正在处理的是哪种类型的东西,为什么不让机器检查一下您的意见。

(或τ₀τ₁…。τₙ)具有元素任何类型的元素τᵢ中的任何元素。

(和τ₀τ₁…。τₙ)具有所有类型为τᵢ的元素

(成员x₀…。Xₙ)是仅由元素xᵢ组成的类型。

;通用类型“t”,值为所有。(typep';x&39;t);;⇒TRUE(typep 12;t);;⇒TRUE;;空类型:nil(typep';x&39;nil);;⇒false;nil没有值。;类型“NULL”包含一个值“NIL”。(类型NIL';NULL);;⇒TRUE(TYPEP()';NULL);;⇒TRUE;;“(EQLx)”是仅由x组成的单调类型。(类型3';(EQL3));;⇒TRUE(类型4';(EQL3));;⇒FALSE;“(成员x₀…。Xₙ)“表示仅由xᵢ组成的枚举类型。(类型3';(成员3 x";c";));;⇒TRUE(类型';x';(成员3 x";c";));;⇒TRUE(类型';y';(成员3 x";c";));;⇒False;“(满足p)”是满足谓词p的值的类型。(类型12';(满足(λ(X)(Oddp X);;⇒FALSE(类型12';(满足⇒));;EVEMP TRUE;;理解类型的计算规则。;;(类型x';(满足p))≈(if(P X)t nil)。

像Haskell这样的强类型语言只允许上面列出的一些类型。例如,Haskell不允许联合,而是提供所谓的sumtype。此外,与Haskell不同的是,Lisp是非参数的:我们可以根据值的类型选择计算的分支。这样的案例分析在C#-c.f、is is as或is is as等语言中都可用。最后,重要的是要认识到cons是一种单态类型-它只表示由Car和Cdr两个部分组成的(任意)元素-我们将在下面展示如何形成多态产品类型。

我们可以要求对象的“基元类型”;这是它所属的最简单的内置类型,例如整数、字符串、cons、符号、记录、subr等。因此,Lisp对象带有固有的基元类型;例如,';(1";2";';3)是一个列表,如果使用显式强制,它只能被视为另一种类型的值。在Lisp中,它不是变量,而是与类型相关联的值。可以选择声明变量的类型,就像在OCaml中一样。

类型关系“:”通常在静态语言的第二个参数中是确定性的:e:τ∧e:τ‘⇒τ≈τ’。然而,这与Lisp的类型不同。

从某种意义上说,动态语言使得生成多态函数变得容易。传统地说,只有当你认识到类型和类型变量的重要性时,前面的句子才有意义。

在Lisp中,类型是推断出来的,不需要声明。但是,声明对其他读者来说是一个很好的文档;-)。

RE变量:只有静态⇒值可以更改;动态⇒值和类型都会更改。

我们可以使用typep检查项的类型,它的第二个参数是“类型说明符”-一个值表示类型的表达式;例如,下面的或表达式形成一个“联合类型”。

还有check-type:它类似于typep,但不会产生true或false,它在前者中保持不变,而在后者中发出输入错误的信号。

(CHECK-TYPE 12整数);;⇒NIL,即无错误(CHECK-TYPE 12(或符号整数));;NIL;即无错误(CHECK-TYPE";12";(或符号整数));;CRASH:类型错误!

类型是程序设计语言理论的核心组织原则,语言特征是类型结构的表现形式,语言的语法由定义其类型的结构决定,语义由这些结构之间的相互作用决定。

除了像数字和字符串这样的原子之外,在Lisp中形成新术语的唯一方法是使用“modus ponens”或“function application”。以下是第一个近似值:

F:τ₁→⋯→τₙ→τe₁:τ₁…。Eτₙ-:ₙ(f e₁…。Eₙ):τ。

人们对这样一个分数的理解如下:如果分子的每个部分--“假设”--都是真的,那么分母--“结论”也是真的。

抽象语法树,或“AST”,是一棵带有分支运算符和子代参数的树。如果最上面的分支操作符的结果类型为τ,则树属于τ类型。下面是一条改进后的规则:

F:τ₁→⋯→τₙ→τe₁:ASTτ₁…。Eτₙ-:ASTₙ(f e₁…。Eₙ):ASTτ。

然后,Lisp顶层可以执行或解释这样的形式来获得值:当我们在顶层编写e时,实质上调用的是(Eval E)。

我们有以下执行规则,其中‘⟿’表示“Reduce to”。

(Eval A)⟿a;;表示原子‘a’(eval(引号e))⟿e(eval(f e₁…。Eₙ))⟿(f(eval e₁)⋯(eval eₙ));;实际调用‘f’

还有变量的“作用域”或“生存期”问题。

也就是说,动态作用域意味着局部变量不仅在作用域的其余部分中充当全局变量,而且即使在作用域中调用的预定义方法的定义中也是如此。

(setq it&34;再见#34;)(deun go()it)(let((It 3))(Go));;⇒3;即使“it”在文本上没有出现!;;临时启用Emacs Lisp中的词汇绑定(setq词汇绑定t)(let((It 3))(Go));;⇒再见;就像大多数语言一样。

当我们想要做涉及有副作用的实用程序的单元测试时,这是非常棒的:我们只需在本地重新定义副作用组件,比方说,什么都不做。(─‿‿─)

LISP有一种强制形式;但是强制语义在任何语言中通常都是不健全的,因此应该非常谨慎地使用。(尽管Haskell有一些合理的强制,也有一些不安全的强制。)。

我们有一种神奇的方法将α类型的元素转换为β类型的元素,有些语言称之为类型转换。

我们可以使用形式执行类型批注;例如,Haskell表达式(1::int)+2检查类型批注,如果通过,则产生值并计算表达式。同样,(类型名称)生成名称,前提是它具有类型类型。

(+(整数1)(整数2));;⇒3(+(整数1)(整数";2";);;⇒类型错误。

有时一个值可以是几种类型中的一种,这是使用联合类型指定的;嵌套的联合本质上是扁平化的-这是‘or’的一个属性,我们将会看到。

(文字12;#39;整数);;⇒t(文字#39;a';符号);;⇒t(文字#39;a';符号);;⇒t(文字#39;(或整数符号));;⇒t(文字#39;(或整数符号));

当给定联合类型时,我们可能希望根据值的类型进行计算。

如果没有匹配的大小写,则返回nil;使用etypecase代替nil返回错误。

类型不是Common Lisp中的对象。例如,没有与整数类型相对应的对象。我们从type-of这样的函数得到的,并作为参数提供给typep这样的函数,不是类型,而是类型说明符。类型说明符是类型的名称。-Paul Graham,ANSI Common Lisp。

(Typep x';τ)≈(τp x);;例如,τ≈整数(Typep x';(和τ₁…。τₙ))≈(AND(类型xτ₁)…。(Typep xτₙ))(Typep x';(或τ₁…。τₙ))≈(或(类型xτ₁)…。(Typep xτₙ))(Typep x#39;(非τ))≈(Not(Typep xτ)(Typep x#39;(成员e₁…。Eₙ))≈(或(Eql x e₁)…。(eql x eₙ))(类型x';(满足p))≈(P X)。

如果我们经常使用类型说明符,我们可能希望使用deftype宏将其缩写-它类似于def宏,但可以扩展为类型说明符而不是表达式。

你可以直接做第三个,但是使用类型描述符的谓词形式是很有用的。

例如,下面是从(-∞..9]中提取的一类数字列表的三个步骤。

;;制作谓词(去掉小数序列p(事物)(AND(Sequencep Thing)(Every#';number p thing)(Every(lambda(X)(<;x10))thing);测试它(setq yes';(1 24))(setq no';(1204))(set no';(1204))(Small-number-seq-p yes);⇒t;;注册它(Deftype Small-Number-Seq-Seq。(满足小数序号));;使用它(类型是#39;小数序号);;⇒TRUE(类型否#39;小数序号);;⇒FALSE。

除了没有显式缺省值的OptionalArguments使用*而不是nil作为默认值之外,参数的处理方式与def宏相同。以下是来自deftype文档的一些示例:

(cl-deftype null()';(满足null));预定义(cl-deftype list()';(或null cons));预定义(cl-deftype无符号字节(&;可选位)(list';整数0(if(eq位';*)位(1-(lsh 1位);某些等价物(无符号字节8)≡(整数0255)(无符号字节)≡(整数0*)无符号字节≡(整数0*)

注意,类型说明符基本上位于它们自己的名称空间中;例如,null是检查列表是否为空的谓词,而null是指定此类列表的类型。

让直接形成一种类型的对-这并不理想!这是一种多态数据类型:它接受两个类型参数,下面称为a和b。

(deftype对(a b&;可选类型)`(满足(lambda(X)(and(Consp X)(typep(Car X)(引号,a))(typep(Cdr X)(引号,b)(typep#39;(";x&34;)。2)';(配对字符串整数);;⇒TRUE(类型';(";x";。2)';(成对符号整数);⇒FALSE(类型为NIL';(成对整数));;⇒FALSE(类型23';(成对整数));;⇒FALSE(集合ss";NICE";nn114)(类型`(,ss.。,nn)';(配对字符串整数);;⇒TRUE(typep(Cons Ss Nn)';(配对字符串整数));;⇒TRUE;;以下为FALSE,因为ss和nn是引号符号!(typep';(ss.。Nn)';(配对字符串整数);⇒FALSE(类型`(Cons Ss Nn)';(配对字符串整数));;⇒FALSE。

练习:定义多态的可能类型,这样(可能是τ)的元素要么为空,要么值为τ。

让定义类型List-of,使得(List-ofτ)是其元素都是τ类型的值的列表的类型。

;;使谓词(DeFun List-of-p(τThing)(AND(Listp Thing)(Every(Listp Thing)(Every(lambda(X)(typep xτ))Thing);;测试它(List-of-p';整数';(1 2 3));;⇒TRUE(List-of-p';Integer';(1 Tw3));;⇒False(List-of p';String';());⇒TRUE(List-of-p';String';(No));;⇒FALSE;;Register it(Deftype List-of(τ)`(Ssubfies(Lambda(Thing)(List-of-p(QUOTE,τ)Thing);;使用它(Typep';(1 2)';List);;⇒True(Typep&39;(1 2)';List);⇒True(Typep'。(1 2)';(整数列表);⇒TRUE(类型';(1";2";)';(字符串列表));;⇒FALSE(类型';(1";2";)';(列表的(或整数字符串);;⇒TRUE。

请注意,通过最后一个示例,我们可以控制列表中的异构度!太酷了!

这里还有一些练习。第一个应该几乎是琐碎的,第二个稍微多一点工作,最后两个让我#难过。

定义一个类型(ROSEτ),其元素可以是τ值,也可以是τ类型的玫瑰树。

定义类型记录,以便(Recordτ₁…。τₙ)表示其Iᵗʰ组件具有类型τᵢ的记录类型。

定义一个类型构造函数∃,例如,(∃τ(Pair Integerτ)表示成对的类型,其中第一个组件是整数,第二个组件都具有相同的类型τ,但是我们不知道是哪一个。

我的想法是让τ指示值在该位置第一次出现的类型,然后所有后续检查现在都引用这个τ值。

生成么半群的记录并跟踪生成的么半群实例。定义谓词(Monoidτ)以检查是否有任何幺半群实例的载体类型为τ。通过这种方式,我们可以模拟Haskell类型类。

数据表达式a=vara|expr a:+:expr a|neg(Expr A)派生Show ex::expr Int ex=var5:+:(var6:+:neg(Var7))int::expr Int->;Int int(Varn)=n int(l:+:r)=int l+int r int(Neg E)=-(Int E){-int ex⇒4-}。

如果我们查看构造函数声明C a₁…。带有多余括号的ₙ(C a₁…。Aₙ),那么翻译成Lisp会立即显示出来:

(deFun exprp(τThing)(pcase Thing(`(var,n)(typep nτ)(`(add,l,r)(AND(exprpτl)(exprpτr)(`(neg,e)(exprpτe)(setq ex';(add(Var 5)(add(Var 6)(neg(Var 7)(exprp';整数ex)。本文末尾定义了“声明类型”。(声明类型int:(expr-of整数))(defun int(Thing)(pcase thing(`(var,n)n)(`(add,l,r)(+(Int L)(Int R)(`(neg,e)(-(Int E)(Int Ex);⇒4

当然,在Lisp中有更好的方法来做到这一点;例如,使用标识、+、就地的var、add、neg标签来生成“携带其语义的语法”,或者通过将正式标签替换为它们的解释来将解释器int表示为一行程序,然后调用Lisps eval。(=。我怀疑这两个想法都不是新想法,但前者的优点似乎很好--至少乍一看是这样。

在这里可以找到对Common Lisp中的ADT的支持,以及看起来不那么笨重的模式匹配-我只简要地看了一下。

Haskell演示文稿中包含类型检查功能,但是我们的Lisp解释器int没有!这似乎非常令人担忧,但是声明类型声明实际上为我们处理类型检查!

;;注册类型(deftype expr-of(τ)`(Ssubfies(λ(Thing)(Exprp(QUOTE,τ)Thing);;尝试一下(Typep';(1 2)';(expr-of Integer));;⇒nil(Typep ex';(Expr-of Integer));;⇒TRUE;例如,这种调用现在会产生有用的错误消息。给定CONS(变量6 4)时,参数0≠应为(表达式-OF整数)。;这是合理的,因为“var”构造函数只有一个参数。

重要的是要认识到,几乎每种语言都是打字的-尽管检查可能发生在不同的阶段-因此,正如本杰明·皮尔斯所说:“动态键入”这样的术语可以说是用词不当,可能应该被“动态检查”取代,但用法是标准的。

特别是,动态类型化并不等同于非类型化,尽管有些人使用它的方式是因为几乎每种语言都是类型化的-可能只有一个匿名类型。

在我喜欢的Haskell社区中,有些人会说“如果它可以检查类型,就把它发送出去”,这通常是正确的,但这会导致一些人避免生成单元测试。例如,以下类型检查应该进行单元测试。

McBride::[int]->;Int McBride Xs=如果Xs为空,则标题Xs否则为666

无论如何,我总体上喜欢静态类型检查和静态分析,但是,向动态检查设置的转变引起了人们对单元测试的更大兴趣。例如,正如任何一个哈斯克勒都会自豪地说的那样(包括我在内),哈斯克尔对有效计算的解决方案是按类型划分的;但要问这样的计算是如何进行单元测试的,而房间里却是寂静的(包括我自己)。

有趣的是,一些单元测试检查输入和输出的类型,这是一个没有未知数的机械过程,因此应该可以使用Lisp宏为其生成语法。这是本文的目标之一,我们稍后再来讨论。

尽管我喜欢Lisp,但我不确定为什么动态类型是最合适的方式-c.f。动态语言是静态语言,它提到了类型系统的不公正暴政。下面是人们可能不喜欢静态类型的两个原因。

T:Then E:-B:𝔹如果T则E:αB:αα。

这意味着诸如If True Then 1 Else&34;Two";这样的有效程序将被拒绝;即使结果类型始终是整数,也无法知道选择是否需要在运行时重写和求值。

实际上,在Haskell中,我们会编写If True Then Left 1 Else Right";Two";type or Int string,要使用结果值,我们需要模式匹配或使用Haskell的Control.Arrow中的消除符(|)。

第二:一些静态类型的语言有超弱类型系统,会毁掉其他语言的性能。例如,C语言很棒,我们当然都喜欢它,但遗憾的是我们只能表达多态的标识函数(id:∀{α}。)。α→α\;=\;λx→x\),通过使用C预处理器-或者通过四处投射指针来取消类型。

这个视频可能有帮助,也可能没有帮助:动态打字对实际程序的不合理效果

(对于代数学家来说:动态类型就像是在一个么半群中工作,它的合成操作是局部的,可能会突然崩溃;而静态类型是在其组成被自豪地键入的类别中工作。)。

总体而言,我没有很好地为被动态检查进行辩护,但您应该忽略我的错误,考虑亲自尝试Lisp,看看它有多棒。

是。

.