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

时间:2018-12-01 22:30:05

标签: swift tuples type-inference

比方说,我有一个整数数组,我想获取所有偶数之和和所有奇数之和。例如,对于数组[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)'的值没有成员'odd'

return语句上。

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

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

1 个答案:

答案 0 :(得分:9)

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

类型检查器创建两个 type变量来表示元组模式的每个元素。此类类型变量在约束求解器内部使用,每个变量必须绑定到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)
  • li>

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

如果没有任何规则支持一个选项优于另一个选项,则这是模棱两可的。幸运的是,绑定类型时使用约束系统has a rule to prefer the un-labelled version of a tuple type。因此,约束系统决定将T绑定到($T0, $T1),此时$T0$T1都可以绑定到Int,这是由于{{ 1}}需要转换为(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只是reduce,没有附加的闭包参数:

magic

当您这样做:

  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)
  • li>

并且由于支持无标签形式的元组的规则,约束求解器选择将($T0, $T1)绑定到Result,而绑定到($T0, $T1)。删除元组分解之所以有效,是因为您不再将类型(Int, Int)引入约束系统中–这意味着($T0, $T1)只能绑定到Result

好的,但是让我们再次考虑关闭。我们显然正在访问元组上的成员(odd: Int, even: Int).odd,所以约束系统为什么不能找出.evenResult的绑定可行吗?好吧,这是因为multiple statement closures don't participate in type inference。这意味着闭包主体是独立于对(Int, Int)的调用来解决的,因此当约束系统意识到绑定reduce无效时,为时已晚。

如果将闭包简化为单个语句,则会解除此限制,并且约束系统可以正确地将(Int, Int)折扣为(Int, Int)的有效绑定:

Result

或者,如果您将模式更改为使用相应的元组标签as pointed out by Martin,则模式的类型现在为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)

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

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) } }) ,这导致Result绑定到(Int, Int)。允许该示例进行编译的事实是,编译器为传递的闭包插入了一个元组转换,从而为其参数重新添加了元组标签。