
Что такое типовой вывод? Что это такое и как это работает
7 июля 2025 г.Это блог версия моего выступления по типовым выводу на Gophercon 2023 в Сан -Диего, слегка расширенная и отредактированная для ясности.
Что такое типовой вывод?
Википедия определяет тип вывода следующим образом:
Вывод типа - это возможность автоматического вывода, частично или полностью, тип выражения во время компиляции. Компилятор часто способен вывести тип переменной или типа подписи функции, без указанных аннотаций с явными типами.
Ключевая фраза здесь - «автоматически вывести… тип выражения». С самого начала поддерживает основную форму вывода типа:
const x = expr // the type of x is the type of expr
var x = expr
x := expr
В этих объявлениях не указано явных типов, и, следовательно, типы постоянных и переменныхx
слева от=
и:=
являются типами соответствующих выражений инициализации справа. Мы говорим, что типывыводитсяиз (типов) их выражений инициализации. С внедрением дженериков в GO 1.18, способности к выводу типа GO были значительно расширены.
Почему вывод типа?
В негенерическом коде GO эффект от ухода из-за того, что типы отдали наиболее выражены в короткой переменной объявлении. Такое объявление сочетает в себе вывод типа и немного синтаксического сахара - способность уйти отvar
Ключевое слово - один очень компактный оператор. Рассмотрим следующее объявление переменной карты:
var m map[string]int = map[string]int{}
против
m := map[string]int{}
Пропуская тип слева от:=
Удаляет повторение и в то же время увеличивает читабельность.
Общий код GO может значительно увеличить количество типов, появляющихся в коде: без вывода типа, каждая универсальная функция и экземпляр типа требуют аргументов типа. Возможность их опустить становится еще более важной.
Рассмотрите возможность использования следующих двух функций из новогосрезы пакет:
package slices
func BinarySearch[S ~[]E, E cmp.Ordered](x S, target E) (int, bool)
func Sort[S ~[]E, E cmp.Ordered](x S)
Без вывода типа, вызовыBinarySearch
иSort
Требуется явные аргументы типа:
type List []int
var list List
slices.Sort[List, int](list)
index, found := slices.BinarySearch[List, int](list, 42)
Мы бы не повторили[List, int]
с каждым таким общим вызовом функции. С выводом типа код упрощает:
type List []int
var list List
slices.Sort(list)
index, found := slices.BinarySearch(list, 42)
Это и чище, и компактно. На самом деле он выглядит точно так же, как негенерический код, и вывод типа делает это возможным.
Важно отметить, что вывод типа является дополнительным механизмом: если аргументы типа делают код более ясным, во что бы то ни стало, записать их.
Вывод типа - это форма сопоставления схем типа
Выводы сравнивают шаблоны типа, где шаблон типа - это тип, содержащий параметры типа. По причинам, которые станут очевидными в некоторых случаях, параметры типа иногда также вызываютсяТип переменныхПолем Сопоставление шаблонов типа позволяет нам вывести типы, которые должны перейти в эти переменные типа. Давайте рассмотрим короткий пример:
// From the slices package
// func Sort[S ~[]E, E cmp.Ordered](x S)
type List []int
var list List
slices.Sort(list)
АSort
функциональный вызов проходитlist
переменная как аргумент функции для параметраx
изslices.Sort
Полем Поэтому типlist
, что естьList
, должен соответствовать типуx
, который является параметром типаS
Полем ЕслиS
имеет типList
, это задание становится действительным. На самом делеПравила для заданийсложны, но сейчас достаточно хорошо, чтобы предположить, что типы должны быть идентичными.
Как только мы вышли из типа дляS
, мы можем посмотреть натип ограничениядляS
Полем В нем говорится:~
Символ - чтоОсновной типизS
Должен быть ломтик[]E
Полем Основной типS
является[]int
, поэтому[]int
должен соответствовать[]E
, и с этим мы можем сделать вывод, чтоE
должно бытьint
Полем Мы смогли найти типы дляS
иE
Такие, что соответствующие типы совпадают. Вывод добился успеха!
Вот более сложный сценарий, в котором у нас есть много параметров типа:S1
ВS2
ВE1
, иE2
отslices.EqualFunc
, иE1
иE2
от общей функцииequal
Полем Локальная функцияfoo
вызовыslices.EqualFunc
сequal
функционируйте как аргумент:
// From the slices package
// func EqualFunc[S1 ~[]E1, S2 ~[]E2, E1, E2 any](s1 S1, s2 S2, eq func(E1, E2) bool) bool
// Local code
func equal[E1, E2 comparable](E1, E2) bool { … }
func foo(list1 []int, list2 []float64) {
…
if slices.EqualFunc(list1, list2, equal) {
…
}
…
}
Это пример, в котором вывод типа действительно сияет, так как мы потенциально можем отказаться от шести типовых аргументов, по одному для каждого из параметров типа. Подход сопоставления типовых схем все еще работает, но мы можем видеть, как он может быстро усложняться, потому что количество отношений типа пролиферирует. Нам нужен систематический подход, чтобы определить, какие параметры типа, а какие типы участвуют с какими шаблонами.
Это помогает взглянуть на тип вывода немного по -другому.
Тип уравнений
Мы можем переосмыслить вывод типа в качестве проблемы решения уравнений типа. Решение уравнений - это то, с чем мы все знакомы из алгебры средней школы. К счастью, решение уравнений типа является более простой проблемой, как мы увидим в ближайшее время.
Давайте еще раз посмотрим на наш предыдущий пример:
// From the slices package
// func Sort[S ~[]E, E cmp.Ordered](x S)
type List []int
var list List
slices.Sort(list)
Вывод достигает успеха, если приведенные ниже уравнения могут быть решены. Здесь≡
означаетидентичен, иunder(S)
представляетОсновной типизS
:
S ≡ List // find S such that S ≡ List is true
under(S) ≡ []E // find E such that under(S) ≡ []E is true
Параметры типа - этопеременныев уравнениях. Решение уравнений означает поиск значений (аргументы типа) для этих переменных (параметры типа), так что уравнения становятся истинными. Эта точка зрения делает проблему вывода типа более подлежащей дальности, потому что она дает нам формальную структуру, которая позволяет нам записать информацию, которая впадает в вывод.
Точное с типовыми отношениями
До сих пор мы просто говорили о типах, которые должны бытьидентичныйПолем Но для фактического кода GO, это слишком сильное требование. В предыдущем примере,S
не нужно быть идентичнымList
, скорееList
должно бытьназначенкS
Полем Сходным образом,S
долженудовлетворятьего соответствующий тип ограничения. Мы можем сформулировать наши уравнения типа более точно, используя конкретных операторов, которые мы пишем как:≡
и∈
:
S :≡ List // List is assignable to S
S ∈ ~[]E // S satisfies constraint ~[]E
E ∈ cmp.Ordered // E satisfies constraint cmp.Ordered
Как правило, мы можем сказать, что уравнения типа бывают из трех форм: два типа должны быть идентичными, один тип должен быть назначен другим типом, или один тип должен удовлетворить ограничение типа:
X ≡ Y // X and Y must be identical
X :≡ Y // Y is assignable to X
X ∈ Y // X satisfies constraint Y
(Примечание: в разговоре Gophercon мы использовали символы≡
А для:≡
и≡
C для∈
Полем Мы верим:≡
более четко вызывает отношение присвоения; и∈
непосредственно выражает, что тип, представленный параметром типа, должен быть элементом его ограниченияТип набор.)
Источники типовых уравнений
В общем вызове функции у нас могут быть явные аргументы типа, хотя большую часть времени мы надеемся, что их можно сделать. Как правило, у нас также есть обычные аргументы функции. Каждый явный аргумент типа вносит уравнение (тривиальное) типа: параметр типа должен быть идентичен аргументу типа, потому что код говорит так. Каждый аргумент обычной функции вносит другое уравнение типа: аргумент функции должен быть назначен соответствующим параметрам функции. И, наконец, каждое ограничение типа также обеспечивает уравнение типа, ограничивая, какие типы удовлетворяют ограничению.
В целом, это производитn
Тип параметров иm
тип уравнений. В отличие от основной алгебры средней школы,n
иm
Не нужно быть тем же, чтобы уравнения типа были разрешаются. Например, отдельное уравнение ниже позволяет нам вывести аргументы типа для двух параметров типа:
map[K]V ≡ map[int]string // K ➞ int, V ➞ string (n = 2, m = 1)
Давайте рассмотрим каждый из этих источников типовых уравнений в свою очередь:
1. Тип уравнений из аргументов типа
Для каждого объявления параметров типа
func f[…, P constraint, …]…
и явно предоставил аргумент типа
f[…, A, …]…
Мы получаем уравнение типа
P ≡ A
Мы можем тривиально решить это дляP
: P
должно бытьA
И мы пишемP ➞ A
Полем Другими словами, здесь нечего делать. Мы все еще могли бы записать соответствующее уравнение типа для полноты, но в этом случае компилятор GO просто заменяет аргументы типа для их параметров типа, а затем исчезают эти параметры, и мы можем забыть о них.
2. Тип уравнений из назначений
Для каждого аргумента функцииx
Передается в параметр функцииp
f(…, x, …)
гдеp
илиx
содержать параметры типа, типx
Должен быть назначен типом параметраp
Полем Мы можем выразить это с уравнением
𝑻(p) :≡ 𝑻(x)
где𝑻(x)
означает «типx
”. Если ни одинp
ниx
Содержит параметры типа, не существует переменной типа для решения для решения для: Уравнение либо true, потому что присвоение действительным код GO, либо FALSE, если код недействителен. По этой причине вывод типа рассматривает только типы, которые содержат типовые параметры вовлеченной функции (или функций).
Начиная с GO 1.21, удаленная или частично созданная функция (но не функциональная вызов) также может быть назначена переменной типа функции, как в:
// From the slices package
// func Sort[S ~[]E, E cmp.Ordered](x S)
var intSort func([]int) = slices.Sort
Аналогично прохождению параметров, такие назначения приводят к соответствующему уравнению типа. Для этого примера это было бы
𝑻(intSort) :≡ 𝑻(slices.Sort)
или упрощен
func([]int) :≡ func(S)
вместе с уравнениями для ограничений дляS
иE
отslices.Sort
(см. ниже).
3. Тип уравнений из ограничений
Наконец, для каждого параметра типаP
Для которого мы хотим вывести аргумент типа, мы можем извлечь уравнение типа из его ограничения, потому что параметр типа должен удовлетворить ограничение. Учитывая декларацию
func f[…, P constraint, …]…
Мы можем записать уравнение
P ∈ constraint
Здесь∈
означает «должен удовлетворить ограничение», которое (почти) то же самое, что является элементом типа типа ограничения. Позже увидим, что некоторые ограничения (такие какany
) не полезны или в настоящее время не могут использоваться из -за ограничений реализации. Вывод просто игнорирует соответствующие уравнения в этих случаях.
Параметры и уравнения типа могут быть из нескольких функций
В GO 1.18 параметры предполагаемого типа должны были быть из той же функции. В частности, было невозможно пройти общую, удаленную или частично создательную функцию в качестве аргумента функции или назначить ее (функциональной) переменной.
Как упоминалось ранее, в этих случаях также работает вывод типа 1.21. Например, общая функция
func myEq[P comparable](x, y P) bool { return x == y }
может быть назначен переменной типа функции
var strEq func(x, y string) bool = myEq // same as using myEq[string]
безmyEq
полностью экземпляры, а вывод типа сделает вывод, что аргумент типа дляP
должно бытьstring
Полем
Кроме того, общая функция может быть использована, удаленная или частично созданная в качестве аргумента для другой, возможно, общая функция:
// From the slices package
// func CompactFunc[S ~[]E, E any](s S, eq func(E, E) bool) S
type List []int
var list List
result := slices.CompactFunc(list, myEq) // same as using slices.CompactFunc[List, int](list, myEq[int])
В этом последнем примере вывод типа определяет аргументы типа дляCompactFunc
иmyEq
Полем В более общем смысле, параметры типа из произвольно многих функций могут быть выведены. При участии в нескольких функциях уравнения типа также могут быть или включать несколько функций. ВCompactFunc
Пример мы получим три параметра типа и пять уравнений типа:
Type parameters and constraints:
S ~[]E
E any
P comparable
Explicit type arguments:
none
Type equations:
S :≡ List
func(E, E) bool :≡ func(P, P) bool
S ∈ ~[]E
E ∈ any
P ∈ comparable
Solution:
S ➞ List
E ➞ int
P ➞ int
Связанные с параметрами свободного типа
На этом этапе у нас есть более четкое понимание различных источников уравнений типа, но мы не были очень точными относительно того, какие параметры типа для решения уравнений. Давайте рассмотрим еще один пример. В коде ниже, функция корпусаsortedPrint
вызовыslices.Sort
для сортировки.sortedPrint
иslices.Sort
являются общими функциями как оба параметра типа объявления.
// From the slices package
// func Sort[S ~[]E, E cmp.Ordered](x S)
// sortedPrint prints the elements of the provided list in sorted order.
func sortedPrint[F any](list []F) {
slices.Sort(list) // 𝑻(list) is []F
… // print list
}
Мы хотим сделать вывод аргумента дляslices.Sort
вызов. Прохождениеlist
к параметруx
изslices.Sort
порождает уравнение
𝑻(x) :≡ 𝑻(list)
что такое же, как
S :≡ []F
В этом уравнении у нас есть два параметра типа,S
иF
Полем Для какого из них нам нужно решить уравнение типа? Потому что вызываемая функцияSort
, мы заботимся о его параметре типаS
, не параметр типаF
Полем Мы говорим этоS
являетсяграницакSort
Потому что это объявленоSort
ПолемS
это соответствующая переменная типа в этом уравнении. В отличие от этого,F
связан (объявлено)sortedPrint
Полем Мы говорим этоF
являетсябесплатноЧто касаетсяSort
Полем У него есть свой, уже данный тип. Этот тип естьF
, что бы это ни было (определено во время экземпляра). В этом уравнении,F
уже дается, этотип постояннойПолем
При решении уравнений типа мы всегда решаем для параметров типа, связанных с функцией, которую мы вызываем (или назначаем в случае общего назначения функции).
Решение уравнений типа
Отсутствующая часть, теперь, когда мы установили, как собирать соответствующие параметры типа и уравнения типа, это, конечно, алгоритм, который позволяет нам решать уравнения. После различных примеров, вероятно, стало очевидно, что решениеX ≡ Y
Просто означает сравнение типовX
иY
рекурсивно друг против друга и в процессе, определяющий подходящие аргументы типа для параметров типа, которые могут возникнуть вX
иY
Полем Цель состоит в том, чтобы сделать типыX
иY
идентичныйПолем Этот процесс сопоставления называетсяобъединениеПолем
Правила дляТип идентификацияРасскажите нам, как сравнить типы. СграницаПараметры типа играют роль переменных типа, нам нужно указать, как они сопоставлены с другими типами. Правила следующие:
- Если тип параметра
P
имеет предполагаемый тип,P
означает этот тип. - Если тип параметра
P
нет предполагаемого типа и соответствует другому типуT
ВP
устанавливается на этот тип:P ➞ T
Полем Мы говорим, что типT
был выведен дляP
Полем - Если
P
совпадает с другим параметром типаQ
и ни одинP
ниQ
есть предполагаемый тип еще,P
иQ
являютсяобъединенныйПолем
Объединение двух параметров типа означает, что они соединяются вместе так, чтобы в будущем они обозначали одно и то же значение параметра типа: если один изP
илиQ
соответствует типуT
, обаP
иQ
установлены наT
одновременно (в целом любое количество параметров типа может быть объединенным таким образом).
Наконец, если два типаX
иY
отличаются, уравнение не может быть сделано правдой и решением оно не удается.
Объединение типов для идентичности типа
Несколько конкретных примеров должны прояснить этот алгоритм. Рассмотрим два типаX
иY
Содержит три параметра связанных типовA
ВB
, иC
, все появляются в типовом уравненииX ≡ Y
Полем Цель состоит в том, чтобы решить это уравнение для параметров типа; то есть найти подходящие аргументы для них такими, чтоX
иY
Станьте идентичным, и, таким образом, уравнение становится истинным.
X: map[A]struct{i int; s []B}
Y: map[string]struct{i C; s []byte}
Объединение продолжается, сравнивая структуруX
иY
рекурсивно, начиная с вершины. Просто просмотр структуры двух типов, которые у нас есть
map[…]… ≡ map[…]…
с…
представляя соответствующие ключа карты и типы значений, которые мы игнорируем на этом шаге. Поскольку у нас есть карта с обеих сторон, типы до сих пор идентичны. Объединение продолжается рекурсивно, сначала с ключевыми типами, которыеA
дляX
карта иstring
дляY
карта Соответствующие типы ключеA
должно бытьstring
:
A ≡ string => A ➞ string
Продолжая с типами элементов карты, мы прибываем на
struct{i int; s []B} ≡ struct{i C; s []byte}
Обе стороны являются структурой, поэтому объединение продолжается с полями структуры. Они идентичны, если они в том же порядке, с одинаковыми именами и идентичными типами. Первая пара поляi int
иi C
Полем Имена совпадают и потому чтоint
должен объединиться сC
, таким образом
int ≡ C => C ➞ int
Это совпадение рекурсивного типа продолжается до тех пор, пока не будет полностью пересечена структура дерева двух типов или до тех пор, пока не появится конфликт. В этом примере в конечном итоге мы получим
[]B ≡ []byte => B ≡ byte => B ➞ byte
Все работает нормально, и объединение позволяет аргументам типа
A ➞ string
B ➞ byte
C ➞ int
Объединение типов с разными структурами
Теперь давайте рассмотрим небольшой вариант предыдущего примера: здесьX
иY
не имеют такой же структуры типа. Когда типовые деревья сравниваются рекурсивно, объединение все еще успешно делает аргумент типа дляA
Полем Но типы значений карт различны, а объединение терпит неудачу.
X: map[A]struct{i int; s []B}
Y: map[string]bool
ОбаX
иY
являются типами карт, поэтому объединение продолжается рекурсивно, как и раньше, начиная с типов ключей. Мы прибываем
A ≡ string => A ➞ string
Также, как и прежде. Но когда мы переходим к типам значений карты
struct{…} ≡ bool
Аstruct
Тип не совпадаетbool
; У нас есть разные типы, а объединение (и, следовательно, вывод типа).
Объединение типов с противоречивыми аргументами типа
Другой вид конфликта появляется, когда разные типы совпадают с одним и тем же параметром типа. Здесь у нас снова есть версия нашего первоначального примера, но теперь параметр типаA
появляется дваждыX
, иC
появляется дваждыY
Полем
X: map[A]struct{i int; s []A}
Y: map[string]struct{i C; s []C}
Объединение рекурсивного типа сначала работает нормально, и у нас есть следующие пары параметров и типов типа:
A ≡ string => A ➞ string // map key type
int ≡ C => C ➞ int // first struct field type
Когда мы добираемся до второго типа поля структуры, у нас
[]A ≡ []C => A ≡ C
С тех пор обаA
иC
есть тип аргумента, выведенный для них, они обозначают эти аргументы, которыеstring
иint
соответственно. Это разные типы, поэтомуA
иC
Не могу соответствовать. Объединение и, следовательно, вывод типа не удается.
Другие типовые отношения
Объединение решает типовые уравнения формыX ≡ Y
где цельТип идентификацияПолем Но как насчетX :≡ Y
илиX ∈ Y
?
Несколько наблюдений помогают нам здесь: задача типового вывода состоит в том, чтобы найти типы аргументов опущенного типа. Тип вывода всегда сопровождается типом или функциейэкземпляркоторый проверяет, что каждый аргумент типа фактически удовлетворяет его ограничению соответствующего типа. Наконец, в случае общего вызова функции компилятор также проверяет, что аргументы функций назначаются их соответствующим параметрам функции. Все эти шаги должны быть успешными, чтобы код был действительным.
Если вывод типа недостаточно точен, он может сделать вывод (неправильный) аргумент типа, где не может быть типа. Если это так, либо экземпляр, либо прохождение аргументов потерпит неудачу. В любом случае, компилятор будет создавать сообщение об ошибке. Просто сообщение об ошибке может быть немного другим.
Это понимание позволяет нам немного разыграть с типовыми отношениями:≡
и∈
Полем В частности, это позволяет нам упростить их так, чтобы с ними можно было обращаться почти так же, как≡
Полем Целью упрощений является извлечение как можно большего количества типов информации из уравнения типа и, таким образом, вывести аргументы типа, в которых точная реализация может потерпеть неудачу, потому что мы можем.
Упрощение x: ≡ y
Правила назначения GO довольно сложные, но большую часть времени мы можем обойтись с идентичностью типа или небольшим изменением. Пока мы находим аргументы потенциальных типов, мы счастливы, именно потому, что тип вывода все еще сопровождается экземпляром типа и вызовом функций. Если вывод находит аргумент типа, где он не должен, он будет пойман позже. Таким образом, при сопоставлении для назначения, мы вносим следующие корректировки в алгоритм нерестации:
- Когда названный (определенный) тип сопоставлен с литералом типа, вместо этого сравниваются их базовые типы.
- При сравнении типов каналов направления канала игнорируются.
Кроме того, направление назначения игнорируется:X :≡ Y
относится как кY :≡ X
Полем
Эти корректировки применяются только на верхнем уровне структуры типа: например, за GOПравила назначения, названный тип карты может быть назначен неназванным типам карты, но типы ключей и элементов все еще должны быть идентичными. С этими изменениями объединение для назначения становится (незначительным) изменением объединения для идентификации типа. Следующий пример иллюстрирует это.
Давайте предположим, что мы передаем ценность нашего более раннегоList
Тип (определяется какtype List []int
) к параметру функции типа[]E
гдеE
параметр связанного типа (т.е.E
объявляется общей функцией, которая называется). Это приводит к уравнению типа[]E :≡ List
Полем
Попытка объединить эти два типа требует сравнения[]E
сList
Эти два типа не идентичны, и без каких -либо изменений в том, как работает объединение, оно не удастся. Но поскольку мы объединяемся для назначения, этот первоначальный матч не должен быть точным.
Нет никакого вреда в продолжении основного типа названного типаList
: В худшем случае мы можем сделать вывод неправильного аргумента типа, но это приведет к ошибке позже, когда назначаются назначения. В лучшем случае мы находим полезный и правильный аргумент типа. В нашем примере неточное объединение добивается успеха, и мы правильно выводимint
дляE
Полем
Упрощение x ∈ Y
Возможность упростить отношение удовлетворенности ограничения еще более важна, поскольку ограничения могут быть очень сложными.
Опять же, удовлетворение ограничений проверяется во время экземпляра, поэтому цель здесь состоит в том, чтобы помочь типу вывода, где мы можем. Обычно это ситуации, когда мы знаем структуру параметра типа; Например, мы знаем, что это должен быть тип среза, и мы заботимся о типе элемента среза. Например, список параметров типа формы[P ~[]E]
говорит нам, что угодноP
это, его базовый тип должен иметь форму[]E
Полем Это именно те ситуации, когда ограничение имеетОсновной типПолем
Поэтому, если у нас есть уравнение формы
P ∈ constraint // or
P ∈ ~constraint
и еслиcore(constraint)
(илиcore(~constraint)
соответственно) существует, уравнение может быть упрощено до
P ≡ core(constraint)
under(P) ≡ core(~constraint) // respectively
Во всех других случаях уравнения типа, включающие ограничения, игнорируются.
Расширение предполагаемых типов
Если объединение успешно, он создает отображение от параметров типа с аргументами предполагаемого типа. Но только объединение не гарантирует, что предполагаемые типы не содержат параметров связанных типов. Чтобы понять, почему это так, рассмотрим общую функциюg
ниже, который вызван одним аргументомx
типаint
:
func g[A any, B []C, C *A](x A) { … }
var x int
g(x)
Ограничение типа дляA
являетсяany
который не имеет основного типа, поэтому мы игнорируем его. Оставшиеся ограничения типа имеют основные типы, и они[]C
и*A
соответственно. Вместе с аргументом, переданнымg
, после незначительных упрощений, типовые уравнения:
A :≡ int
B ≡ []C
C ≡ *A
Поскольку каждое уравнение связывает параметр типа против типа параметра, не являющегося типом, Unitific
A ➞ int
B ➞ []C
C ➞ *A
Но это оставляет параметры типаA
иC
В предполагаемых типах, что не полезно. Как и в алгебре средней школы, после того, как уравнение решается для переменнойx
, нам нужно заменитьx
с его значением на протяжении оставшихся уравнений. В нашем примере на первом шагеC
в[]C
заменяется предполагаемым типом («значение») дляC
, что есть*A
, и мы прибываем в
A ➞ int
B ➞ []*A // substituted *A for C
C ➞ *A
Еще на два шага мы заменимA
в предполагаемых типах[]*A
и*A
с предполагаемым типом дляA
, что естьint
:
A ➞ int
B ➞ []*int // substituted int for A
C ➞ *int // substituted int for A
Только сейчас вывод сделан. И, как в средней школе алгебры, иногда это не работает. Можно прийти к такой ситуации, как
X ➞ Y
Y ➞ *X
После одного раунда замены у нас
X ➞ *X
Если мы продолжим, предполагаемый тип дляX
продолжает расти:
X ➞ **X // substituted *X for X
X ➞ ***X // substituted *X for X
etc.
Тип вывода обнаруживает такие циклы во время расширения и сообщает об ошибке (и, следовательно, сбой).
Нетистные постоянные
К настоящему времени мы увидели, как работает тип вывода, решая уравнения типа с объединением, за которым следует расширение результата. Но что, если нет типов? Что если аргументы функции являются нетипированными константами?
Другой пример помогает нам пролить свет на эту ситуацию. Давайте рассмотрим функциюfoo
который принимает произвольное количество аргументов, которые должны иметь один и тот же тип.foo
вызывается с разнообразными постоянными аргументами, включая переменнуюx
типаint
:
func foo[P any](...P) {}
var x int
foo(x) // P ➞ int, same as foo[int](x)
foo(x, 2.0) // P ➞ int, 2.0 converts to int without loss of precision
foo(x, 2.1) // P ➞ int, but parameter passing fails: 2.1 is not assignable to int
Для вывода типа напечатанные аргументы имеют приоритет по сравнению с нетипированными аргументами. Неугоминная константа рассматривается только для вывода только в том случае, если параметр типа, которому он назначен, пока не имеет предполагаемого типа. В этих первых трех призывах кfoo
, переменнаяx
определяет предполагаемый тип дляP
: Это типx
который естьint
Полем Нетипедные константы игнорируются для вывода типа в этом случае, и звонки ведут себя точно так же, как если быfoo
был явно создан сint
Полем
Становится все интереснее, еслиfoo
вызывается только с нетипированными постоянными аргументами. В этом случае вывод типа учитываетТипы по умолчаниюнетипированных констант. В качестве быстрого напоминания, вот возможные типы по умолчанию в Go:
Example Constant kind Default type Order
true boolean constant bool
42 integer constant int earlier in list
'x' rune constant rune |
3.1416 floating-point constant float64 v
-1i complex constant complex128 later in list
"gopher" string constant string
С помощью этой информации давайте рассмотрим вызов функции
foo(1, 2) // P ➞ int (default type for 1 and 2)
Нетипные постоянные аргументы1
и2
оба целочисленные постоянные, их тип по умолчаниюint
и таким образом этоint
это выводится для параметра типаP
изfoo
Полем
Если разные константы-непредвзятые целочисленные и константы с плавающей точкой-содержите одну и ту же переменную типа, у нас есть разные типы по умолчанию. До хода 1.21 это считалось конфликтом и привело к ошибке:
foo(1, 2.0) // Go 1.20: inference error: default types int, float64 don't match
Такое поведение было не очень эргономичным в использовании, а также отличалось от поведения нетипированных констант в выражениях. Например, GO разрешает постоянное выражение1 + 2.0
; Результатом является постоянная с плавающей точкой3.0
с типом по умолчаниюfloat64
Полем
В Go 1.21 поведение было изменено соответственно. Теперь, если несколько нетипированных численных константов сопоставлены с одним и тем же параметром, тип по умолчанию, который появляется позже в спискеint
Вrune
Вfloat64
Вcomplex
выбрано, соответствующее правилам дляпостоянные выражения:
foo(1, 2.0) // Go 1.21: P ➞ float64 (larger default type of 1 and 2.0; behavior like in 1 + 2.0)
Специальные ситуации
К настоящему времени у нас есть общая картина о типовом выводе. Но есть пара важных особых ситуаций, которые заслуживают некоторого внимания.
Зависимости заказа параметров
Первый связан с зависимостями заказа параметров. Важным свойством, которое мы хотим от вывода типа, является то, что те же типы выводятся независимо от порядка параметров функции (и соответствующий порядок аргумента в каждом вызове этой функции).
Давайте пересмотрим нашу варидическуюfoo
Функция: тип, предполагаемый дляP
должен быть таким же независимо от порядка, в котором мы передаем аргументыs
иt
(детская площадка)
func foo[P any](...P) (x P) {}
type T struct{}
func main() {
var s struct{}
var t T
fmt.Printf("%T\n", foo(s, t))
fmt.Printf("%T\n", foo(t, s)) // expect same result independent of parameter order
}
От звонков кfoo
Мы можем извлечь соответствующие уравнения типа:
𝑻(x) :≡ 𝑻(s) => P :≡ struct{} // equation 1
𝑻(x) :≡ 𝑻(t) => P :≡ T // equation 2
К сожалению, упрощенная реализация для:≡
создает зависимость заказа:
Если объединение начинается с уравнения 1, оно соответствуетP
противstruct
; P
пока не имеет этого типа, и, следовательно, объединениеP ➞ struct{}
Полем Когда объединение видит типT
Позднее в уравнении 2 он продолжается с базовым типомT
который естьstruct{}
ВP
иunder(T)
Объединение и объединение и, следовательно, вывод достигает успеха.
Наоборот, если объединение начинается с уравнения 2, оно соответствуетP
противT
; P
пока не имеет этого типа, и, следовательно, объединениеP ➞ T
Полем Когда объединение видитstruct{}
Позже в уравнении 1 он продолжается с базовым типом типаT
выводится дляP
Полем Этот основной типstruct{}
, что соответствуетstruct
в уравнении 1, а также объединение и, следовательно, вывод достигает успеха.
Как следствие, в зависимости от порядка, в котором объединение решает два уравнения типа, предполагаемый тип либо илиstruct{}
илиT
Полем Это, конечно, неудовлетворительно: программа может внезапно перестать собирать просто потому, что аргументы могли быть перетасованы во время рефакторинга кода или очистки.
Восстановление независимости порядка
К счастью, лекарство довольно простое. Все, что нам нужно, это небольшая коррекция в некоторых ситуациях.
В частности, если объединение решаетP :≡ T
и
P
параметр типа, который уже вышелA
:P ➞ A
A :≡ T
это правдаT
это названный тип
затем установите предполагаемый тип дляP
кT
: P ➞ T
Это гарантирует этоP
это названный тип, если есть выбор, независимо от того, в какой момент названный тип появился в матче противP
(т.е. независимо от того, в каком порядке решаются уравнения типа). Обратите внимание, что если различные названные типы совпадают с одним и тем же параметром типа, у нас всегда есть сбой нереста, потому что различные названные типы не идентичны по определению.
Поскольку мы сделали аналогичные упрощения для каналов и интерфейсов, они также нуждаются в аналогичной специальной обработке. Например, мы игнорируем направления канала при объединении для назначения и в результате могут сделать вывод направленного или двунаправленного канала в зависимости от порядка аргумента. Подобные проблемы возникают с интерфейсами. Мы не собираемся обсуждать это здесь.
Возвращаясь к нашему примеру, если объединение начинается с уравнения 1, оно проживаетP ➞ struct{}
как раньше. Когда он продолжается с уравнением 2, как и прежде, объединение преуспевает, но теперь у нас есть именно то условие, которое требует коррекции:P
это параметр типа, который уже имеет тип (struct{}
), struct{}
Вstruct{} :≡ T
это правда (потому чтоstruct{} ≡ under(T)
это правда), иT
это названный тип. Таким образом, объединение создает коррекцию и наборыP ➞ T
Полем В результате, независимо от порядка объединения, результат такой же (T
) в обоих случаях.
Саморекурсирующие функции
Другим сценарием, который вызывает проблемы в наивной реализации вывода, является саморекурсирующие функции. Давайте рассмотрим общую факторную функциюfact
, определено таким образом, что он также работает для аргументов с плавающей точкой (детская площадка) Обратите внимание, что это не математически правильная реализацияГамма -функция, это просто удобный пример.
func fact[P ~int | ~float64](n P) P {
if n <= 1 {
return 1
}
return fact(n-1) * n
}
Смысл здесь не факториальная функция, а скорееfact
вызывает себя аргументомn-1
что такого же типаP
в качестве входящего параметраn
Полем В этом вызове параметр типаP
одновременно связан и параметр свободного типа: он связан, потому что он объявленfact
, функция, которую мы называем рекурсивно. Но это также бесплатно, потому что она объявлена функцией, заключающей вызов, который также оказываетсяfact
Полем
Уравнение в результате передачи аргументаn-1
к параметруn
ямыP
против себя:
𝑻(n) :≡ 𝑻(n-1) => P :≡ P
Объединение видит то же самоеP
по обе стороны уравнения. Объединение добивается успеха, поскольку оба типа идентичны, но информация не получена и не полученаP
остается без предполагаемого типа. Как следствие, вывод типа не удается.
К счастью, трюк для решения этого прост: перед выводом вывода типа, и для (временного) использования только по типовым выводу компилятор переименовает параметры типа в подписях (но не в телах) всех функций, связанных с соответствующим вызовом. Это не изменяет значение подписей функции: они обозначают те же общие функции независимо от того, каковы имена параметров типа.
Для целей этого примера давайте предположимP
в подписиfact
был переименован вQ
Полем Эффект, как будто рекурсивный вызов был сделан косвенно черезhelper
функция (детская площадка):
func fact[P ~int | ~float64](n P) P {
if n <= 1 {
return 1
}
return helper(n-1) * n
}
func helper[Q ~int | ~float64](n Q) Q {
return fact(n)
}
С переименованием или сhelper
функция, уравнение, возникающее в результате прохожденияn-1
к рекурсивному вызовуfact
(илиhelper
функционировать, соответственно) изменения в
𝑻(n) :≡ 𝑻(n-1) => Q :≡ P
Это уравнение имеет два параметра типа: параметр граничного типаQ
, объявлена функцией, которая вызывается, и параметром свободного типаP
, объявлено функцией окружения. Это уравнение типа тривиально решено дляQ
и приводит к выводуQ ➞ P
Что, конечно, то, чего мы ожидаем, и что мы можем проверить, явно создав рекурсивный вызов (детская площадка):
func fact[P ~int | ~float64](n P) P {
if n <= 1 {
return 1
}
return fact[P](n-1) * n
}
Чего не хватает?
Явно отсутствует в нашем описании, тип вывод для общих типов: в настоящее время общие типы всегда должны быть явно созданы.
Есть несколько причин для этого. Прежде всего, для создания типов, вывод типа имеет только типовые аргументы для работы; Нет других аргументов, как в случае вызовов функций. Как следствие, по крайней мере один аргумент типа всегда должен быть предоставлен (за исключением патологических случаев, когда ограничения типа предписывают именно один возможный аргумент типа для всех параметров типа). Таким образом, тип вывод для типов полезен только для завершения частично создательного типа, где все аргументы пропущенных типов могут быть выведены из уравнений, возникающих в результате ограничений типа; то есть, где есть как минимум два параметра типа. Мы считаем, что это не очень распространенный сценарий.
Во -вторых, и более уместные параметры типа позволяют совершенно новый вид рекурсивных типов. Рассмотрим гипотетический тип
type T[P T[P]] interface{ … }
где ограничение дляP
тип объявляется. В сочетании с способностью иметь параметры нескольких типов, которые могут относиться друг к другу в сложной рекурсивной моде, вывод типа становится гораздо более сложным, и мы не до конца понимаем все последствия этого в данный момент. Тем не менее, мы считаем, что не должно быть слишком сложно обнаружить циклы и продолжать с выводом типа, когда такие циклы не существует.
Наконец, существуют ситуации, когда вывод типа просто недостаточно силен, чтобы сделать вывод, как правило, потому, что объединение работает с определенными упрощенными предположениями, такими как описанные ранее в этом посте. Основным примером здесь являются ограничения, которые не имеют основного типа, но там, где более сложный подход может в любом случае вывести информацию типа.
Это все области, где мы можем увидеть постепенные улучшения в будущих выпусках. Важно отметить, что мы полагаем, что случаи, когда вывод в настоящее время терпит неудачу, являются либо редкими, либо неважными в производственном коде, и что наша текущая реализация охватывает подавляющее большинство всех полезных сценариев кода.
Тем не менее, если вы столкнетесь с ситуацией, когда вы верите, что вывод типа должен работать или сбиться с пути, пожалуйстаподать проблему! Как всегда, команда GO любит слышать от вас, особенно когда это помогает нам сделать еще лучше.
Роберт Грисмер
ФотоJr KorpaнаНеспособный
Эта статья доступна на
Оригинал