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

Swe*_*per 7 type-inference tuples swift

假设我有一个整数数组,我想得到所有偶数和所有奇数之和的总和.例如,对于数组[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)
    }
})
Run Code Online (Sandbox Code Playgroud)

这本身工作得很好,但是一旦我尝试解构返回的元组:

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)
    }
})
Run Code Online (Sandbox Code Playgroud)

它给了我错误:

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

return声明上.

为什么解构元组会导致泛型类型的推断不同?解构部分应该只说明如何处理结果.方法调用应该自己解释,然后与模式匹配(oddSum, evenSum).

要解决这个问题,我必须将第一个参数更改为(0, 0),这使得闭包中的内容非常难以理解.我必须将奇数总和称为result.0甚至求和result.1.

Ham*_*ish 9

TL; DR

这种行为是不幸的,但由于以下因素的结合,"按预期工作":


为什么解构元组会导致泛型类型的推断不同?解构部分应该只说明如何处理结果.方法调用应该自己解释,然后与模式匹配(evenSum, oddSum).

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

func magic<T>() -> T { 
  fatalError() 
}

let x: Int = magic() // T == Int
Run Code Online (Sandbox Code Playgroud)

类型的图案被用来推断TInt.

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

let (x, y) = magic() // error: Generic parameter 'T' could not be inferred
Run Code Online (Sandbox Code Playgroud)

类型检查器创建两个类型变量来表示元组模式的每个元素.这种类型变量在约束求解器内部使用,并且在约束系统可以被认为是求解之前,每个必须绑定到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)
Run Code Online (Sandbox Code Playgroud)

现在编译,我们可以看到通用占位符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)两者$T0并且$T1可以绑定到该点.Int(x: Int, y: Int)($T0, $T1)

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

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)
Run Code Online (Sandbox Code Playgroud)

T现在受到约束(x: Int, y: Int).为什么?因为模式类型现在只是类型$T0.

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

在这两种情况下,解决方案都是一样的,所以没有歧义.T由于事先没有未标记的元组类型被引入系统,因此不可能绑定到未标记的元组类型.

那么,这如何适用于您的示例?好了,magic只是reduce没有额外的封闭参数:

  public func reduce<Result>(
    _ initialResult: Result,
    _ nextPartialResult: (_ partialResult: Result, Element) throws -> Result
  ) rethrows -> Result
Run Code Online (Sandbox Code Playgroud)

当你这样做时:

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)
    }
})
Run Code Online (Sandbox Code Playgroud)

如果我们暂时忽略闭包,我们可以选择相同的绑定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)
})
Run Code Online (Sandbox Code Playgroud)

或者,如果您更改模式以使用相应的元组标签,正如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)
  }
})
Run Code Online (Sandbox Code Playgroud)

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)
  }
})
Run Code Online (Sandbox Code Playgroud)

但请注意,与前两个示例不同Result,(Int, Int)由于引入首选类型的模式,这会导致绑定($T0, $T1).允许此示例编译的原因是编译器为传递的闭包插入元组转换,这会为其参数重新添加元组标签.