TL;DR
Такое поведение вызывает сожаление, но «работает как положено» из-за комбинации:
Почемудеконструкция кортежа приводит к тому, что универсальный тип выводится по-разному?Часть деконструкции должна просто сказать, что делать с результатом.Вызов метода должен быть интерпретирован сам по себе, а затем сопоставлен с шаблоном (evenSum, oddSum)
.
Средство проверки типов делает двунаправленный вывод типа, что означает, что используемый шаблон может влиять на то, как назначенное выражениеТип проверен.Например, рассмотрим:
func magic<T>() -> T {
fatalError()
}
let x: Int = magic() // T == Int
Тип шаблона используется для вывода, что T
равен Int
.
Так что же происходит с шаблоном деконструкции кортежа?
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)
, и в этот момент и $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)
не является жизнеспособной?Это связано с тем, что закрытие нескольких операторов не участвует в выводе типа .Это означает, что тело замыкания решается независимо от вызова 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)
}
})
Обратите внимание, однако, что в отличие от двух предыдущих примеров, это приводит к тому, что Result
привязывается к (Int, Int)
из-за шаблона, вводящего предпочтительный тип ($T0, $T1)
.Что позволяет этому примеру компилироваться, так это тот факт, что компилятор вставляет преобразование кортежа для переданного замыкания, которое повторно добавляет метки кортежа для его параметра.