为什么从reduce的返回值解构元组会导致错误



假设我有一个整数数组,我想得到所有偶数的和和和所有奇数的和。例如,对于阵列[1,2,3],所有奇数的和为4,所有偶数的和为2。

我就是这样做的:

array.reduce((odd: 0, even: 0), { (result, int) in
if int % 2 == 0 {
return (result.odd, result.even + int)
} else {
return (result.odd + int, result.even)
}
})

这本身就很好,但当我试图解构返回的元组时:

let (oddSum, evenSum) = a.reduce((odd: 0, even: 0), { (result, int) in
if int % 2 == 0 {
return (result.odd, result.even + int)
} else {
return (result.odd + int, result.even)
}
})

它给了我一个错误:

元组类型"(Int,Int)"的值没有成员"奇数">

return语句上。

为什么解构元组会导致对泛型类型的推断不同?解构主义部分应该只是说对结果做什么。方法调用应该自己进行解释,然后根据模式(oddSum, evenSum)进行匹配。

为了解决这个问题,我必须将第一个参数更改为(0, 0),这使得闭包中的内容非常不可读。我不得不把奇数和称为result.0,把偶数和称为result.1

TL;DR

这种行为是不幸的,但由于以下因素的组合,"如预期那样工作":

  • 在类型变量绑定方面,约束系统倾向于未标记的元组类型而不是标记的元组
  • 不参与类型推理的多语句闭包

为什么解构元组会导致以不同的方式推断泛型类型?解构主义部分应该只是说对结果做什么。方法调用应该自己进行解释,然后根据模式(evenSum, oddSum)进行匹配。

类型检查器进行双向类型推断,这意味着使用的模式可以影响指定表达式的类型检查方式。例如,考虑:

func magic<T>() -> T { 
fatalError() 
}
let x: Int = magic() // T == Int

图案的类型用于推断TInt

那么元组解构模式会发生什么呢?

let (x, y) = magic() // error: Generic parameter 'T' could not be inferred

类型检查器创建两个类型变量来表示元组模式的每个元素。此类类型变量在约束解算器内部使用,并且在考虑求解约束系统之前,每个类型变量都必须绑定到Swift类型。在约束系统内,模式let (x, y)具有类型($T0, $T1),其中$T{N}是类型变量。

函数返回通用占位符T,因此约束系统推断T可转换为($T0, $T1)。然而,没有关于$T0$T1可以绑定到什么的进一步信息,因此系统失败。

好吧,让我们给系统一种方法,通过向函数添加一个参数,将类型绑定到那些类型变量。

func magic<T>(_ x: T) -> T {
print(T.self)
fatalError()
}
let labelledTuple: (x: Int, y: Int) = (x: 0, y: 0)
let (x, y) = magic(labelledTuple) // T == (Int, Int)

现在进行编译,我们可以看到通用占位符T被推断为(Int, Int)。这是怎么发生的?

  • magic属于(T) -> T
  • 参数的类型为(x: Int, y: Int)
  • 结果模式的类型为($T0, $T1)

这里我们可以看到约束系统有两个选项,它可以是:

  • T绑定到未标记的元组类型($T0, $T1),强制类型(x: Int, y: Int)的参数执行元组转换以剥离其标签
  • T绑定到带标签的元组类型(x: Int, y: Int),强制返回的值执行元组转换,去掉其标签,以便将其转换为($T0, $T1)

(这掩盖了通用占位符被打开到新类型变量中的事实,但这是不必要的细节)

如果没有任何规则来支持一种选择而不是另一种选择,这是模棱两可的。然而,幸运的是,约束系统有一条规则,在绑定类型时更喜欢元组类型的未标记版本。因此,约束系统决定将T绑定到($T0, $T1),在这一点上,由于(x: Int, y: Int)需要转换为($T0, $T1)$T0$T1都可以绑定到Int

让我们看看当我们移除元组解构模式时会发生什么:

func magic<T>(_ x: T) -> T {
print(T.self)
fatalError()
}
let labelledTuple: (x: Int, y: Int) = (x: 0, y: 0)
let tuple = magic(labelledTuple) // T == (x: Int, y: Int)

CCD_ 37现在绑定到CCD_ 38。为什么?因为模式类型现在只是类型$T0

  • 如果T绑定到$T0,则$T0将绑定到参数类型(x: Int, y: Int)
  • 如果T绑定到(x: Int, y: Int),那么$T0也将绑定到(x: Int, y: Int)

在这两种情况下,解决方案是相同的,因此没有歧义。T不可能绑定到未标记的元组类型,这仅仅是因为系统中首先没有引入未标记的tuple类型。

那么,这是如何应用于您的示例的呢?好吧,magic只是reduce,没有额外的闭包参数:

public func reduce<Result>(
_ initialResult: Result,
_ nextPartialResult: (_ partialResult: Result, Element) throws -> Result
) rethrows -> Result

当你这样做:

let (oddSum, evenSum) = a.reduce((odd: 0, even: 0), { (result, int) in
if int % 2 == 0 {
return (result.odd, result.even + int)
} else {
return (result.odd + int, result.even)
}
})

如果我们现在忽略闭包,我们可以为Result:选择相同的绑定

  • Result绑定到未标记的元组类型($T0, $T1),强制类型(odd: Int, even: Int)的参数执行元组转换,以剥离其标签
  • Result绑定到带标签的元组类型(odd: Int, even: Int),强制返回的值执行元组转换,去掉其标签,以便将其转换为($T0, $T1)

由于有利于元组的未标记形式的规则,约束求解器选择将Result绑定到($T0, $T1),并将其解析为(Int, Int)。删除元组分解是有效的,因为您不再将类型($T0, $T1)引入约束系统中——这意味着Result只能绑定到(odd: Int, even: Int)

好吧,但让我们再次考虑结束。我们显然正在访问元组上的成员.odd.even,那么为什么约束系统不能确定Result(Int, Int)的绑定是不可行的呢?这是因为多个语句闭包不参与类型推断。这意味着闭包主体是独立于对reduce的调用求解的,所以当约束系统意识到绑定(Int, Int)无效时,已经太晚了。

如果您将闭包缩减为单个语句,则此限制将被取消,并且约束系统可以正确地将(Int, Int)作为Result:的有效绑定进行贴现

let (oddSum, evenSum) = a.reduce((odd: 0, even: 0), { (result, int)  in
return int % 2 == 0 ? (result.odd, result.even + int)
: (result.odd + int, result.even)
})

或者,如Martin所指出的,如果您更改模式以使用相应的元组标签,则模式的类型现在为(odd: $T0, even: $T1),这避免了在约束系统中引入未标记的形式:

let (odd: oddSum, even: evenSum) = a.reduce((odd: 0, even: 0), { (result, int) in
if int % 2 == 0 {
return (result.odd, result.even + int)
} else {
return (result.odd + int, result.even)
}
})

正如Alladinian所指出的,另一种选择是显式注释闭包参数类型:

let (oddSum, evenSum) = a.reduce((odd: 0, even: 0), { (result: (odd: Int, even: Int), int) in
if int % 2 == 0 {
return (result.odd, result.even + int)
} else {
return (result.odd + int, result.even)
}
})

然而,注意,与前两个示例不同,由于引入优选类型($T0, $T1)的模式,这导致Result绑定到(Int, Int)。允许此示例进行编译的是,编译器为传递的闭包插入元组转换,从而为其参数重新添加元组标签。

最新更新