Week 8
ML与Racket ML vs. Racket
- 最主要的区别:
- ML具有一个类型系统,能在程序运行前拒绝掉不能完善类型细节的程序
- Racket对待程序类型细节更加宽容,并具有更加复杂的运行时错误
- 从Racket看ML:撇开语法结构等细节,ML很像是Racket的一个子集
- 从ML看Racket:有一个非常大的数据类型
- 现有的类型即对于这个类型的内建构造器
- 每定义一个结构,创建一个新的构造器
静态检查 Static Checking
- 静态检查是在程序被解析后,运行前可能被拒绝的一系列过程
- 作为语言定义的一部分,静态检查只允许合法的(符合定义的)程序进入运行阶段
- 一个程序语言定义一个静态检查的主要方式是通过一个类型系统
- 方法是对每一个变量和表达式等,赋予一个类型
- 目的是防止基础类型的误用,强制抽象化以及避免动态类型检查
- 修改静态检查规则,意味着修改一个语言(修改了定义)
- 但是,静态检查(尤其ML中是类型系统)不会做所有事,其不会防止所有的错误
- 通常情况下,没有类型系统会防止逻辑或算数错误(因为这是程序的一部分,类型系统看不懂这是什么)
- ML的类型系统实际上是避免了在运行时会产生错误的某些行为,但是这和类型系统如何定义是相互分开的两件事
- 因此语言设计包含两件事:
- 类型系统的目的,要检查些什么
- 如何实现上述目的
- 最难的地方在于如何确保“方法”真的实现了“目的”
- 实际上静态检查或者动态检查只是在一个连续过程上的不同的两个点,其选择在一个过程的不同阶段阻止一些事情的发生
健全性和完备性 Soundness and Completeness
- 对于一个类型系统的评价:健全性和完整性
- 假设一个类型系统应当将某些X从一些X中阻止掉(比如条件判定变成了字符串,变量未定义等等)
- 一个类型系统是健全的,则其永远不会接受一个,在接收了输入后执行X的程序(无假阴性判定,不放过任何一个可能违例)
- 一个类型系统是完备的,则其永远不会拒绝一个,在不管接收了什么输入,都不会执行X的程序(无假阳性判定,不包含任何一个可能正例)
- 对于一个类型系统,其设计的目标是达到健全性而不是完整性(以牺牲部分正例的代价拒绝一切违例)
- 一些高级类型特性,如泛型,主要用于减少假阳性情况出现的数量
- 几乎所有想要在静态时期就终止的事情都会具有一个性质:不可判定(Undecidable)
- 任何静态检查器不能完全同时做到以下三点:
- 经常终止
- 健全
- 完备
- 任何静态检查器不能完全同时做到以下三点:
- 不可判定性是计算机的一个重要理论
- 静态检查固有的近似性(不能绝对完备)是最重要的衍生
- 不健全的情况
- 修复之
- 使用动态检查以避免
- 直接允许(脚本语言)
- 允许任何事情发生(出问题了也能执行一些事情)
弱类型检查 Weak Typing
- C和C++使用的类型检查方法
- 弱类型检查:存在一类程序,根据语言定义其必须要通过静态检查但是在运行时可以做任何事情
- 在运行时可以避免错误的动态检查变成了可选项,实际上根本就不做(运行时可能发生任何事情)
- 弱类型检查的原因:
- 更加简便的语言实现(便于在不同的平台上实现,将这些)
- 性能原因(动态检查更加耗时)
- 低级语言(没有对数据结构加入额外的信息,因此无法进行动态检查)
- 弱类型检查实际上就是既不做静态检查,也不做动态检查:完全允许发生任何事情,
- Racket不是弱类型的
- 执行动态类型检查:任何事情都是动态检查的
- 动态类型检查是其定义的一部分:如果语言的实现能够分析代码并确保这些检查是没必要的(代码绝对不会出错),那么检查可以被移除(对于弱类型,实际上是在语言定义上通过了的内容,但是会做一些非法的,意想不到的事情)
- 对于基本类型的操作有时是多种多样的,而对其的评估往往既不是静态检查也不是动态检查,是一种一定程度上改变了评估规则的一种折衷,这是一种语言上的灵活性
静态检查与动态检查 Static Versus Dynamic Typing
- 绝大多数语言会是两者中都取一部分来做(比如,基本数据类型是静态检查,而数组边界是动态的检查)
- 第一个争论(方便):
- 动态检查更加方便(实现一个异构列表或者返回多种类型的值)
- 静态检查更加方便(可以充分假设传入数据正确而不需要在函数内容动态检查类型并处理相关错误)
- 第二个争论(阻止有用的程序):
- 静态检查阻止了一些有用的程序(健全性决定了一些情况下理应正确的程序将会被静态检查阻止)
- 静态检查可以按需标记类型(ML中的One类型,决定了那些可以同型而论)
- 第三个争论(尽早找到bug):
- 静态检查可以更早地找到bug(编译时错误可以在测试时直接跳过)
- 静态检查只能检查出简单的bug(虽然能一定程度上找到bug,但是程序内容还是需要进一步测试)
- 第四个争论(性能):
- 静态类型检查更快(既不需要浪费检查类型标签的时间,也可以节省打标签的空间和时间,不需要不停的判定参数的类型)
- 动态检查更快(使用优化方法减少了大量不必要的标签和测试,因此在一些对性能要求高的场景)
- 第五个争论(可重用性):
- 动态可以让重用代码变得更加容易(没有严格的类型限制,能够更加频繁地复用代码)
- 静态可以让重用代码变得更加容易(现代类型系统使用泛型或者子类型支持合理的代码复用,将一些独立的想法但是重用了一些代码的情况分隔开,可以避免对库的误用)
- 第六个争论(建立原型):
- 动态检查对于建立原型更好(最开始并不能知道需要哪些类型和函数,在静态类型中需要将所有的情况都实现才能通过检查,因此需要大量不成熟的设计,后者效率并不高)
- 静态检查对于建立原型更好(对系统的演变确保类型一致性,对于一些不必要的分支也可有很好的方式规避,比如产生一个异常)
- 第七个争论(软件内容演变):
- 动态检查更容易演变,因为可以在不改变原有的函数调用方式的前提下增加更多的调用方式
- 静态检查更容易演变,因为类型检查器会给出一个“待处理表”(原有内容因改变类型报错),指明因类型变动而需要改动的地方
- 一个正确的辨证思路:究竟哪些内容需要静态检查,即便面对不可避免的假阳性情况;而哪些应该动态检查,在测试和运行中发现问题
eval
& quote
- 很多语言中都包含
eval
- 在运行时创建一些数据(在Racket中通常是列表,JS中通常是字符串)
- 将这些数据当作另外的一个程序并运行之(使用同一种语言在一个已经运行的该语言编写的程序中运行该段“数据程序”)
- 对于动态类型,我们可以直接将这段数据送入解释器运行;对于编译器,我们可能需要将处理一些语言实现
- 引用(quoting)的表示
(quote ...)
和‘()
是一种特殊形式,将其中的内容变成原子化内容(列表)而非变量或是调用(list 'begin (list 'print "hi") (list '+ 4 2))
(quote (begin (print "hi") (+ 4 2)))
- 二者等价
- 还有一种准引用(quasiquoting),及将其中内容变成非引用(unquote),并将其运行结果送入这个“程序”的对应位置