Static VS Dynamic 静态动态对比
一门语言称之为静态或是动态,指的是它在什么时候进行类型检查。
ML VS Racket
可以有两种看法:
- ML 某种程度上是 Racket 的子集,Racket 支持的写法更多,因为一部分 Racket 中允许的表达式不会被 ML 的静态检查通过。比如 if 语句的两个分支返回不同的类型。
- Racket 里的变量其实不是“没有类型”,而是只有“一个类型”。
datatype theType = (* 所有的变量类型都一样,所以没有必要标注或是检查 *)
Int of int | (* 变量所持有的值是有不同类型的,即运行时值是带有所谓标签的 *)
String of string |
Pair of theType * theType |
Fun of theType -> theType |
...
Static Checking 静态检查
静态检查处在语法分析之后,程序运行之前,所以称之为“编译时检查”(但这跟语言用编译器还是解释器没有关系)。静态检查如何作用是语言定义的一部分,有的语言做得多有的语言做得少,给定检查规则,也可以自己实现工具来做期望的检查。
实现静态检查的方式主要是类型系统,但类型系统只是实现静态检查的方式,而不是静态检查的目的。静态检查的目的是防止错误,但这个错误的范围很有限,比如大多数静态检查都不会检查数组是否越界,静态检查也没法告诉你某个地方你应该用乘法但是你用了加法。它能防止的是,字符串不能与整型相除,某个类型不能做某些事情。
相比于动态检查(即给值加上标签,运行时进行检查),静态检查其实拒绝了一部分并不会出错的程序。这里值得讨论的东西在于,首先,你如何定义错误。
考虑一个简单的“错误”: 3 / 0
,你可以在下面这些时间点阻止这个“错误”
- Keystroke-time: 在编辑这段代码的时候阻止,意味着你写下
3 / 0
就已经是一个错误 - Compile-time: 当静态检查器“看到”这段代码的时候阻止,意味着这段代码不应该出现
- Link-time: 当发现这段代码会被调用的时候阻止,意味着调用这段代码是一个错误
- Run-time: 当执行这段代码的时候阻止,即被
0
除这个运算是一种错误 - Even-later
- 执行这段代码没有错,但如果用到了这个运算返回的结果(可以是一个表示未定义的标识,比如
undefined
),才是一种错误(比如把undefined
用作数组下标) - 甚至用返回的结果来做计算也没错,
3 / 0
应该表示正无穷,同样的tan(π/2)
也表示正无穷,你可以让它参与计算(事实上, Racket 里(/ 3.0 0.0)
就返回+inf.0
)
- 执行这段代码没有错,但如果用到了这个运算返回的结果(可以是一个表示未定义的标识,比如
第二个值得讨论的就是如何定义正确,怎么判断类型检查是符合语言定义的。
Soundness And Completeness 可靠与完备
设 X
为我们希望类型检查阻止的一件事,定义一个类型系统是
- 可靠的(sound):不接受存在某个输入,使得
X
会发生的程序 - 完备的(complete):不拒绝对任意输入都不会使得
X
发生的程序
现代语言的类型系统都是可靠的,但不是完备的。可靠意味着使用这门语言的人,可以依赖于 X
不会发生这个事实来编程。完备当然很好,但是实际上被类型系统误诊的情况很少,并且也很容易修改以通过类型检查。
但是为什么现代语言都不是完备的?实际上,静态检查不能够同时满足下面三件事:
- 程序会终止 (Always terminate)
- 可靠 (Soundness)
- 完备 (Completeness)
如果一定要抛弃一个,抛弃完备性是一个较好的选择。
(为什么不能同时满足?是因为不可判定性,我解释不了)
Weak Typing 弱类型
假设对于某个 X
而言,类型系统是不可靠的,那么至少要在运行时阻止这个行为。但另一种选择是,不阻止这样的行为,这是程序员的错误,语言不是必须去做检查的。这种情况下,程序可能发生各种不可预料的行为,存在这种情况的语言称为弱类型的。典型的弱类型语言就是 C 和 C++,一个典型的 X
行为就是数组越界。强类型语言比如 Java 会在运行时抛出异常,但弱类型语言不会阻止这个行为,从而发生无法预料的事。
为什么 C/C++ 会如此设计?
- 接近底层,效率优先。检查错误需要额外的时间和空间,程序员也不希望有隐藏的时间空间开销
- “strong types for weak minds”, 即认为人比计算机聪明,我这么写肯定是对的,不用你来告诉我错了,事实证明这种想法是错误的,计算机比人要可靠得多
Flexible 灵活性
除了检查错误的时间点不同,不同的语言对相同的行为也有不同的对待,也就是有严格和灵活之分。一些语言认为是错误的,可能另一些语言不这么认为,相反赋予了一些看上去是错误的行为正确的意义。(It’s not a bug, but a feature!)
- 数组越界:一些语言没有下标越界一说,用了越界的下标会使那个数组变大。还有些语言有从尾部开始的负下标。
- 函数传参:一些语言无所谓参数的个数,传的多了会忽略,传的少了会用默认值代入
- 隐式转换:一些语言字符串可以和数相加
(上述举例我真的没有在黑 JavaScript)
这些特性一方面可能是不明智的,容易隐藏潜在的错误,让程序难以正确调试,但另一方面不可否认的,这种灵活有时候会很有用。这跟语言是动态还是静态也没什么关系,可能容易认为这种灵活让语言更加“动态”,但这只是改变了语义,不再阻止某些行为而已。
Static or Dynamic 静态还是动态
回到正题,究竟该选择静态语言还是动态语言,各有什么优点和缺点?(永远没有答案可以说,静态一定比动态好,或是反过来)
- 方便?动态没有类型的约束表达自然更加自由,但是静态有了确定类型的前提也可以少写很多类似判断类型的代码
- 静态语言是否阻止了一些有用的程序?事实上,这些因为类型系统被阻止的程序,因为动态语言因为给值加上标签所以可以在运行时检查,但静态语言依然可以用类似 ML 的 datatype 或是面向对象语言中的多态来显式模拟这种标签。
- “过早”地检查错误?软件工程中有一个真理,bug 发现越早越容易修复。静态检查能帮你发现一些低级错误,使得程序员能专注于业务逻辑。但是动态语言的支持者同样可以说,仍然有一些错误是静态检查无法检测,需要通过测试来发现的,既然最终都要测试,那有没有静态检查也不重要。
- 性能好坏?动态语言有标签的开销,但可以做优化,而且静态语言也有需要标签的时候。
- 代码复用?显然动态语言更容易复用代码,但也因此更容易出错。
- 产品原型设计?动态语言不用设计类型,同时可以先设计一部分,而静态语言必须通过类型检查,但也一样可以通过通配符之类的 case 来表达剩余的未设计部分。
- 维护代码?动态语言方便给函数添加新的参数来扩展,并且可以不让用户感知到这一变化。对于静态语言,只要改动了一个地方,类型检查工具会告诉你所有相关联的应该修改的地方,也很方便。
OOP VS Functional 面向对象与函数式对比
Multiple Inheritance, Mixin, Interface 多重继承,混入与接口
TO BE DONE…