programming-languages学习笔记–第7部分
目录
1 ML对比Racket
有很多共同点,也有一些不同点,最大的不同是ML的类型系统。
类型检查的优点与缺点。 ML在程序运行前通过类型检查拒绝程序并报告错误。 ML通过在编译时强制一些规则来确保没有某些错误(比如不能传递一个字符串给+)。
从Racket的观点看ML:
- 语法上,ML是Racket的一个子集:对于产生同样结果的程序,ML会认为很多程序不合法。 优点是ML会拒绝看上去像是错误的程序。
- 许多程序不允许有bug,比如:
(define (g x) (+ x x)) ;类型检查确保正确。
- 实际上,ML中不需要number?这样的判断
- 一些程序无法实现,比如:
(define (f x) (if (> x 0) #t (list 1 2))) (define xs (list 1 #t "hi"))
从ML的观点看Racket:
- 可以认为Racet有一个巨大的datatype,所有的值都是这个类型:
datatype theType = Int of int | String of string | Pair of theType * the Type | Fun of theType -> theType | ...
- 构造器是隐式的(值被标记)
- 42就是Int 42
- primitives隐式检查标记并提取数据,对错误的构造器抛出异常
theType内置的构造器:numbers,strings,booleans,pair,symbols,procedures,等
struct-definition创建一个新的构造器,动态添加到theType.(ML中不允许,但是异常exn与此类似)
2 静态检查
静态检查在程序parse之后运行之前进行,发现其中的错误。语法解析过程中的错误叫做语法错误,静态检查的错误叫做类型错误。
执行静态检查是语言定义的一部分。 常用的做法是通过类型系统进行静态检查。比如ML中每个语言构造的类型规则。 静态检查的目的是拒绝无意义或可能试图滥用语言功能的程序。 作为对比,racket使用动态检查(运行时检查)发现错误,为不同类型的值打上标记(tag),在函数应用时检查是否符合规则。
静态检查会拒绝一些不会产生任何错误的程序。
类型系统无法防止逻辑/算法错误。
类型系统麻烦的的部分在于确定类型检查确实达到目的。
静态检查/动态检查是整个流程中的两个点,编译时和运行时。 比如要防止3/0的错误,可以在以下几个方法入手:
- 键入时: 编辑器中不允许
- 编译时:代码中不允许出现
- 链接时: 不允许执行调用
- 运行时: 当进行除法时禁止
- 之后处理: 代替除法,直接返回+inf.0
3 正确性:可靠性,完整性,不确定性
使用精确的术语描述类型系统的正确性,比如要防止一些事情X:
- 可靠性:当类型系统从不允许一个程序使用某些输入时执行X操作,称它为可靠的
- 完整性:当类型系统从不拒绝一个不管什么输入都不会执行X操作的程序,就是完整的。
可以认为可靠性防止漏报,完整性防止误报。
可靠性很重要,因为它让语言用户和语言实现者可以依赖X永远不会发生。
ML的类型检查器就是不完整的,有些正常的程序无法通过检查,比如: if true then 3 else "hi"
不确定性是计算理论研究的核心。
4 弱类型
weak typing:程序必须通过静态检查,但是运行时没有任何限制。 c/c++,最常见的就是数组越界访问。
Racket是动态类型,运行时有类型检查,不是弱类型。
5 静态类型vs动态类型
静态类型在早期发现很多错误,可靠性保证一些类型的错误不会发生,不完整性意味着一些完美的程序被拒绝。
5.1 静态类型或动态类型哪个更方便?
动态类型的优点是可以混合和匹配不同类型的数据,而不需要声明新的类型定义或使用模式匹配。
另一方面,静态类型可以假定数据有正确的类型,不需要额外的代码进行检查:
(define (cube x) (if (not (number? x)) (error "bad arguments") (* x x x))) (cube 7)
fun cube x = x * x * x cube 7
5.2 静态类型是否限制一些有用的程序?
静态类型不能实现一些程序,比如:
(define (f g) (cons (g 7) (g #t))) (define pair_of_pairs (f (lambda (x) (cons x x))))
fun f g = (g 7, g true) (*无法通过类型检查*) val pair_of_pairs = f (fn x => (x,x))
静态类型可以按照需要进行tag,使用datatype,实践中很少需要,(Racket总是包含tag):
datatype tort = Int of int | String of string | Cons of tort * tort | Fun of tort -> tort ... if e1 then Fun (fn x => case x of Int i => Int (i * i *i)) else Cons (Int 7, String "hi")
支持静态类型的论点是现代类型系统具有足够的表达能力,很少会妨碍你的工作。
5.3 静态类型的早期错误检测重要吗?
因为静态类型检查可以捕获已知的错误类型,所以可以使用这些知识将注意力集中到其他地方。
(define (pow x) (lambda (y) (if (= y 0) 1 (* x (pow x (- y 1))))))
fun pow x y = (* 无法通过类型检查 *) if y = 0 then 1 else x * pow (x, (y - 1))
动态类型的支持者会说静态类型检查只能发现测试中可以捕获的bug,对于语义错误无法发现。
5.4 动态类型还是静态类型能带来更好的性能?
- 静态类型更快,语言实现:
- 不需要存储tags(空间,时间)
- 不需要在运行时检查tags进行类型测试(时间)
- 动态类型反对意见:
- 在大多数软件中,这种低水平的性能并不重要。
- 语言实现可以优化移除不必要的tags和测试
- 如果在静态类型系统中要突破类型系统的限制,使用变通方法也会影响性能优势
5.5 静态还是动态类型更容易代码重用?
动态类型更容易重用库函数,如果使用cons构造不同的数据,只要使用car,cdr,cadr等就可以访问,不需要为不同的数据类型定义不同的访问器函数。
静态类型的观点:
- 现代类型系统通过generics和subtyping等特性支持代码重用
- 如果使用cons表示所有东西,会被表示的东西弄混,并且很难调试错误。
- 使用单独的静态类型保持思想独立
- 静态类型可以避免库的误用
5.6 静态类型和动态类型哪个更适合原型开发
在软件项目的早期,你会开发一个原型,与此同时,你将改变对软件做什么和用什么方法实现的看法。
动态类型更容易实现原型(prototyping),在早期,你可能不知道需要什么数据类型和函数,因此不用去定义。有一部分程序还没有实现,可以测试已经写好的部分,动态类型可以让不完整的程序运行。 静态类型不允许这样的代码,,因此过早地对数据结构作出承诺,然后编写以后会扔掉的代码用来通过类型检查器 ,是令人沮丧的原型设计。
静态类型的反对观点是,静态类型更容易原型设计,使用类型系统能更好地记录你不断变化的数据结构和代码用例的决策: 新的、不断演化的代码最有可能作出不一致的假设。
5.7 静态类型或动态类型更适合于代码演进吗?
软件工程中的很多工作都花在维护工作上,修复bug、添加新特性,以及对代码进行修改。
动态类型更容易演化改进,修改代码不会影响旧的调用者,比如接受一个int或string代替一个int: Racket调用者不用做任何修改
(define (f x) (* 2 x)) (define (f x) (if (number? x) (* 2 x) (string-append x x)))
ML调用者修改后必须在参数上使用构造器,并对结果使用模式匹配。
fun f x = 2 * x fun f x = case f x of Int i => Int (2 * i) | String s => String (s ^ s)
另一方面,静态类型检查对于捕获演进过程中引入的bug很有用,修改数据或函数的类型,类型检查器会给我们一个所有要修改的地方的"to do" list:
- 避免引入bug
- 类型中的规范越多,类型检查器列出的类型更改时要更改的内容就越多。
- 反方:todo list是强制性的,这导致改进过程很痛苦:不能以部分的方式进行测试。
开发项目中的现实:
- 在实现稳定前经常需要很多原型
- 在1.0版之后会有很多维护/演化
静态类型对比动态类型哪个好不是个好问题。更好的问题是:我们应该静态执行什么?
合理决策:以事实为依据的理性讨论。
Created: 2019-01-09 三 11:07