本节书摘来自华章出版社《Hack与HHVM权威指南》一书中的第1章,第1.6.3节,作者 Owen Yamauchi,更多章节内容可以访问“华章计算机”公众号查看。
1.6.3 推理局限在函数内
关于Hack的类型推理的一个最基本限制就是:当分析一个函数的时候,它绝对不会看另外一个函数或方法的代码体。举例来说,假设以下是你的全部代码:
function f($str) {
return 'Here is a string: ' . $str;
}
function main() {
echo f('boo!');
}
main();
对于读者来说,如下两个事实是非常清晰的:$str总是一个字符串类型,并且函数 f() 总是返回一个字符串。然而,Hack的类型检查器并不能推断出上述事实。
当对函数f()进行类型推理的时候,它并不会检查函数f()的调用者以找出调用者实际传递的参数值的类型是什么。在函数main()内部进行类型推理时,它也不会查看函数f()的代码体来找到它实际上返回的是什么类型。它只会检查函数f()的签名用于返回类型标注。然后关于函数的返回类型标注,它什么也没有发现,所以它就认为函数f()的返回类型为“any”(具体参见1.4.2节的内容)。
这种限制的存在是基于性能的原因。对一个函数的推理限制在这个函数的内部,是为了对这个函数的分析所需要的计算量设置一个严格上限。 扩展开来的话,就是对整个代码库的所需计算量设置一个上限。在计算复杂度方面,类型推理算法在复杂度上是超线性(superlinear)的。所以给出多次小的输入而不是一个巨大的输入对于保持运行总时间的可管理性是非常重要的。
对于大型的代码库而言(比如Hack语言原生设计的Facebook),这个属性非常重要。当一个函数体内部变化后(但是它的函数签名并没有变化),类型检查服务端只需要重新分析这个函数,然后保持相关的知识点是及时的即可。并且这个重新分析的过程几乎是实时处理的。当一个函数签名发生了变化后,类型检查器的服务端将会重新分析这个函数和它的所有调用者,并不是它们的调用者,这就对需要的工作量提出了一个比较低的上限。
但是对于此限制也有特例:闭包。虽然一个闭包从定义上来说是个独立的函数,但是对一个内部有闭包的函数进行类型推理的时候,允许对闭包内部进行检查。详细考虑一下如下示例:
并推断出返回
var_dump($doubler(10)); // int(20)
var_dump($doubler(3.14)); // float(6.28)
虽然这个闭包没有任何的返回类型标注(这在严格模式下也是合法的),类型检查器依然能够推断出$doubler(10)是个整型。这是因为它分析了闭包的函数体内部的内容,在$x是个整型的假设下,并且在两个整型上应用加号操作符,仍然会得到一个整型结果注5。同理,它可以推断出$doubler(3.14)是浮点数类型。
顺便提一下,正是因为类型推理能够对闭包内部进行推理,所以即使在严格模式下,也允许闭包没有类型标注。