为什么从reduce的return值解构元组会报错?

Why does deconstructing a tuple from the return value of reduce cause an error?

假设我有一个整数数组,我想得到所有偶数的总和和所有奇数的总和。例如数组[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)
    }
})

它给我错误:

Value of tuple type '(Int, Int)' has no member 'odd'

关于 return 语句。

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

匹配

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

TL;博士

这种行为很不幸,但 'working as expected' 由于以下因素的结合:


Why does deconstructing the tuple cause the generic type to be inferred differently? The deconstruction part should just say what to do to the result. The method call should have been interpreted on its own, and then matched against the pattern (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} 是类型变量。

函数 returns 通用占位符 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)

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

没有任何规则支持一个选项优于另一个选项,这是模棱两可的。幸运的是,约束系统 has a rule to prefer the un-labelled version of a tuple type 绑定类型时。因此,约束系统决定将 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)

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

当你这样做时:

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) 的绑定不是可行吗?好吧,这是由于 multiple statement closures don't participate in type inference。这意味着闭包体独立于对 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)
})

或者如果您更改模式以使用相应的元组标签,,模式的类型现在是 (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)
  }
})

另一种选择,如,是显式注释闭包参数类型:

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)。允许这个例子编译的是编译器为传递的闭包插入一个元组转换,它为其参数重新添加元组标签。