苹果最近更新的swift5.1的时候,在这篇博客介绍了新的诊断系统。其实博客也对苹果的诊断系统原理做了一些简单的分析,理解这个特点,对以后用swift语言做开发,解决一些编译错误,感觉帮助挺大的。
苹果也描述了新的诊断系统的好处,就是不仅仅是报告错误,而是会提供解决问题的方法。在swift5.1之前的诊断系统,出一个错误后就停止诊断,而新的诊断系统会继续诊断后面的错误并提供一些fix建议,可以说是非常智能了。
这么智能的诊断系统,苹果也讲解了他们基本的设计思路,swift语言的诊断系统本质上是基于Hindley-Milner类型推断算法开发的。如果熟悉函数式编程语言的话,就会知道Hindley-Milner类型推断系统是函数式程序设计语言的基石。因为函数式语言大多是表达式结构,在程序编译的时候,计算机来确定表达式中变量类型的合法性,都是靠这个算法保证的。对于这个算法相对来讲彻底理解还是比较复杂的,苹果举了几个简单的实际应用的例子给开发者,也方便开发者之后理解swift这套智能诊断系统。
类型推导算法
通过下面这段代码,我们来分析下swift编译器是如何来做语法诊断的
func foo(_ str: String) {
str + 1
}
要分析上面的代码,对于诊断系统的话,大致分为三步
变量类型的分析
- $Str代表的str变量的类型,这其实是Hindley-Milner算法的定义方式,str是作为+调用第一个参数
- $One代表的是1这个类型,代表的是+调用的第二个参数
- $Result代表着调用+后的结果类型
- $Plus代表是+操作符本身的类型,并且这个符号可以被重载。本质上就是函数的调用。
变量的约束
- $Str <bind to> String 这个表达式是类型推断系统的语法,意思是参数str有一个固定的String类型
- $One <conforms to> ExpressibleByIntegerLiteral 这代表这个类型要遵循ExpressibleByIntegerLiteral协议,比如Int,Double都是遵循这个协议的
- $Plus <bind to> disjunction ((String, String) -> String, (Int, Int) -> Int, …) 这个代表的是一个表达式,满足里面这些参数条件,并且可以被重载
- ($Str, $One) -> $Result <applicable to> $Plus 这时候$Result这个类型就是未知的。他将需要上面的表达式,带上二个参数的类型来决定的。
在swift语法解析后,会生成这样的一个树形结构,我们把类型推导的变量约束放到语法树中,就得到如下的图:
推断算法执行的过程
有了上面对变量和表达式的类型定义和约束之后,下面我们来看下swift语言类型推导的过程
1.首先绑定$Plus这个表达式的第一个表述(String, String) -> String
2.第二步我们就拿($Str, $One) -> $Result上面解析出来的表达式,应用到$Plus这个表达式的约束,这个过程如下:
- 首先拿表达式第0个参数String匹配$Str的类型。
- 再拿第一个参数String匹配$One的类型
- 最后就是String类型匹配$Result的类型
3.然后就针对第二步,对类型匹配做判断:
- $Str匹配了String类型
- $Result类型如果匹配上面的表达式,应该也要分配为String类型
4.现在就剩下$One类型的判断
- 从表达式匹配来看$One需要是String类型
- 而前面一步已经确定$One需要 <conforms to> 符合ExpressibleByIntegerLiteral这个协议
5.符合ExpressibleByIntegerLiteral有Int和Double类型,他们都不符合String类型。所以$One的类型约束就出现了歧义
6.尝试$Plus其他表达式(例如 (Int, Int) -> Int)循环上面的1-5步骤
在遍历完所有的情况后,发现都不能满足约束,这时候编译器的推断系统就会报错,通过上面的分析,我们清楚了如何识别错误位置,但是如何帮助诊断系统,判断出来这种情况下如何修改为正确的内容,下面就是苹果给出的一个完整的解决方案。
错误提示解决方案
swift新的诊断系统会根据上面判断出来的错误位置,给出解决的方法。这个是先前的诊断系统和现在的主要区别,以前的只会告诉你那里错误了,而现在会告诉你怎么更改。例如上面的这个例子,推断系统判断,如果使用$Plus表达式,错误的地方是$One代表的参数类型不能适配ExpressibleByIntegerLiteral协议,我们基于这个错误,可以给出下面两个提示。
error: binary operator '+' cannot be applied to arguments 'String' and 'Int'
还有一个错误提示如下:
error: argument type 'String' does not conform to 'ExpressibleByIntegerLiteral'
我们会根据一些规则来选择这两个提示方案中的一个,新的推断系统往往会选择第二个错误提示方案,下面就通过另一个例子再来深入剖析下,具体为啥选择第二个提示。
深入剖析
当一个错误约束被侦测出来的时候,我们通过捕获下面的信息来fix这个错误:
- 哪种类型的错误
- 错误在源码中的位置
- 错误的类型和声明
诊断系统会综合判断上面所有的错误,直到能够给出所有的解决方法,然后就会停止诊断,下面看个例子
func foo(_: inout Int) {}
var x: Int = 0
foo(x)
通过上面的分析我们知道错误如何定位了,下面我就简单的列出来步骤
变量类型分析
$X := Int
$Foo := (inout Int) -> Void
$Result
变量的约束
先看下3个变量类型约束的表达式
($X) -> $Result <applicable to> $Foo
($X) -> $Result to (inout Int) -> Void这个表达式的约束,可以让我们推断出下面的约束
Int <convertible to> inout Int
$Result <equal to> Void
这时候就很容易发现Int不能够适配inout Int这个类型,因此约束推断系统就会记录这个错误位置,变量缺少一个&地址符号。然后根据这个错误,类型诊断系统就会提示需要插入一个&解决这个错误:
error: passing value of type 'Int' to an inout parameter requires explicit '&'
foo(x)
^
&
下面这个例子是说明swift的诊断系统不会判断出来一个错误后就停止诊断了,而是会继续寻找直到把所有的错误都解决掉,例如:
func foo(_: inout Int, bar: String) {}
var x: Int = 0
foo(x, "bar")
上面的错误提示就会有下面这些
error: passing value of type 'Int' to an inout parameter requires explicit '&'
foo(x)
^
&
error: missing argument label 'bar:' in call
foo(x, "bar")
^
bar:
错误提示改进的例子
苹果针对新的诊断系统,给了一些例子,来说明新系统的优点
func foo(answer: Int) -> String { return "a" }
func foo(answer: String) -> String { return "b" }
let _: [String] = [42].map { foo($0) }
先前的系统提示是这样的
error: argument labels '(_:)' do not match any available overloads`
现在的解决方案提示
error: missing argument label 'answer:' in call
let _: [String] = [42].map { foo($0) }
^
answer:
如果你想看更多的例子的话可以去swift的blog
总结
总之苹果新的诊断系统,能够提供出解决方案,可以更方便开发者快速解决错误。这看起来是相当诱人的,不过从之前苹果对编译器智能提示的优化的体验来看,但愿新的诊断系统,别让我的mac电脑的风扇转动的更厉害,然后所有的提示都罢工的情况再出现了😹。