文章目录
ML Versus Racket
本章主要比较静态类型与动态类型语言的差别,基本上就是编程语言哲学了,挺抽象的,学起来还是需要一些自主思考。先比较ML和Racket。最大的区别是有无type system(不过Racket有Typed Racket,但不在我们课程范围内)
从双方的视角来看ML或Racket
抛开语法之类的东西不谈,ML是一个定义的很好的Racket子集,ML能运行Racket的部分程序,也会拒绝Racket的部分程序(例如类型检测不通过的)。这一部分被拒绝的程序,有可能是存在bug的程序(理应被拒绝),但也有可能是因为限制太严格被拒绝(不会有bug的程序,不应该被拒绝)。
然后从ML的视角来看Racket,认为Racket是一个庞大的数据类型,相当于把Racket各种类型看做某种datatype的值,这样所有的Racket类型都是同一种datatype(在ML中就被判断为同一种类型),因此理所应当地能被混合应用(比如list中既有Int也有String,他们都被看做theType的值)
What is Static Checking?
首先定义static checking。type system只是static checking的一种实现方式。
type system的作用包括:
(1)为每个变量、表达式等赋一个type
(2)目的是防止基元误用、加强抽象、避免动态类型检查(有了静态类型检查就不需要运行时检查类型了)
以ML为例:type system需要保证程序运行时不会出现一下情况(拒绝一些情况):
必须注意的是,静态检测static checking是语言定义的一部分,对于某种语言而言,它的检查规则就是一定的。如果修改其中某些规则(例如删除pattern match 中的冗余pattern检测),虽然不会导致什么特别大的bug,但整个type-checking发生了一些细微的改变,所以语言定义本身也改变了。
相当于我们实现了一种稍微不同的语言,因为静态检查接受了一组不同的程序。按老师的话来说:
静态检查是语言定义的一部分,因为它影响合法程序的内容。ML中的冗余模式检查有点不寻常,因为如果我们删除它,并没有什么“不好”的事情发生——我们只是有一些程序不能使用某些分支。尽管如此,如果我们改变了语言实现执行的静态检查,那么我们就改变了语言。
type system也要允许程序出现某些情况,让某些情况在运行时判断(某些与类型本身无关且无法在未运行前就发现的问题):
拒绝一些情况的目的:
我们既希望在一个bug造成严重问题(变得严重)之前捕获它,也希望在bug不严重时不被过分汇报。这两者之间存在一种固有矛盾。
动态和静态检查是在一个连续过程中的两个关键点,他们有不同的急迫(eagerness)程度。
Soundness and Completeness
type system的稳健性和完整性,二者通常是相悖的。
稳健是不会通过任何出现X的程序,没有漏报(假阴性,false negatives)发生
完整是不会拒绝任何没有出现X的程序,没有误报(假阳性,false positives)发生
通常,一个编程语言的type system的目标是稳健,而不是完整。我们让type system 更“花俏”(fancy)是为了在绝对稳健的情况下追求更少的误报(保证用户能够使用其他不被误报的方式来通过type check)
ML静态检查是典型的Sound but not Complete。ML一些误报的例子:
不完整的原因:
很多因素在静态环境中是无法决策的,无法在静态时判断(所以不得不一刀切)。
没有一个static checker能够同时(1)总是终止(不运行,静止)(2)稳健(3)完整
如果有一个检查器,当且仅当没有错误参数数目错误发生时,它会接受一个程序,这将是有用的。但是,没有一个总是终止的过程可以为编程语言实现这样的检查器(除非该语言限制太大,不能实现所有程序)。
那么编程语言如何处理不稳健的情况呢(因为编程语言通常都需要实现完全稳健的目标)。
如果选择直接允许X通过(比如为X情况设置一个默认值),那么更糟糕的情况可能发生,可能我们允许了X,同时让程序变得可能出现任意情况的错误(之后使用C/C++讨论这一点)。
Weak Typing
本节前言:强类型和弱类型的概念一直是比较很模糊的:
(1)有些人认为有严格的类型限制,不能进行类型之间的转换(不论是显式还是隐式)才能算的上强类型;
(2)而有些人用“强类型语言”称呼那些,省略了隐式类型转换(就是说编译器为了利益编程者而插入的转换)的语言(也就是只能显式转换的语言),对于他们而言,编程语言是强类型的,如果类型必须通过通常叫做“强制”的显式符号来转换;此时的C/C++、Java等都是弱类型的。
(3)还有些人认为**(可能有误解在其中),必须先声明再使用,且类型必须严格符合定义声明、不可改变的语言就叫强类型(强类型定义语言),这种定义里面C/C++、java、C#都算是强类型语言(如果想要将某种类型和其他类型同时应用,就要(显示或隐式)“强制”**类型转换),而JavaScript这种属于弱类型(能将某种类型不进行任何转换,就视为其他类型进行应用)。
持第三类观点的人是居多的,如果在谷歌百度上搜索,也通常会出现第三类定义。但这并不代表谁人多谁就是对的,很多人对编程语言的基本认知是有误的(有些人连强弱类型和动静态类型都分不清楚,写些博客和文章来误导人)。
强弱类型其实是类型检查的严格程度,严格程度没有一个绝对的标准,很多时候是相对的,比如C可能比JavaScript更严格,然后有一种静态的语言(不运行运行时任何修改,包括类型转换),那么他就比C更严格,更“强类型”。
但如果实在要给一个客观的标准线来区分强弱类型,那么上述三种标准都有一定的道理。
甚至,在课程中,老师给出了weak typing的另一种概念,在这种标准下,C/C++是(相对)弱类型,而Java、Python等是强类型。
很多时候,明晰一个概念是对学习编程有用的(脑子里有一定了解),但这不代表我们在真正应用时需要死死地去纠结这些“无用的术语”,别被概念框死了,WIKI上有一句话:
编程语言专家 Benjamin C. Pierce,《Types and Programming Languages》和《Advanced Types and Programming Languages》的作者,曾说:
“我花了几个星期…试着弄清楚“強类型”、“静态类型”、“安全”等术语,但我发现这异常的困难…这些术语的用法不尽相同,所以也就近乎无用。”
回到本节的课程:
weak typing在这里是指某些语言例如C/C++,使用了static checking,但dynamic checking是可选的(并且在实际中不会动态检查),所以也不能检查出某些动态的错误(静态检查难以检查运行时错误)。
这样虽然让程序实现变得更简单(将检查工作留给了编程者),让程序表现效果更好(时间上不需要动态检测,空间上不需要储存动态检测的变量、环境等),让程序更“低级”(更接近底层,编译器中没有数组size等信息,所以不会做这些检测)。
老师最后纠正了一下weak typing弱类型的定义:指那些既不静态类型检测也不动态类型检测的语言。
所以像C/C++这种“弱类型”属于是**“strong type for weak minds"**,在静态检查上很严格、但不做或少做动态检查(把这部分的工作交给人类程序员)。大型程序是不能容许漏检的,一个bug就会让程序十分脆弱。因此作为程序员,需要计算机能给的所有帮助,而不是weak minds。
**Racket是个动态类型的解释型语言,但它不是弱类型的语言。**虽然它(大部分)只进行动态检查,但它在动态检查中检查了尽可能多的事情(这也说明强弱与动静态检查本身实际上关联不大,而是取决于是否做了足够的检查)。动态检查的定义是,如果实现可以分析代码以确保某些检查是不需要的,那么它可以优化掉它们。
当然Racket也会有一些静态检查(例如macro和未定义变量)
再次解释课程中的weak typing弱类型
弱类型实际上是指一种类型系统,该类型系统允许正确的语言实现对程序做任何事情,甚至是与程序中的代码完全无关的事情
另外一些错误观念:
哪些行为是基元定义的,哪些是会导致错误的
这不是关于动静态类型的讨论,而是关于“什么是基元定义的运行时语义”,是一个语义问题(之前讲过,语义关乎evaluation)。我们对这些语句的不同对待结果,来自于我们不同的evaluation定义,比如某些语言定义时“foo” + “bar”就会被evaluate成为字符串相连接,但某些语言没有定义这样的evaluation规则,自然就在check时不允许。(因此这当然就不是动静态类型的问题,而是语义定义(也就是evaluation rules)的问题)
Static Versus Dynamic Typing, Part One And Part Two
前面比较了各种语言,也讲了很多预备知识,现在正式来对比静态和动态类型
(1a)dynamic更便利的场合,例如创建更多元化的列表或者直接返回one-of的类型
(1b)static更便利的场合:可以假设数据拥有期待的数据类型,不会有动态检查的语言错误或者逻辑错误的errors等(因为静态检测帮编程者检查过type了,例如ML中的x*x*x(根据*乘法运算规则)就一定被判断为数值类型比如int*int*int)
(2a)static拒绝有用的程序:这点很好理解,因为static完整性更差,所以会拒绝更多程序,以求严格的type checking不漏检。下面的例子就是因为ML function的参数type check必须是同一类型,所以被ML拒绝,而Racket不存在这个问题。
(2b)static让编程者能够在需要的时候标记:相比于动态语言对每个类型的值都标记或记录到环境(耗费大量时间、空间,并且有error风险),static可以通过自定义datatype或者类似结构,给需要tag的内容tag。要理解这一点,就需要回顾本章第一节,**“从ML的视角来看Racket“**的内容,动态语言相当于为每个类型都进行tag。
(3a)static更早捕获bug:这一点不难理解,比如某些类型不匹配的问题,static在编译时就发现问题,但dynamic需要在运行时才有可能发现。下面的例子是函数调用方法不对,也是一种类型不匹配问题,Currying的函数必须用Currying的方法调用(否则参数类型肯定不匹配,比如ML的pow x y,应该是两个int参数,但pow(x,y-1)就只有一个元组参数,这是不匹配的类型。但Racket中,只有运行时,函数真正被调用时,才会发现这个类型不匹配问题)
(3b)static只能捕获一些简单的bug(其实算是能够更早捕获bug的代价):某些错误只有运行时才能捕获(甚至有些逻辑错误运行时都不能捕获),这些错误当然就不能被static捕获到。
(4a)static更快:程序实现时在时间空间上都有一定优势(运行时不存储tag,不检查tag),编写代码时也不需要检查参数和结果(的类型)
(4b)dynamic也有更快的场合:程序实现时也能够实现优化,去去掉一些不需要的tag和test(以加快速度减少空间),即使这样做实现比较难。编写代码时不需要手动去编写一些额外的tag来添加type-system的限制。
例如下面例子中,两个y只需要检测其中一个(tag和test),并且因为x的值来自于两个y相加的运算,x的类型也不用检测。
(5a)dynamic的代码重用更简单:这不难理解,因为编译时不检查类型,动态语言实现多态更容易。
(5b)static代码重用更简单的场合在于:现代语言提供可重用的特性,例如泛型和子类型等。不过这些特性的使用也会带来混淆,让代码更难以debug。
小结,5个角度的比较。
现实中,程序都是在不断迭代的,从原型的实现,到正式版的维护升级。所以比较动静态语言也需要从这个角度来看。
(6a)dynamic更适合实现原型的方面在于:可能不确定你需要的数据类型或者函数,动态语言完整性更强(即不那么严格)。
(6b)static更适合原型的方面在于:type system是一个很好的手段来记录(document)并检查(check)不断迭代的程序中数据结构和代码,代码更规整,引入一些临时的代码残段也更容易。
(7a)dynamic更适合迭代的方面在于:在不影响使用者的情况下,能更宽松地更改代码(更不容易去修改函数中用到的各种数据结构)。
(7b)static更适合于迭代的场合:当我们改变了某种类型或代码,type-checker会给出“to-do”list,其实就是类型检测器会检查出来哪些位置使用了被更改的类型或代码,这给程序编写者提供了一个需要修改的“to-do”list,因此能够在出bug之前修改(avoid introducing bug)。而动态类型就很难做到这一点,一旦修改了某个代码,可能被影响的位置难以明确(也许只有出bug的时候才知道)
真就是编程哲学了,我们的实际问题不在于讨论哪种更好,而是**“我们需要什么东西静态执行”**。动静态类型有各自的代价,在需要的时候使用合适的类型检查方式。
题外话:看看作业中的第四题,这里想要记录一下
有四个选项大概分别是:
A.ML without change is sound …
B.ML with change is sound …
C.ML without change is complete …
D.ML with change is complete …
我一开始选了AD但错了一个选项,所以正确答案应该是AB,ML本身是sound的应该没问题,然后因为更改成为题述规则之后,ML仍然没有漏检错误的情况(理论上多一些参数应该不会造成太大的问题),虽然变得稍微complete了一些但总体上还是sound的(没有漏检的实用type checker)。应该是程度达不到complete(complete没有误报的代价是或多或少会漏检)。之后再做一遍题目,验证一下这个想法。
Optional: eval and quote
eval就是在运行时获取某种数据结构,并将这种数据结构作为一段程序,然后运行的方法。(这一节需要接在上周的内容后面一起看)。eval其实就需要有一种程序实现方式来支撑(解释、编译、或者两者结合),类似于上周的eval-exp。
eval在Racket中的实现:
对于实现了eval的程序会更难分析,因为相当于程序中夹杂了另一段程序。如果eval计算的是Racket程序,那么实际上使用到的就是Racket的解释器(如果是JavaScript的eval计算JS程序,那么就使用JS的解释器)
Racket中两种参与eval的数据结构,一种是带符号的list,其中的symbol就是关键词e0,列表元素就是使用的参数表达式e1…en。另外也可以用quote引用结构直接将一段Racket语句包括进来。
Quote的语法如下。在Ruby、Python、Perl或者JS中的eval使用含有代码语句的字符串作为eval的参数来计算,实际上也是一种Quote(字符串毕竟就是“引号”表示的)