Go: предложение: spec: добавить типы сумм / размеченные союзы

Созданный на 6 мар. 2017  ·  320Комментарии  ·  Источник: golang/go

Это предложение для типов суммы, также известных как дискриминируемые объединения. Типы сумм в Go должны действовать как интерфейсы, за исключением того, что:

  • это типы значений, такие как структуры
  • типы, содержащиеся в них, фиксируются во время компиляции

Типы сумм можно сопоставить с помощью оператора switch. Компилятор проверяет соответствие всех вариантов. В ответвлениях оператора switch значение может использоваться так, как если бы оно было сопоставлено с вариантом.

Go2 LanguageChange NeedsInvestigation Proposal

Самый полезный комментарий

Спасибо за создание этого предложения. Я обдумывал эту идею уже год или около того.
Вот что у меня есть с конкретным предложением. думаю
«Тип выбора» может быть лучше, чем «тип суммы», но YMMV.

Типы сумм в Go

Тип суммы представлен двумя или более типами в сочетании с символом "|"
оператор.

type: type1 | type2 ...

Значения результирующего типа могут содержать только один из указанных типов. В
type рассматривается как тип интерфейса - его динамический тип соответствует типу
присвоенное ему значение.

В качестве особого случая можно использовать "nil", чтобы указать, может ли значение
стать нулевым.

Например:

type maybeInt nil | int

Набор методов типа сумма содержит пересечение набора методов
всех его типов компонентов, исключая любые методы, которые имеют одинаковые
имя но разные подписи.

Как и любой другой тип интерфейса, тип суммы может быть предметом динамического
преобразование типов. В переключателях типа первое плечо переключателя, которое
соответствует сохраненному типу, будет выбран.

Нулевое значение типа суммы - это нулевое значение первого типа в
сумма.

При присвоении значения типу суммы, если значение может поместиться в более
чем один из возможных типов, то выбирается первый.

Например:

var x int|float64 = 13

приведет к значению с динамическим типом int, но

var x int|float64 = 3.13

приведет к значению с динамическим типом float64.

Реализация

Наивная реализация могла бы реализовать типы сумм точно так же, как интерфейс
ценности. Более сложный подход может использовать представление
соответствующий набору возможных значений.

Например, тип суммы, состоящий только из конкретных типов без указателей.
может быть реализован с типом без указателя, используя дополнительное значение для
запомните реальный тип.

Для sum-of-struct-types можно даже использовать запасные отступы
байты, общие для структур для этой цели.

Все 320 Комментарий

Это обсуждалось несколько раз в прошлом, начиная с выпуска с открытым исходным кодом. Прошлый консенсус заключался в том, что типы суммы не очень сильно добавляют к типам интерфейсов. Разобравшись со всем этим, вы получите тип интерфейса, в котором компилятор проверяет, что вы заполнили все случаи переключения типа. Это довольно небольшое преимущество для новой смены языка.

Если вы хотите продвигать это предложение дальше, вам нужно будет написать более полный документ предложения, в том числе: Каков синтаксис? Как именно они работают? (Вы говорите, что это «типы значений», но типы интерфейсов также являются типами значений). Какие компромиссы?

См. Https://www.reddit.com/r/golang/comments/46bd5h/ama_we_are_the_go_contributors_ask_us_anything/d03t6ji/?st=ixp2gf04&sh=7d6920db, чтобы узнать о некоторых прошлых обсуждениях.

Я считаю, что это слишком существенное изменение системы типов для Go1 и в этом нет необходимости.
Я предлагаю вернуться к этому в более широком контексте Go 2.

Спасибо за создание этого предложения. Я обдумывал эту идею уже год или около того.
Вот что у меня есть с конкретным предложением. думаю
«Тип выбора» может быть лучше, чем «тип суммы», но YMMV.

Типы сумм в Go

Тип суммы представлен двумя или более типами в сочетании с символом "|"
оператор.

type: type1 | type2 ...

Значения результирующего типа могут содержать только один из указанных типов. В
type рассматривается как тип интерфейса - его динамический тип соответствует типу
присвоенное ему значение.

В качестве особого случая можно использовать "nil", чтобы указать, может ли значение
стать нулевым.

Например:

type maybeInt nil | int

Набор методов типа сумма содержит пересечение набора методов
всех его типов компонентов, исключая любые методы, которые имеют одинаковые
имя но разные подписи.

Как и любой другой тип интерфейса, тип суммы может быть предметом динамического
преобразование типов. В переключателях типа первое плечо переключателя, которое
соответствует сохраненному типу, будет выбран.

Нулевое значение типа суммы - это нулевое значение первого типа в
сумма.

При присвоении значения типу суммы, если значение может поместиться в более
чем один из возможных типов, то выбирается первый.

Например:

var x int|float64 = 13

приведет к значению с динамическим типом int, но

var x int|float64 = 3.13

приведет к значению с динамическим типом float64.

Реализация

Наивная реализация могла бы реализовать типы сумм точно так же, как интерфейс
ценности. Более сложный подход может использовать представление
соответствующий набору возможных значений.

Например, тип суммы, состоящий только из конкретных типов без указателей.
может быть реализован с типом без указателя, используя дополнительное значение для
запомните реальный тип.

Для sum-of-struct-types можно даже использовать запасные отступы
байты, общие для структур для этой цели.

@rogpeppe Как это будет взаимодействовать с утверждениями типов и переключателями типов? Предположительно, было бы ошибкой времени компиляции иметь case для типа (или утверждения для типа), который не является членом суммы. Было бы также ошибкой иметь неисчерпывающий переключатель на таком типе?

Для переключателей типа, если у вас есть

type T int | interface{}

а вы делаете:

switch t := t.(type) {
  case int:
    // ...

и t содержит интерфейс {}, содержащий int, соответствует ли он первому случаю? Что, если первый случай - case interface{} ?

Или типы сумм могут содержать только конкретные типы?

А как насчет type T interface{} | nil ? Если вы напишете

var t T = nil

что это за тип? Или это строительство запрещено? Аналогичный вопрос возникает для type T []int | nil , так что дело не только в интерфейсах.

Да, я думаю, было бы разумно иметь ошибку времени компиляции
иметь дело, которое не может быть сопоставлено. Не уверен, что это
хорошая идея разрешить неполные переключатели для такого типа - мы
больше нигде не требуется полноты. Одна вещь, которая могла бы
будьте внимательны: если переключатель является исчерпывающим, мы не можем требовать значение по умолчанию
чтобы сделать его завершающим заявлением.

Это означает, что вы можете вызвать ошибку компилятора, если у вас есть:

func addOne(x int|float64) int|float64 {
    switch x := x.(type) {
    case int:
        return x + 1
    case float64:
         return x + 1
    }
}

и вы меняете тип суммы, чтобы добавить дополнительный случай.

Для переключателей типа, если у вас есть

тип T int | интерфейс{}

а вы делаете:

switch t: = t. (type) {
case int:
// ...
и t содержит интерфейс {}, содержащий int, соответствует ли он первому случаю? Что, если первый случай - это case interface {}?

t не может содержать интерфейс {}, содержащий int. t - интерфейс
type так же, как и любой другой тип интерфейса, за исключением того, что он может только
содержат перечислимый набор типов, из которых он состоит.
Точно так же, как интерфейс {} не может содержать интерфейс {}, содержащий int.

Типы сумм могут соответствовать типам интерфейсов, но они все равно получают конкретную
тип для динамического значения. Например, было бы хорошо иметь:

type R io.Reader | io.ReadCloser

А как насчет интерфейса типа T {} | ноль? Если вы напишете

var t T = ноль

что это за тип? Или это строительство запрещено? Аналогичный вопрос возникает для типа T [] int | nil, так что дело не только в интерфейсах.

Согласно предложенному выше предложению вы получаете первый предмет
в сумме, которой может быть присвоено значение, поэтому
вы получите нулевой интерфейс.

Фактически интерфейс {} | nil технически избыточен, потому что любой интерфейс {}
может быть нулевым.

Для [] int | nil, nil [] int - это не то же самое, что интерфейс nil, поэтому
конкретное значение ([]int|nil)(nil) будет []int(nil) не нетипизированным nil .

Случай []int | nil интересен. Я бы ожидал, что nil в объявлении типа всегда будет означать "значение интерфейса nil", и в этом случае

type T []int | nil
var x T = nil

будет означать, что x - это интерфейс nil, а не nil []int .

Это значение будет отличаться от nil []int закодированного в том же типе:

var y T = []int(nil)  // y != x

Разве nil не требуется всегда, даже если сумма представляет собой все типы значений? Иначе что было бы var x int64 | float64 ? Моей первой мыслью, экстраполированной из других правил, было бы нулевое значение первого типа, но как насчет var x interface{} | int ? Как указывает @bcmills , это должна быть отдельная сумма nil.

Это кажется слишком тонким.

Было бы неплохо переключатели исчерпывающего типа. Вы всегда можете добавить пустой default: если это нежелательное поведение.

В предложении говорится: "При присвоении значения типу суммы, если это значение может соответствовать большему количеству
чем один из возможных типов, то выбирается первый ».

Итак, с:

type T []int | nil
var x T = nil

x будет иметь конкретный тип [] int, потому что nil присваивается [] int, а [] int является первым элементом типа. Он был бы равен любому другому значению [] int (nil).

Разве nil не требуется всегда, даже если сумма представляет собой все типы значений? Иначе что бы var x int64 | float64 быть?

В предложении говорится: «Нулевое значение типа суммы - это нулевое значение первого типа в
сумма. ", поэтому ответ - int64 (0).

Моя первая мысль, экстраполированная из других правил, была бы нулевым значением первого типа, но потом как насчет var x interface {} | int? Как указывает @bcmills , это должна быть отдельная сумма nil

Нет, в этом случае это будет обычное значение интерфейса nil. Этот тип (interface {} | nil) является избыточным. Возможно, было бы хорошей идеей сделать его компилятором, чтобы указывать типы сумм, в которых один элемент является надмножеством другого, поскольку в настоящее время я не вижу никакого смысла в определении такого типа.

Нулевое значение типа суммы - это нулевое значение первого типа суммы.

Это интересное предложение, но поскольку тип суммы должен где-то записывать тип значения, которое он в настоящее время содержит, я считаю, что это означает, что нулевое значение типа суммы не является нулевым для всех байтов, что сделало бы его отличным от любой другой тип в Go. Или, возможно, мы могли бы добавить исключение, говорящее, что если информация о типе отсутствует, тогда значение будет нулевым значением первого перечисленного типа, но тогда я не уверен, как представить nil если это не так. первый тип в списке.

Итак, (stuff) | nil имеет смысл только тогда, когда ничто в (материале) не может быть нулевым, а nil | (stuff) означает что-то другое, в зависимости от того, может ли что-нибудь в материале быть нулевым? Какое значение добавляет ноль?

@ianlancetaylor Я считаю, что многие функциональные языки реализуют (закрытые) типы сумм, по сути, как в C

struct {
    int which;
    union {
         A a;
         B b;
         C c;
    } summands;
}

если which индексирует поля объединения по порядку, 0 = a, 1 = b, 2 = c, определение нулевого значения работает так, что все байты равны нулю. И вам нужно будет хранить типы в другом месте, в отличие от интерфейсов. Вам также понадобится специальная обработка какого-либо тега nil везде, где вы храните информацию о типе.

Это сделало бы типы значений union вместо специальных интерфейсов, что тоже интересно.

Есть ли способ заставить работать все нулевые значения, если поле, в котором записан тип, имеет нулевое значение, представляющее первый тип? Я предполагаю, что это может быть представлено одним из возможных способов:

type A = B|C
struct A {
  choice byte // value 0 or 1
  value ?// (thing big enough to store B | C)
}

[редактировать]

Прости, @jimmyfrasche опередил меня.

Есть ли что-нибудь добавленное nil, что не может быть сделано с помощью

type S int | string | struct{}
var None struct{}

?

Похоже, это позволяет избежать путаницы (по крайней мере, у меня)

Или лучше

type (
     None struct{}
     S int | string | None
)

таким образом вы можете ввести переключатель None и назначить None{}

@jimmyfrasche struct{} не равно nil . Это мелочь, но из-за этого переключатели типов на суммах без нужды (?) Расходятся с переключателями типов на других типах.

@bcmills Я не собирался утверждать иное - я имел в виду, что его можно использовать для той же цели, что и для определения отсутствия значения, не пересекаясь со значением nil в любом из типов суммы.

@rogpeppe что это печатает?

// r is an io.Reader interface value holding a type that also implements io.Closer
var v io.ReadCloser | io.Reader = r
switch v.(type) {
case io.ReadCloser: fmt.Println("ReadCloser")
case io.Reader: fmt.Println("Reader")
}

Я бы предположил, что "Читатель"

@jimmyfrasche Я бы предположил, что ReadCloser , как и при переключении типа на любом другом интерфейсе.

(И я также ожидал бы, что суммы, включающие только типы интерфейсов, будут использовать не больше места, чем обычный интерфейс, хотя я полагаю, что явный тег может сэкономить немного служебных данных при поиске в переключателе типа.)

@bcmills - вот что интересно, подумайте: https://play.golang.org/p/PzmWCYex6R

@ianlancetaylor Это отличный вопрос, чтобы поднять, спасибо. Я не думаю, что это сложно обойтись, хотя это подразумевает, что мое предложение «наивной реализации» само по себе слишком наивно. Тип суммы, хотя и рассматривается как тип интерфейса, на самом деле не должен содержать прямой указатель на тип и его набор методов - вместо этого он может, при необходимости, содержать целочисленный тег, который подразумевает тип. Этот тег может быть ненулевым, даже если сам тип равен нулю.

Данный:

 var x int | nil = nil

во время выполнения значение x не обязательно должно быть нулем. При включении типа х или конвертации
его к другому типу интерфейса, тег может быть перенаправлен через небольшую таблицу, содержащую
фактические указатели типов.

Другая возможность - разрешить тип nil, только если это первый элемент, но
что исключает такие конструкции, как:

var t nil | int
var u float64 | t

@jimmyfrasche Я бы предположил, что ReadCloser, как и при переключении типа на любом другом интерфейсе.

да.

@bcmills - вот что интересно, подумайте: https://play.golang.org/p/PzmWCYex6R

Я этого не понимаю. Почему "это [...] должно быть действительным, чтобы переключатель типа печатал ReadCloser"
Как и любой тип интерфейса, тип суммы будет хранить не больше, чем конкретное значение того, что в нем.

Когда в сумме имеется несколько типов интерфейса, представление среды выполнения является просто значением интерфейса - просто мы знаем, что базовое значение должно реализовывать одну или несколько заявленных возможностей.

То есть, когда вы присваиваете что-либо типу (I1 | I2), где и I1, и I2 являются типами интерфейса, позже невозможно определить, было ли значение, в которое вы ввели, было известно, что оно реализует I1 или I2 в то время.

Если у вас есть тип io.ReadCloser | io.Reader, вы не можете быть уверены, когда набираете switch или утверждаете в io.Reader, что это не io.ReadCloser, если только присвоение типу суммы не распаковывает и не переупаковывает интерфейс.

Если у вас io.Reader | io.ReadCloser либо никогда не примет io.ReadCloser, потому что он идет строго справа налево, либо реализация должна будет искать «наиболее подходящий» интерфейс из всех интерфейсов в сумме, но это не может быть точно определено.

@rogpeppe В вашем предложении, игнорируя возможности оптимизации в реализации и тонкости нулевых значений, основное преимущество использования типа суммы по сравнению с вручную созданным типом интерфейса (содержащим пересечение соответствующих методов) заключается в том, что средство проверки типов может указывать на ошибки во время компиляции, а не во время выполнения. Второе преимущество заключается в том, что значение типа более различается и, таким образом, может помочь с удобочитаемостью / пониманием программы. Есть ли другие важные преимущества?

(Я никоим образом не пытаюсь преуменьшить значение предложения, просто пытаюсь понять мою интуицию. Особенно, если дополнительная синтаксическая и семантическая сложность «достаточно мала» - что бы это ни значило - я могу окончательно увидеть преимущества наличия компилятора обнаруживать ошибки на ранней стадии.)

@griesemer Да, именно так.

В частности, при передаче сообщений по каналам или сети, я думаю, что возможность иметь тип, который точно выражает доступные возможности, помогает удобочитаемости и правильности. В настоящее время обычным делом является нерешительная попытка сделать это, включив неэкспортированный метод в тип интерфейса, но это а) можно обойти с помощью встраивания и б) трудно увидеть все возможные типы, потому что неэкспортированный метод скрыт.

@jimmyfrasche

Если у вас есть тип io.ReadCloser | io.Reader, вы не можете быть уверены, когда набираете switch или утверждаете в io.Reader, что это не io.ReadCloser, если только присвоение типу суммы не распаковывает и не переупаковывает интерфейс.

Если у вас есть этот тип, вы знаете, что это всегда io.Reader (или nil, потому что любой io.Reader также может быть nil). Эти две альтернативы не исключают друг друга - предлагаемый тип суммы является «включающим или», а не «исключающим или».

Если у вас io.Reader | io.ReadCloser либо никогда не примет io.ReadCloser, потому что он идет строго справа налево, либо реализация должна будет искать «наиболее подходящий» интерфейс из всех интерфейсов в сумме, но это не может быть точно определено.

Если, говоря «пойти другим путем», вы имеете в виду присвоение этому типу, в предложении говорится:

"При присвоении значения типу суммы, если значение может поместиться в более
чем один из возможных типов, то выбирается первый ».

В этом случае io.ReadCloser может поместиться как в io.Reader, так и в io.ReadCloser, поэтому он выбирает io.Reader, но на самом деле нет никакого способа сказать потом. Нет заметной разницы между типом io.Reader и типом io.Reader | io.ReadCloser, потому что io.Reader также может содержать все типы интерфейсов, которые реализуют io.Reader. Вот почему я подозреваю, что было бы неплохо заставить компилятор отклонять такие типы. Например, он может отклонить любой тип суммы, включающий interface {}, потому что interface {} уже может содержать любой тип, поэтому дополнительные квалификации не добавляют никакой информации.

@rogpeppe, мне много всего нравится в вашем предложении. Семантика присваивания слева направо и нулевое значение - это нулевое значение крайнего левого типа. Правила очень ясны и просты. Очень хорошо.

Что меня беспокоит, так это присвоение значения, которое уже помещено в коробку в интерфейсе, переменной с типом суммы.

Давайте пока воспользуемся моим предыдущим примером и скажем, что RC - это структура, которую можно присвоить io.ReadCloser.

Если ты сделаешь это

var v io.ReadCloser | io.Reader = RC{}

результаты очевидны и ясны.

Однако если вы сделаете это

var r io.Reader = RC{}
var v io.ReadCloser | io.Reader = r

единственное разумное решение - иметь v store r в качестве io.Reader, но это означает, что когда вы набираете switch on v, вы не можете быть уверены, что когда вы нажмете на корпус io.Reader, у вас на самом деле нет io.ReadCloser. Вам понадобится что-то вроде этого:

switch v := v.(type) {
case io.ReadCloser: useReadCloser(v)
case io.Reader:
  if rc, ok := v.(io.ReadCloser); ok {
    useReadCloser(rc)
  } else {
    useReader(v)
  }
}

Теперь есть смысл, в котором io.ReadCloser <: io.Reader, и вы можете просто запретить их, как вы предложили, но я думаю, что проблема более фундаментальная и может относиться к любому предложению типа суммы для Go †.

Допустим, у вас есть три интерфейса A, B и C с методами A (), B () и C () соответственно, а также структура ABC со всеми тремя методами. A, B и C не пересекаются, поэтому A | B | C и его перестановки - все допустимые типы. Но у вас еще есть дела вроде

var c C = ABC{}
var v A | B | C = c

Существует множество способов изменить это, и вы по-прежнему не получаете значимых гарантий относительно того, что такое v, когда задействованы интерфейсы. После того, как вы распаковали сумму, вам нужно распаковать интерфейс, если порядок важен.

Может быть ограничение должно состоять в том, что ни одно из слагаемых вообще не может быть интерфейсами?

Единственное другое решение, которое я могу придумать, - это запретить назначать интерфейс переменной типа суммы, но это по-своему кажется более серьезным.

†, в котором не используются конструкторы типов для типов в сумме для устранения неоднозначности (как в Haskell, где вы должны сказать Just v для создания значения типа Maybe), но я совсем не сторонник этого.

@jimmyfrasche Действительно ли это важно , легко работать вокруг с явными коробчатых структур:

type ReadCloser struct {  io.ReadCloser }
type Reader struct { io.Reader }

var v ReadCloser | Reader = Reader{r}

@bcmills Более того, результаты не очевидны и неудобны, и это означает, что все гарантии, которые вы хотите получить с типом суммы, испаряются, когда задействованы интерфейсы. Я вижу, как это вызывает множество мелких ошибок и недоразумений.

Приведенный вами пример явных блочных структур показывает, что запрещение интерфейсов в типах суммы вообще не ограничивает возможности типов суммы. Это эффективно создает конструкторы типов для устранения неоднозначности, о которых я упоминал в сноске. По общему признанию, это немного раздражает и является дополнительным шагом, но это просто и очень похоже на философию Go, которая позволяет языковым конструкциям быть как можно более ортогональными.

все гарантии, которые вы хотите с типом суммы

Это зависит от того, какие гарантии вы ожидаете. Я думаю, вы ожидаете, что сумма будет
строго помеченное значение, поэтому, учитывая любые типы A | B | C, вы точно знаете, какие статические
тип, который вы ему присвоили. Я рассматриваю это как ограничение типа на одно значение конкретного
тип - ограничение заключается в том, что значение совместимо по типу (по крайней мере) с одним из A, B и C.
В конце концов, это просто интерфейс со значением в.

То есть, если значение может быть присвоено типу суммы в силу его совместимости с присваиванием
с одним из членов типа суммы, мы не записываем, какой из этих членов был
«selected» - мы просто фиксируем само значение. То же, что и при назначении io.Reader
к интерфейсу {}, вы теряете статический тип io.Reader и получаете только само значение
который совместим с io.Reader, но также и с любым другим типом интерфейса, который бывает
реализовать.

В вашем примере:

var c C = ABC{}
var v A | B | C = c

Утверждение типа v для любого из A, B и C будет успешным. Мне это кажется разумным.

@rogpeppe, эта семантика имеет больше смысла, чем то, что я себе представлял. Я все еще не совсем уверен, что интерфейсы и суммы хорошо сочетаются, но я больше не уверен, что это не так. Прогресс!

Допустим, у вас есть type U I | *T где I - это тип интерфейса, а *T - тип, реализующий I .

Данный

var i I = new(T)
var u U = i

динамический тип u - *T , а в

var u U = new(T)

вы можете получить доступ к этому *T как к I с утверждением типа. Это верно?

Это означало бы, что при присвоении действительного значения интерфейса сумме необходимо будет искать первый соответствующий тип в сумме.

Это также будет несколько отличаться от чего-то вроде var v uint8 | int32 | int64 = i которое, я полагаю, всегда будет соответствовать любому из этих трех типов i , даже если i было int64 который может поместиться в uint8 .

Прогресс!

Ура!

вы можете получить доступ к этому * T как к I с помощью утверждения типа. Это верно?

да.

Это означало бы, что при присвоении действительного значения интерфейса сумме необходимо будет искать первый соответствующий тип в сумме.

Ага, как говорится в предложении (конечно, компилятор статически знает, какой из них выбрать, поэтому поиск во время выполнения отсутствует).

Это также несколько отличалось бы от чего-то вроде var v uint8 | int32 | int64 = i, который, как я полагаю, всегда будет использовать любой из этих трех типов i, даже если бы я был int64, который мог бы поместиться в uint8.

Да, потому что, если i не является константой, его можно будет присвоить только одной из этих альтернатив.

Да, потому что, если i не является константой, его можно будет присвоить только одной из этих альтернатив.

Я понимаю, что это не совсем так из-за правила, разрешающего присвоение безымянных типов именованным типам. Я не думаю, что это имеет большое значение. Правило остается прежним.

Таким образом, тип I | *T из моего последнего сообщения фактически совпадает с типом I а io.ReadCloser | io.Reader - фактически того же типа, что и io.Reader ?

Верно. Оба типа будут охвачены предложенным мной правилом, согласно которому компилятор отклоняет типы сумм, в которых один тип является интерфейсом, который реализуется другим типом. Такое же или подобное правило может охватывать типы сумм с повторяющимися типами, например int|int .

Одна мысль: возможно, нелогично, что int|byte - это не то же самое, что byte|int , но на практике это, вероятно, нормально.

Это означало бы, что при присвоении действительного значения интерфейса сумме необходимо будет искать первый соответствующий тип в сумме.

Ага, как говорится в предложении (конечно, компилятор статически знает, какой из них выбрать, поэтому поиск во время выполнения отсутствует).

Я не слежу за этим. В моем понимании (который может отличаться от задуманного) есть как минимум два способа справиться с объединением U из I и T-орудий-I.

1a) при присвоении U u = t тегу присваивается значение T. Дальнейший выбор приводит к T, потому что тег является T.
1b) при присвоении U u = i (i на самом деле T) тегу присваивается значение I. Более поздний выбор приводит к T, потому что тег является I, но вторая проверка (выполняется, потому что T реализует I и T является членом U) обнаруживает T.

2а) как 1а
2b) при присвоении U u = i (i на самом деле T) сгенерированный код проверяет значение (i), чтобы увидеть, действительно ли оно является T, потому что T реализует I, а T также является членом U. Поскольку то есть тег установлен на T. Более поздний выбор напрямую приводит к T.

В случае, если все T, V, W реализуют I и U = *T | *V | *W | I , присвоение U u = i требует (до) 3 тестов типа.

Однако интерфейсы и указатели не были исходным вариантом использования типов объединения, не так ли?

Я могу представить себе определенные виды хакерства, при которых "хорошая" реализация будет выполнять некоторую битовую обработку - например, если у вас есть объединение из 4 или менее типов указателей, где все референты выровнены по 4 байта, сохраните тег в нижних 2 биты значения. Это, в свою очередь, означает, что нецелесообразно брать адрес члена объединения (в любом случае это было бы неправильно, поскольку этот адрес можно было бы использовать для повторного сохранения «старого» типа без изменения тега).

Или, если бы у нас было 50-битное адресное пространство и мы были бы готовы позволить себе некоторую вольность с NaN, мы могли бы объединить целые числа, указатели и удвоения в 64-битное объединение и возможные затраты на возню с битами.

Оба дополнительных предложения являются грубыми, я уверен, что у обоих будет небольшое (?) Число фанатичных сторонников.

Это, в свою очередь, означает, что не следует брать адрес члена профсоюза.

Верный. Но я не думаю, что результат утверждения типа в любом случае можно адресовать сегодня, не так ли?

при присвоении U u = i (i на самом деле является T) тег устанавливается в I.

Думаю, в этом суть - тега I нет.

На мгновение проигнорируйте представление среды выполнения и рассмотрите тип суммы как интерфейс. Как и любой интерфейс, он имеет динамический тип (тип, который в нем хранится). «Тег», на который вы ссылаетесь, является именно тем динамическим типом.

Как вы предлагаете (и я пытался подразумевать в последнем абзаце предложения), могут быть способы сохранить тег типа более эффективными способами, чем указатель на тип времени выполнения, но в конце концов он всегда просто кодирует динамический тип значения типа суммы, а не какая из альтернатив была «выбрана» при ее создании.

Однако интерфейсы и указатели не были исходным вариантом использования типов объединения, не так ли?

Этого не было, но, на мой взгляд, любое предложение должно быть как можно более ортогональным по отношению к другим языковым особенностям.

@ dr2chase до сих пор я понимаю, что, если тип суммы включает в себя какие-либо типы интерфейса в своем определении, то во время выполнения его реализация идентична интерфейсу (содержащему пересечение наборов методов), но инварианты времени компиляции о допустимых типах все еще принудительно.

Даже если тип суммы содержал только конкретные типы и был реализован как размеченное объединение в стиле C, вы не смогли бы адресовать значение в типе суммы, поскольку этот адрес мог стать другим типом (и размером) после того, как вы взяли адрес. Однако вы можете взять адрес самого типизированного значения суммы.

Желательно, чтобы типы сумм вели себя подобным образом? Мы могли бы так же легко объявить, что выбранный / утвержденный тип совпадает с тем, что программист сказал / подразумевал, когда значение было присвоено объединению. В противном случае мы можем попасть в интересные места относительно int8, int16, int32 и т. Д. Или, например, int8 | uint8 .

Желательно, чтобы типы сумм вели себя подобным образом?

Это вопрос суждения. Я считаю, что это так, потому что у нас уже есть концепция интерфейсов в языке - значения как со статическим, так и с динамическим типом. Предлагаемые типы сумм просто обеспечивают более точный способ определения типов интерфейсов в некоторых случаях. Это также означает, что типы сумм могут работать без ограничений с любыми другими типами. Если вы этого не сделаете, вам нужно исключить типы интерфейсов, и тогда функция не будет полностью ортогональной.

В противном случае мы можем попасть в интересные места относительно int8 vs int16 vs int32 и т. Д. Или, например, int8 | uint8.

Что вас здесь беспокоит?

Вы не можете использовать тип функции в качестве ключевого типа карты. Я не говорю, что это эквивалентно, просто существует прецедент для типов, ограничивающих другие типы типов. Все еще открыт для разрешающих интерфейсов, еще не продан.

Какие программы вы можете написать с типом суммы, содержащим интерфейсы, которые вы не смогли бы иначе?

Встречное предложение.

Тип объединения - это тип, который перечисляет ноль или более типов, записанных

union {
  T0
  T1
  //...
  Tn
}

Все перечисленные типы (T0, T1, ..., Tn) в объединении должны быть разными, и ни один из них не может быть типами интерфейса.

Методы могут быть объявлены для определенного (именованного) типа объединения по обычным правилам. Никакие методы из перечисленных типов не продвигаются.

Для типов объединения нет встраивания. Перечислить один тип объединения в другой - это то же самое, что перечислить любой другой допустимый тип. Однако объединение не может рекурсивно перечислить свой собственный тип по той же причине, что type S struct { S } недействителен.

Союзы могут быть встроены в структуры.

Значение типа объединения - это динамический тип, ограниченный одним из перечисленных типов, и значение динамического типа, называемое сохраненным значением. Ровно один из перечисленных типов всегда является динамическим.

Нулевое значение пустого объединения уникально. Нулевое значение непустого объединения - это нулевое значение первого типа, указанного в объединении.

Значение для типа объединения, U , может быть создано с помощью U{} для нулевого значения. Если U имеет один или несколько типов и v является значением одного из перечисленных типов, T , U{v} создает значение объединения, в котором хранится v с динамическим типом T . Если v относится к типу, не указанному в U который может быть назначен более чем одному из перечисленных типов, для устранения неоднозначности требуется явное преобразование.

Значение типа объединения U может быть преобразовано в другой тип объединения V как в V(U{}) если набор типов в U является подмножеством набор типов в V . То есть, игнорируя порядок, U должен иметь все те же типы, что и V , а U не может иметь типы, не входящие в V но V может иметь типы не в U .

Присваиваемость между типами объединения определяется как преобразуемость, если определено (названо) не более одного из типов объединения.

Значение одного из перечисленных типов T типа объединения U может быть присвоено переменной типа объединения U . Это устанавливает динамический тип в T и сохраняет значение. Значения, совместимые с присваиванием, работают, как указано выше.

Если все перечисленные типы поддерживают операторы равенства:

  • операторы равенства могут использоваться для двух значений одного и того же типа объединения. Два значения типа объединения никогда не равны, если их динамические типы различаются.
  • значение этого объединения можно сравнить со значением любого из его перечисленных типов. Если динамический тип объединения не является типом другого операнда, == ложно, а != истинно независимо от сохраненного значения. Значения, совместимые с присваиванием, работают, как указано выше.
  • объединение может использоваться как ключ карты

Никакие другие операторы не поддерживаются для значений типа объединения.

Утверждение типа против типа объединения для одного из перечисленных в нем типов выполняется, если утверждаемый тип является динамическим типом.

Утверждение типа против типа объединения для типа интерфейса сохраняется, если его динамический тип реализует этот интерфейс. (Примечательно, что если все перечисленные типы реализуют этот интерфейс, утверждение всегда выполняется).

Переключатели типа должны быть исчерпывающими, включая все перечисленные типы, или содержать регистр по умолчанию.

Утверждения типа и переключатели типа возвращают копию сохраненного значения.

Отражение пакета потребует способ получить динамический тип и сохраненное значение отраженного значения объединения, а также способ получить перечисленные типы отраженного типа объединения.

Примечания:

Синтаксис union{...} был выбран частично, чтобы отличаться от предложения типа суммы в этом потоке, в первую очередь для сохранения хороших свойств в грамматике Go и, между прочим, для подтверждения того, что это размеченное объединение. Как следствие, это допускает несколько странные объединения, такие как union{} и union{ int } . Первый во многих смыслах эквивалентен struct{} (хотя по определению другого типа), поэтому он не добавляет к языку, кроме добавления еще одного пустого типа. Второй, возможно, более полезен. Например, type Id union { int } очень похож на type Id struct { int } за исключением того, что версия union допускает прямое присвоение без необходимости указывать idValue.int что позволяет ему больше походить на встроенный тип.

Преобразование, устраняющее неоднозначность, необходимое при работе с типами, совместимыми с присваиванием, немного сурово, но может обнаруживать ошибки, если объединение обновляется, чтобы внести неоднозначность, к которой нижестоящий код не готов.

Отсутствие встраивания является следствием разрешения методов для объединений и необходимости полного согласования в переключателях типов.

Разрешение методов в самом объединении, а не допустимое пересечение методов перечисленных типов, позволяет избежать случайного получения нежелательных методов. Тип, утверждающий сохраненное значение для общих интерфейсов, позволяет использовать простые явные методы-оболочки, когда требуется продвижение. Например, для типа объединения U все перечисленные типы которого реализуют fmt.Stringer :

func (u U) String() string {
  return u.(fmt.Stringer).String()
}

В связанной ветке Reddit rsc сказал:

Было бы странно использовать нулевое значение sum {X; Y} отличаться от суммы {Y; ИКС }. Суммы обычно работают не так.

Я думал об этом, так как это действительно применимо к любому предложению.

Это не ошибка: это особенность.

Рассмотреть возможность

type (
  Undefined = struct{}
  UndefinedOrInt union { Undefined; int }
)

против.

type (
  Illegal = struct{}
  IntOrIllegal union { int; Illegal }
)

UndefinedOrInt говорит, что по умолчанию он еще не определен, но, если это так, это будет значение int . Это аналогично *int как тип суммы (1 + int) теперь должен быть представлен в Go, и нулевое значение также аналогично.

IntOrIllegal другой стороны, *int но нулевое значение более выражает намерение, например, принуждение к тому, что оно по умолчанию равно new(int) .

Это похоже на возможность сформулировать поле типа bool в структуре как отрицательное, так что нулевое значение - это то, что вы хотите по умолчанию.

Оба нулевых значения сумм полезны и значимы сами по себе, и программист может выбрать наиболее подходящее для ситуации.

Если сумма представляет собой перечисление дней недели (каждый день является определенным struct{} ), то, что указано первым, является первым днем ​​недели, то же самое для перечисления в стиле iota .

Кроме того, я не знаю ни одного языка с типами суммы или размеченными / помеченными союзами, которые имеют концепцию нулевого значения. C будет самым близким, но нулевое значение - это неинициализированная память - вряд ли это наводит на мысль. Я полагаю, что в Java по умолчанию установлено значение null, но это потому, что все является ссылкой. Во всех других известных мне языках есть обязательные конструкторы типов для слагаемых, поэтому на самом деле нет понятия нулевого значения. Есть такой язык? Что оно делает?

Если разница от математических понятий «сумма» и «объединение» является проблемой, мы всегда можем назвать их как-то иначе (например, «вариант»).

По именам: Union сбивает с толку пуристов c / c ++. Вариант в основном знаком программистам на COBRA и COM, где, по-видимому, функциональные языки предпочитают размеченное объединение. Set - это глагол и существительное. Мне нравится ключевое слово _pick_. Лимбо использовал _pick_. Он короткий и описывает намерение типа выбирать из конечного набора типов.

Имя / синтаксис в значительной степени не имеет значения. Выбрать было бы хорошо.

Любое предложение в этой теме соответствует теоретико-множественному определению.

Первый тип, являющийся специальным для нулевого значения, не имеет значения, поскольку теоретико-типовые суммы коммутируют, поэтому порядок не имеет значения (A + B = B + A). Мое предложение поддерживает это свойство, но типы продуктов также теоретически коммутируют и на практике считаются разными для большинства языков (включая Go), поэтому это, вероятно, не является существенным.

@jimmyfrasche

Я лично считаю, что запрет на использование интерфейсов в качестве «выбираемых» членов является очень большим недостатком. Во-первых, это полностью отвергнет один из лучших вариантов использования типов «выбора» - наличие ошибки в одном из членов. Или вы хотите иметь дело с типом выбора, который имеет либо io.Reader, либо строку, если вы не хотите заранее заставлять пользователя использовать StringReader. Но в целом интерфейс - это просто еще один тип, и я считаю, что не должно быть ограничений по типу для «выбора» членов. В этом случае, если тип выбора имеет 2 элемента интерфейса, из которых один полностью окружен другим, это должно быть ошибкой времени компиляции, как упоминалось ранее.

Что мне действительно нравится в вашем встречном предложении, так это то, что методы могут быть определены для типа выбора. Я не думаю, что он должен обеспечивать перекрестное сечение методов членов, поскольку я не думаю, что будет много случаев, когда какие-либо методы будут принадлежать всем членам (и у вас в любом случае есть интерфейсы для этого). И исчерпывающий переключатель + случай по умолчанию - очень хорошая идея.

@rogpeppe @jimmyfrasche Я не вижу в ваших предложениях того, почему мы должны это сделать. Есть явный недостаток добавления нового типа: это новая концепция, которую каждый, кто использует Go, должен будет изучить. Какое компенсирующее преимущество? В частности, что дает нам новый тип типа, чего мы не получаем от типов интерфейса?

@ianlancetaylor Роберт хорошо резюмировал это здесь: https://github.com/golang/go/issues/19412#issuecomment -288608089

@ianlancetaylor
В конце концов, это делает код более читабельным, и это основная директива Go. Рассмотрим json.Token, в настоящее время он определен как интерфейс {}, однако в документации указано, что на самом деле он может быть только одним из определенного количества типов. Если же, с другой стороны, это написано как

type Token Delim | bool | float64 | Number | string | nil

Пользователь сможет сразу увидеть все возможности, а инструменты смогут автоматически создать исчерпывающий переключатель. Более того, компилятор также не позволит вам вставить туда неожиданный тип.

В конце концов, это делает код более читабельным, и это основная директива Go.

Больше возможностей означает, что нужно знать больше, чтобы понять код. Для человека со средним знанием языка его читабельность обязательно обратно пропорциональна количеству [недавно добавленных] функций.

@cznic

Больше возможностей означает, что нужно знать больше, чтобы понять код.

Не всегда. Если вы можете заменить «знать больше о языке» на «знать больше о плохо или непоследовательно задокументированных инвариантах в коде», это все равно может быть чистой выгодой. (То есть глобальные знания могут заменить потребность в местных знаниях.)

Если лучшая проверка типов во время компиляции действительно является единственным преимуществом, то мы можем получить очень похожее преимущество без изменения языка, введя комментарий, проверенный ветеринаром. Что-то вроде

//vet:types Delim | bool | float64 | Number | string | nil
type Token interface{}

В настоящее время у нас нет никаких комментариев ветеринаров, так что это не совсем серьезное предложение. Но я серьезно отношусь к основной идее: если единственное преимущество, которое мы получаем, - это то, что мы можем полностью сделать с помощью инструмента статического анализа, действительно ли стоит добавлять новую сложную концепцию в сам язык?

Многие, а возможно, и все тесты, выполняемые cmd / vet, могут быть добавлены к языку в том смысле, что они могут быть проверены компилятором, а не отдельным инструментом статического анализа. Но по разным причинам мы считаем полезным отделить vet от компилятора. Почему эта концепция относится скорее к языковой, чем к ветеринарной стороне?

@ianlancetaylor перепроверил комментарии: https://github.com/BurntSushi/go-sumtype

@ianlancetaylor, что касается оправданности изменения, я активно игнорировал это - или, скорее, отодвигал его назад. Говорить об этом абстрактно - расплывчато и мне это не помогает: для меня все это звучит как «хорошее - это хорошо, а плохое - плохо». Я хотел получить представление о том, каким на самом деле будет этот тип - каковы его ограничения, какие последствия он имеет, каковы плюсы, каковы недостатки - чтобы я мог увидеть, как он впишется в язык (или нет! ) и иметь представление о том, как я могу / мог бы использовать его в программах. Я думаю, что теперь хорошо представляю, какие типы суммы должны означать в Go, по крайней мере, с моей точки зрения. Я не совсем уверен, что они того стоят (даже если я хочу их очень сильно), но теперь, когда у меня есть что-то твердое для анализа с четко определенными свойствами, о которых я могу рассуждать. Я знаю, что на самом деле это не совсем ответ, но, по крайней мере, я так думаю.

Если лучшая проверка типов во время компиляции действительно является единственным преимуществом, то мы можем получить очень похожее преимущество без изменения языка, введя комментарий, проверенный ветеринаром.

Это все еще уязвимо для критики, связанной с необходимостью учиться новому. Если мне нужно узнать об этих волшебных комментариях ветеринара, чтобы отлаживать / понимать / использовать код, это ментальный налог, независимо от того, отнесем ли мы его к бюджету на языке Go или бюджету на техническом языке, не относящемся к Go. Во всяком случае, волшебные комментарии обходятся дороже, потому что я не знал, что мне нужно их выучить, когда думал, что выучил язык.

@cznic
Я не согласен. Исходя из вашего текущего предположения, вы не можете быть уверены, что человек тогда поймет, что такое канал или даже что такое функция. Тем не менее, эти вещи существуют в языке. И новая функция автоматически не означает, что она усложнит язык. В этом случае я бы сказал, что это фактически упростит понимание, потому что это сразу дает читателю понять, каким должен быть тип, в отличие от использования типа {} интерфейса черного ящика.

@ianlancetaylor
Я лично считаю, что эта функция больше связана с тем, чтобы сделать код более удобным для чтения и рассуждений. Безопасность времени компиляции - очень приятная функция, но не главная. Это не только сразу сделало бы сигнатуру типа более очевидной, но и облегчило бы ее последующее использование для понимания и написания. Людям больше не нужно будет прибегать к панике, если они получат тип, которого они не ожидали - это текущее поведение даже в стандартной библиотеке, поэтому им было бы легче подумать об использовании, не будучи обремененным неизвестным. . И я не думаю, что для этого стоит полагаться на комментарии и другие инструменты (даже если они являются собственными), потому что более чистый синтаксис более читабелен, чем такой комментарий. Комментарии не имеют структуры, и их гораздо проще испортить.

@ianlancetaylor

Почему эта концепция относится скорее к языковой, чем к ветеринарной стороне?

Вы можете применить тот же вопрос к любой функции за пределами полного по Тьюрингу ядра, и, возможно, мы не хотим, чтобы Go был «тьюринговым брезентом». С другой стороны, у нас есть примеры языков, которые вытеснили значительные подмножества реального языка в общий синтаксис «расширения». (Например, «атрибуты» в Rust, C ++ и GNU C.)

Основная причина помещать функции в расширения или атрибуты, а не в основной язык, состоит в том, чтобы сохранить совместимость синтаксиса, включая совместимость с инструментами, которые не знают о новой функции. (Действительно ли «совместимость с инструментами» работает на практике, сильно зависит от того, что на самом деле делает эта функция.)

В контексте Go кажется, что основная причина добавления функций в vet - это реализация изменений, которые не сохранят совместимость с Go 1, если их применить к самому языку. Я не считаю это проблемой.

Одна из причин не помещать функции в vet - это необходимость их распространения во время компиляции. Например, если я напишу:

switch x := somepkg.SomeFunc().(type) {
…
}

получу ли я правильные предупреждения для типов, которых нет в сумме, вне границ пакета? Для меня не очевидно, что vet может выполнять такой глубокий транзитивный анализ, так что, возможно, это причина, по которой необходимо перейти на базовый язык.

@ dr2chase В целом, конечно, вы правы, но верны ли вы в этом конкретном примере? Код вполне понятен, даже если мы не знаем, что означает волшебный комментарий. Магический комментарий никоим образом не меняет того, что делает код. Сообщения об ошибках от ветеринара должны быть четкими.

@bcmills

Почему эта концепция относится скорее к языковой, чем к ветеринарной стороне?

Вы можете применить тот же вопрос к любой функции за пределами полного по Тьюрингу ядра ...

Я не согласен. Если обсуждаемая функция влияет на скомпилированный код, то есть автоматический аргумент в пользу этого. В этом случае функция явно не влияет на скомпилированный код.

(И да, ветеринар может проанализировать источник импортированных пакетов.)

Я не пытаюсь утверждать, что мои аргументы насчет ветеринара убедительны. Но каждое изменение языка начинается с отрицательной позиции: простой язык очень желателен, а такая важная новая функция неизбежно делает язык более сложным. Вам нужны веские аргументы в пользу смены языка. И, с моей точки зрения, этих веских аргументов пока нет. В конце концов, мы долго думали над этой проблемой, и это FAQ (https://golang.org/doc/faq#variant_types).

@ianlancetaylor

В этом случае функция явно не влияет на скомпилированный код.

Я думаю, это зависит от конкретных деталей? "Нулевое значение суммы - это нулевое значение первого типа" поведения, которое @jimmyfrasche упомянул выше (https://github.com/golang/go/issues/19412#issuecomment-289319916), безусловно, будет.

@urandom Я писал длинное объяснение того, почему типы интерфейса и объединения не смешиваются без явных конструкторов типов, но потом я понял, что есть какой-то разумный способ сделать это, так что:

Быстрое и грязное встречное предложение на мое встречное предложение. (Все, что прямо не упомянуто, совпадает с моим предыдущим предложением). Я не уверен, что одно предложение лучше другого, но это допускает интерфейсы и все более явное:

Объединение имеет явные «имена полей», в дальнейшем называемые «именами тегов»:

union { //or whatever
  None, invalid struct{} //None is zero value
  Good, Bad int
  Err error //okay because it's explicitly named
}

Встраивания по-прежнему нет. Наличие типа без имени тега всегда является ошибкой.

Значения Union имеют динамический тег, а не динамический тип.

Создание буквального значения: U{v} действительно только в том случае, если оно полностью однозначно, в противном случае оно должно быть U{Tag: v} .

Конвертируемость и совместимость присвоения также учитывают имена тегов.

Присвоение к союзу - это не волшебство. Это всегда означает присвоение совместимого значения объединения. Чтобы установить сохраненное значение, желаемое имя тега должно быть явно использовано: v.Good = 1 устанавливает для динамического тега значение Good, а для сохраненного значения - 1.

Для доступа к сохраненному значению используется утверждение тега, а не утверждение типа:

g := v.[Tag] //may panic
g, ok := v.[Tag] //no panic but could return zero-value, false

v.Tag - это ошибка справа, поскольку она неоднозначна.

Переключатели тегов похожи на переключатели типа, написанные switch v.[type] , за исключением того, что регистры являются тегами объединения.

Утверждения типа остаются в силе по отношению к типу динамического тега. Переключатели типа работают аналогично.

Даны значения a, b некоторого типа объединения, a == b, если их динамические теги одинаковы и сохраненное значение одинаково.

Проверка того, является ли сохраненное значение некоторым конкретным значением, требует утверждения тега.

Если имя тега не экспортировано, его можно установить и получить к нему доступ только в пакете, который определяет объединение. Это означает, что переключатель тега объединения со смешанными экспортируемыми и неэкспортированными тегами никогда не может быть исчерпывающим за пределами определяющего пакета без регистра по умолчанию. Если все теги не экспортированы, это черный ящик.

Отражение также требует дескриптора имен тегов.

e: Разъяснение для вложенных объединений. Данный

type U union {
  A union {
    A1 T1
    A2 T2
  }
  B union {
    B1 T3
    B2 T4
  }
}
var u U

Значение u - это динамический тег A, а сохраненное значение - это анонимное объединение с динамическим тегом A1, а его сохраненное значение - нулевое значение T1.

u.B.B2 = returnsSomeT3()

- это все, что необходимо для переключения u с нулевого значения, даже если он перемещается от одного из вложенных объединений к другому, поскольку все они хранятся в одной ячейке памяти. Но

v := u.[A].[A2]

имеет два шанса запаниковать, поскольку тег утверждает два значения объединения, а двухзначная версия утверждения тега недоступна без разделения на несколько строк. В этом случае переключатели вложенных тегов будут более чистыми.

edit2: Разъяснение по утверждениям типа.

Данный

type U union {
  Exported, unexported int
}
var u U

утверждение типа u.(int) вполне разумно. В рамках определяющего пакета это всегда будет выполняться. Однако, если u находится за пределами определяющего пакета, u.(int) вызовет панику, когда динамический тег равен unexported чтобы избежать утечки деталей реализации. Аналогично для утверждений к типу интерфейса.

@ianlancetaylor Вот несколько примеров того, как эта функция может помочь:

  1. В основе некоторых пакетов (например, go/ast ) лежит один или несколько типов больших сумм. Трудно ориентироваться в этих пакетах, не понимая их типов. Что еще более сбивает с толку, иногда тип суммы представлен интерфейсом с методами (например, go/ast.Node ), а иногда - пустым интерфейсом (например, go/ast.Object.Decl ).

  2. Компиляция функции protobuf oneof в Go приводит к неэкспортируемому типу интерфейса, единственная цель которого - убедиться, что присвоение полю oneof является типобезопасным. Это, в свою очередь, требует создания типа для каждой ветви oneof. Литералы типа для конечного продукта трудно читать и писать:

    &sppb.Mutation{
               Operation: &sppb.Mutation_Delete_{
                   Delete: &sppb.Mutation_Delete{
                       Table:  m.table,
                       KeySet: keySetProto,
                   },
               },
    }
    

    Некоторые (хотя и не все) oneofs могут быть выражены типами суммы.

  3. Иногда тип «может быть» - это именно то, что нужно. Например, многие операции обновления ресурсов Google API позволяют изменять подмножество полей ресурса. Один из естественных способов выразить это в Go - это вариант структуры ресурса с типом «возможно» для каждого поля. Например, ресурс ObjectAttrs Google Cloud Storage выглядит так:

    type ObjectAttrs struct {
       ContentType string
       ...
    }
    

    Для поддержки частичных обновлений в пакете также определены

    type ObjectAttrsToUpdate struct {
       ContentType optional.String
       ...
    }
    

    Где optional.String выглядит так ( годок ):

    // String is either a string or nil.
    type String interface{}
    

    Это сложно объяснить и небезопасно для типов, но на практике это оказывается удобным, потому что литерал ObjectAttrsToUpdate выглядит точно так же, как литерал ObjectAttrs при кодировании присутствия. Я бы хотел, чтобы мы могли написать

    type ObjectAttrsToUpdate struct {
       ContentType string | nil
       ...
    }
    
  4. Многие функции возвращают (T, error) с семантикой xor (T имеет смысл, если ошибка равна нулю). Запись типа возврата как T | error прояснит семантику, повысит безопасность и предоставит больше возможностей для композиции. Даже если мы не можем (по соображениям совместимости) или не хотим изменять возвращаемое значение функции, тип суммы по-прежнему полезен для переноса этого значения, например, для записи его в канал.

По общему признанию, аннотация go vet могла бы помочь во многих из этих случаев, но не в тех, где анонимный тип имеет смысл. Я думаю, если бы у нас были типы сумм, мы бы увидели много

chan *Response | error

Этот тип достаточно короткий, чтобы писать несколько раз.

@ianlancetaylor, вероятно, это не

(Используя мое последнее предложение с тегами для синтаксиса / семантики, приведенного ниже. Также предполагается, что генерируемый код в основном похож на код C, который я опубликовал намного раньше в потоке.)

Типы сумм перекрываются йотой, указателями и интерфейсами.

йота

Эти два типа примерно эквивалентны:

type Stoplight union {
  Green, Yellow, Red struct {}
}

func (s Stoplight) String() string {
  switch s.[type] {
  case Green: return "green" //etc
  }
}

а также

type Stoplight int

const (
  Green Stoplight = iota
  Yellow
  Red
)

func (s Stoplight) String() string {
  switch s {
  case Green: return "green" //etc
  }
}

Компилятор, скорее всего, выдаст один и тот же код для обоих.

В версии union int превращается в скрытую деталь реализации. В версии iota вы можете спросить, что такое желтый / красный, или установить значение Stoplight на -42, но не для версии union - все это ошибки компилятора и инварианты, которые можно учесть во время оптимизации. Точно так же вы можете написать переключатель (значение), который не учитывает желтые огни, но с переключателем тега вам понадобится случай по умолчанию, чтобы сделать это явным.

Конечно, есть вещи, которые можно сделать с йотой, чего нельзя сделать с типами профсоюзов.

указатели

Эти два типа примерно эквивалентны

type MaybeInt64 union {
  None struct{}
  Int64 int64
}

а также

type MaybeInt64 *int64

Версия указателя более компактна. В версии union потребуется дополнительный бит (который, в свою очередь, будет иметь размер слова) для хранения динамического тега, поэтому размер значения, вероятно, будет таким же, как https://golang.org/pkg/database/sql/ # NullInt64

Версия союза более четко документирует намерение.

Конечно, с указателями можно делать то, что нельзя делать с типами объединения.

интерфейсы

Эти два типа примерно эквивалентны

type AB union {
  A A
  B B
}

а также

type AB interface {
  secret()
}
func (A) secret() {}
func (B) secret() {}

Объединенную версию нельзя обойти с помощью встраивания. A и B не нуждаются в общих методах - на самом деле они могут быть примитивными типами или иметь полностью непересекающиеся наборы методов, как в опубликованном примере json.Token @urandom .

Действительно легко увидеть, что вы можете поместить в объединение AB по сравнению с интерфейсом AB: определение - это документация (мне приходилось читать исходный код go / ast несколько раз, чтобы понять, что это такое).

Объединение AB никогда не может быть нулевым, и ему могут быть предоставлены методы за пределами пересечения его составляющих (это можно смоделировать, встраивая интерфейс в структуру, но тогда конструкция становится более сложной и подверженной ошибкам).

Конечно, есть вещи, которые можно делать с интерфейсами, чего нельзя делать с типами объединения.

Резюме

Может быть, это перекрытие слишком много.

В каждом случае основным преимуществом версий union действительно является более строгая проверка времени компиляции. То, что вы не можете сделать, важнее того, что вы можете. Для компилятора, который переводит в более сильные инварианты, которые он может использовать для оптимизации кода. Для программиста это означает другое, о чем вы можете позволить компилятору беспокоиться - он просто скажет вам, если вы ошибаетесь. В версии интерфейса, по крайней мере, есть важные преимущества документации.

Неуклюжие версии примеров йоты и указателя могут быть созданы с использованием стратегии «интерфейс с неэкспортированным методом». Однако в этом отношении структуры можно моделировать с помощью map[string]interface{} и (непустых) интерфейсов с типами функций и значениями методов. Никто бы не стал, потому что так сложнее и менее безопасно.

Все эти функции добавляют что-то к языку, но их отсутствие можно обойти (болезненно и под протестом).

Итак, я предполагаю, что панель не для демонстрации программы, которую невозможно даже приблизить в Go, а для демонстрации программы, которую на Go гораздо проще и понятнее написать с объединениями, чем без них. Итак, что еще предстоит показать, так это это.

@jimmyfrasche

Я не вижу причин, по которым тип объединения должен иметь именованные поля. Имена полезны только в том случае, если вы хотите различать разные поля одного и того же типа. Однако в объединении никогда не должно быть нескольких полей одного типа, поскольку это совершенно бессмысленно. Таким образом, наличие имен просто излишне и приводит к путанице и большему набору текста.

По сути, ваш тип объединения должен выглядеть примерно так:

union {
    struct{}
    int
    err
}

Сами типы будут предоставлять уникальные идентификаторы, которые можно использовать для присвоения объединению, очень похоже на то, как встроенные типы в структурах используются в качестве идентификаторов.

Однако для того, чтобы явные назначения работали, нельзя создать тип объединения, указав безымянный тип в качестве члена, поскольку синтаксис допускает такое выражение. Например, v.struct{} = struct{}

Таким образом, такие типы, как raw struct, unions и funcs, должны быть названы заранее, чтобы стать частью объединения и стать присваиваемыми. Имея это в виду, вложенное объединение не будет чем-то особенным, поскольку внутреннее объединение будет просто другим типом члена.

Я не уверен, какой синтаксис лучше.

[union|sum|pick|oneof] {
    type1
    package1.type2
    ....
}

Вышеупомянутое кажется более стандартным, но немного многословным для такого типа.

С другой стороны, type1 | package1.type2 может не выглядеть как ваш обычный тип го, однако он получает преимущество от использования символа '|' символ, который преимущественно распознается как ИЛИ. И это уменьшает многословие, не будучи загадочным.

@urandom, если у вас нет "имен тегов", но разрешены интерфейсы, суммы сворачиваются в interface{} с дополнительными проверками. Они перестают быть типами сумм, так как вы можете вставить что-то одно, но получить его несколькими способами. Имена тегов позволяют им быть типами сумм и содержать интерфейсы без двусмысленности.

Однако имена тегов решают гораздо больше, чем просто проблему интерфейса {}. Они делают этот шрифт менее волшебным и позволяют всему быть великолепно явным, не изобретая кучу типов, чтобы различать. Как вы отметили, у вас может быть явное присваивание и литералы типа.

То, что вы можете присвоить одному типу более одного тега, является особенностью. Рассмотрим тип, чтобы измерить, сколько успехов или неудач произошло подряд (1 успех отменяет N неудач и наоборот)

type Counter union {
  Successes, Failures uint 
}

без нужных вам имен тегов

type (
  Success uint
  Failures uint
  Counter Successes | Failures
)

и присвоение будет выглядеть как c = Successes(1) вместо c.Successes = 1 . Вы не получите многого.

Другой пример - тип, представляющий локальный или удаленный сбой. С помощью имен тегов это легко смоделировать:

type Failure union {
  Local, Remote error
}

Источник ошибки можно указать с помощью имени тега, независимо от того, что это за ошибка. Без имен тегов вам понадобится type Local { error } и то же самое для удаленного, даже если вы разрешите интерфейсы непосредственно в сумме.

Имена тегов как бы создают особые ни псевдонимы, ни именованные типы локально в объединении. Наличие нескольких «тегов» с одинаковыми типами не является уникальным для моего предложения: это то, что делает каждый функциональный язык (о котором я знаю).

Возможность создавать неэкспортированные теги для экспортируемых типов и наоборот - тоже интересный поворот.

Кроме того, наличие отдельных утверждений тега и типа позволяет использовать некоторый интересный код, например, возможность продвигать общий метод в объединение с помощью однострочной оболочки.

Кажется, что это решает больше проблем, чем вызывает, и делает все вместе намного лучше. Честно говоря, я не был так уверен, когда писал об этом, но я все больше убеждаюсь, что это единственный способ решить все проблемы с интеграцией сумм в Go.

Чтобы немного расширить это, я мог бы привести пример из io.Reader | io.ReadCloser . Допускаются интерфейсы без тегов, это тот же тип, что и io.Reader .

Вы можете вставить ReadCloser и вытащить его как Reader. Вы теряете A | B означает свойство типа суммы A или B.

Если вам нужно уточнить, как иногда обрабатывать io.ReadCloser как io.Reader вам необходимо создать структуры оболочки, как указано в type Reader struct { io.Reader } и т. Д. И иметь тип Reader | ReadCloser .

Даже если вы ограничите суммы интерфейсами с непересекающимися наборами методов, у вас все равно будет эта проблема, потому что один тип может реализовать более одного из этих интерфейсов. Вы теряете ясность типов суммы: они не «A или B»: они «A или B, или иногда как вам нравится».

Хуже того, если эти типы принадлежат другим пакетам, они могут внезапно вести себя по-другому после обновления, даже если вы были очень осторожны при построении своей программы, чтобы A никогда не обрабатывался так же, как B.

Первоначально я исследовал запрет на использование интерфейсов для решения проблемы. Это никого не устраивало! Но это также не избавило от таких проблем, как a = b означающих разные вещи в зависимости от типов a и b, что мне не нравится. Также должно было быть много правил о том, какой тип выбирается при выборе, когда в игру вступает возможность присваивания типа. Это много волшебства.

Вы добавляете теги, и все уходит.

С помощью union { R io.Reader | RC io.ReadCloser } вы можете явно сказать, что я хочу, чтобы этот ReadCloser считался читателем, если это имеет смысл. Типы обертки не требуются. Это подразумевается в определении. Независимо от типа тега, это либо один тег, либо другой.

Обратной стороной является то, что, если вы получаете io.Reader откуда-то еще, скажем, вызов chan receive или func, и это может быть io.ReadCloser, и вам нужно назначить его правильному тегу, который вы должны ввести assert в io. Прочтите Closer и проверьте. Но это делает цель программы намного более ясной - именно то, что вы имеете в виду, находится в коде.

Кроме того, поскольку утверждения тега отличаются от утверждений типа, если вам действительно все равно, и вы просто хотите, чтобы io.Reader независимо, вы можете использовать утверждение типа, чтобы вытащить это, независимо от тега.

Это лучшая транслитерация игрушечного примера в Go без объединений, сумм и т. Д. Вероятно, это не лучший пример, но я использовал его, чтобы увидеть, как это будет выглядеть.

Он показывает семантику в более функциональном виде, который, вероятно, будет легче понять, чем некоторые краткие пункты в предложении.

В транслитерации довольно много шаблонов, поэтому я обычно писал только первые экземпляры нескольких методов с примечанием о повторении.

In Go с предложением профсоюзов:

type fail union { //zero value: (Local, nil)
  Local, Remote error
}

func (f fail) Error() string {
  //Could panic if local/remote nil, but assuming
  //it will be constructed purposefully
  return f.(error).Error()
}

type U union { //zero value: (A, "")
  A, B, C string
  D, E    int
  F       fail
}

//in a different package

func create() pkg.U {
  return pkg.U{D: 7}
}

func process(u pkg.U) {
  switch u := u.[type] {
  case A:
    handleA(u) //undefined here, just doing something with unboxed value
  case B:
    handleB(u)
  case C:
    handleC(u)
  case D:
    handleD(u)
  case E:
    handleE(u)
  case F:
    switch u := u.[type] {
    case Local:
      log.Fatal(u)
    case Remote:
      log.Printf("remote error %s", u)
      retry()
    } 
  }
}

Транслитерировано на текущий Go:

(включены примечания о различиях между транслитерацией и приведенным выше)

const ( //simulates tags, namespaced so other packages can see them without overlap
  Fail_Local = iota
  Fail_Remote
)

//since there are only two tags with a single type this can
//be represented precisely and safely
//the error method on the full version of fail can be
//put more succinctly with type embedding in this case

type fail struct { //zero value (Fail_Local, nil) :)
  remote bool
  error
}

// e, ok := f.[Local]
func (f *fail) TagAssertLocal2() (error, bool) { //same for TagAssertRemote2
  if !f.remote {
    return nil, false
  }
  return f.error, true
}

// e := f.[Local]
func (f *fail) TagAssertLocal() error { //same for TagAssertRemote
  if !f.remote {
    panic("invalid tag assert")
  }
  return f.error
}

// f.Local = err
func (f *fail) SetLocal(err error) { //same for SetRemote
  f.remote = false
  f.error = err
}

// simulate tag switch
func (f *fail) TagSwitch() int {
  if f.remote {
    return Fail_Remote
  }
  return Fail_Local
}

// f.(someType) needs to be written as f.TypeAssert().(someType)
func (f *fail) TypeAssert() interface{} {
  return f.error
}

const (
  U_A = iota
  U_B
  // ...
  U_F
)

type U struct { //zero value (U_A, "", 0, fail{}) :(
  kind int //more than two types, need an int
  s string //these would all occupy the same space
  i int
  f fail
}

//s, ok := u.[A]
func (u *U) TagAssertA2() (string, bool) { //similar for B, etc.
  if u.kind == U_A {
    return u.s, true
  }
  return "", false
}

//s := u.[A]
func (u *U) TagAssertA() string { //similar for B, etc.
  if u.kind != U_A {
    panic("invalid tag assert")
  }
  return u.s
}

// u.A = s
func (u *U) SetA(s string) { //similar for B, etc.
  //if there were any pointers or reference types
  //in the union, they'd have to be nil'd out here,
  //since the space isn't shared
  u.kind = U_A
  u.s = s
}

// special case of u.F.Local = err
func (u *U) SetF_Local(err error) { //same for SetF_Remote
  u.kind = U_F
  u.f.SetLocal(err)
}

func (u *U) TagSwitch() int {
  return u.kind
}

func (u *U) TypeAssert() interface{} {
  switch u.kind {
  case U_A, U_B, U_C:
    return u.s
  case U_D, U_E:
    return u.i
  }
  return u.f
}

//in a different package

func create() pkg.U {
  var u pkg.U
  u.SetD(7)
  return u
}

func process(u pkg.U) {
  switch u.TagSwitch() {
  case U_A:
    handleA(u.TagAssertA())
  case U_B:
    handleB(u.TagAssertB())
  case U_C:
    handleC(u.TagAssertC())
  case U_D:
    handleD(u.TagAssertD())
  case U_E:
    handleE(u.TagAssertE())
  case U_F:
    switch u := u.TagAssertF(); u.TagSwitch() {
    case Fail_Local:
      log.Fatal(u.TagAssertLocal())
    case Fail_Remote:
      log.Printf("remote error %s", u.TagAssertRemote())
    }
  }
}

@jimmyfrasche

Поскольку объединение содержит теги, которые могут иметь один и тот же тип, не лучше ли подходит следующий синтаксис:

func process(u pkg.U) {
  switch v := u {
  case A:
    handleA(v) //undefined here, just doing something with unboxed value
  case B:
    handleB(v)
  case C:
    handleC(v)
  case D:
    handleD(v)
  case E:
    handleE(v)
  case F:
    switch w := v {
    case Local:
      log.Fatal(w)
    case Remote:
      log.Printf("remote error %s", w)
      retry()
    } 
  }
}

На мой взгляд, при использовании с переключателем объединение очень похоже на такие типы, как int или string. Основное отличие состоит в том, что ему могут быть присвоены только конечные «значения», в отличие от предыдущих типов, а сам переключатель является исчерпывающим. Таким образом, в данном случае я особо не вижу необходимости в особом синтаксисе, снижающем умственную работу разработчика.

Кроме того, в соответствии с этим предложением будет действителен такой код:

type Foo union {
    // Completely different types, no ambiguity
    A string
    B int
}

func Bar(f Foo) {
    switch v := f {
        ....
    }
}

....

func main() {
    // No need for Bar(Foo{A: "hello world"})
    Bar("hello world")
    Bar(1)
}

@urandom Я выбрал синтаксис, отражающий семантику, по возможности используя аналогии с существующим синтаксисом Go.

С типами интерфейсов вы можете делать

var i someInterface = someValue //where someValue implements someInterface.
var j someInterface = i //this assignment is different from the last one.

Это хорошо и недвусмысленно, поскольку не имеет значения, какой тип someValue пока выполняется контракт.

Когда вы вводите теги † в объединениях, это иногда может быть двусмысленным. Магическое назначение будет действовать только в определенных случаях. Специальная оболочка только иногда заставляет вас быть откровенным.

Я не вижу смысла в возможности иногда пропускать шаг, особенно когда изменение кода может легко сделать недействительным этот особый случай, а затем вам все равно придется вернуться и обновить весь код. Чтобы использовать ваш пример Foo / Bar, если C int добавляется к Foo тогда нужно изменить Bar(1) , но не Bar("hello world") . Это усложняет все, чтобы сэкономить несколько нажатий клавиш в ситуациях, которые могут быть не такими распространенными, и затрудняет понимание концепций, потому что иногда они выглядят так, а иногда они выглядят так - просто обратитесь к этой удобной блок-схеме, чтобы узнать, что подходит именно вам!

† Хотел бы я придумать для них лучшее название. Структурные теги уже есть. Я бы назвал их лейблами, но в Go они тоже есть. Называть их полями кажется более подходящим и наиболее запутанным. Если кто-то хочет покататься на велосипеде, ему действительно пригодится свежее пальто.

В некотором смысле помеченные объединения больше похожи на структуру, чем на интерфейс. Это особый вид структуры, в которой одновременно может быть задано только одно поле. В этом свете ваш пример с Foo / Bar будет примерно таким:

type Foo struct {
  A string
  B int
}

func Bar(f Foo) {...}

func main() {
  Bar("hello world") //same as Bar(Foo{A: "hello world", B: 0})
  Bar(1) //same as Bar(Foo{A: "", B: 1})
}

Хотя в данном случае это однозначно, я не думаю, что это хорошая идея.

Также в предложении разрешено Bar(Foo{1}) если это однозначно, если вы действительно хотите сохранить нажатия клавиш. У вас также могут быть указатели на объединения, поэтому синтаксис составного литерала по-прежнему необходим для &Foo{"hello world"} .

Тем не менее, объединения имеют сходство с интерфейсами в том, что у них есть динамический тег, для которого в настоящее время установлено «поле».

switch v := u.[type] {... красиво отражает switch v := i.(type) {... для интерфейсов, но при этом позволяет переключать типы и утверждения непосредственно на значениях объединения. Может быть, это должно быть u.[union] чтобы его было легче обнаружить, но в любом случае синтаксис не такой уж тяжелый и понятно, что имеется в виду.

Вы можете привести тот же аргумент, что .(type) не нужны, но когда вы видите, что всегда точно знаете, что происходит, и, на мой взгляд, это полностью оправдывает это.

Это было моей причиной такого выбора.

@jimmyfrasche
Синтаксис переключателя кажется мне немного противоречащим интуиции, даже после ваших объяснений. В интерфейсе switch v := i.(type) {... переключает возможные типы, как указано в случаях переключения и обозначено .(type) .
Однако при объединении переключатель переключает не возможные типы, а значения. Каждый случай представляет различное возможное значение, причем значения могут фактически иметь один и тот же тип. Это больше похоже на строки и переключатели int, где case также перечисляет значения, а их синтаксис - простой switch v := u {... . Из этого мне кажется более естественным, что переключение значений объединения будет switch v := u { ... , поскольку случаи аналогичны, но более ограничительны, чем случаи для целых чисел и строк.

@urandom , это очень хороший момент по поводу синтаксиса. По правде говоря, это пережиток моего предыдущего предложения без ярлыков, так что тогда это был тип. Я просто слепо скопировал, не задумываясь. Спасибо, что указали на это.

switch u {... будет работать, но проблема с switch v := u {... том, что он слишком похож на switch v := f(); v {... (что затруднит создание отчетов об ошибках - неясно, что было задумано).

Если ключевое слово union было переименовано в pick как было предложено @as, то переключатель тега может быть записан как switch u.[pick] {... или switch v := u.[pick] {... что сохраняет симметрию. с переключателем типа, но теряет путаницу и выглядит довольно красиво.

Даже если реализация включает int, все еще существует неявная деструктуризация выбора в динамический тег и сохраненное значение, которое, как мне кажется, должно быть явным, независимо от грамматических правил.

вы знаете, просто вызов полей тегов и их использование assert и переключение полей имеет большой смысл.

edit: это сделало бы использование отражения с пиками неудобным, хотя

[Извините за задержку с ответом - меня не было в отпуске]

@ianlancetaylor написал:

Я не вижу в ваших предложениях того, почему мы должны это делать. Есть явный недостаток добавления нового типа: это новая концепция, которую каждый, кто использует Go, должен будет изучить. Какое компенсирующее преимущество? В частности, что дает нам новый тип типа, чего мы не получаем от типов интерфейса?

Я вижу два основных преимущества. Первое - это языковое преимущество; второй - преимущество в производительности.

  • при обработке сообщений, особенно при чтении из параллельного процесса, очень полезно знать полный набор сообщений, которые могут быть получены, потому что каждое сообщение может приходить с соответствующими требованиями протокола. Для данного протокола количество возможных типов сообщений может быть очень небольшим, но когда мы используем открытый интерфейс для представления сообщений, этот инвариант не ясен. Часто люди будут использовать разные каналы для каждого типа сообщений, чтобы избежать этого, но это связано с собственными издержками.

  • бывают случаи, когда имеется небольшое количество известных возможных типов сообщений, ни один из которых не содержит указателей. Если мы используем открытый интерфейс для их представления, нам нужно выделить выделение для создания значений интерфейса. Использование типа, ограничивающего возможные типы сообщений, означает, что этого можно избежать и, следовательно, уменьшить нагрузку на сборщик мусора и увеличить локальность кеша.

Особую боль для меня, которую могут решить типы суммы, - это годок. Возьмите, например, ast.Spec : https://golang.org/pkg/go/ast/#Spec

Многие пакеты вручную перечисляют возможные базовые типы именованного типа интерфейса, чтобы пользователь мог быстро получить представление, не глядя на код или полагаясь на суффиксы или префиксы имен.

Если язык уже знает все возможные значения, это можно автоматизировать в godoc же, как типы перечислений с iotas. Они также могут ссылаться на типы, а не быть просто текстом.

Изменить: еще один пример: https://github.com/mvdan/sh/commit/ebbfda50dfe167bee741460a4491ffec1006bdef

@mvdan - отличный практический способ улучшить историю в Go1 без каких-либо изменений языка. Можете ли вы подать для этого отдельный выпуск и сослаться на этот?

Извините, вы имеете в виду только ссылки на другие имена на странице godoc, но по-прежнему перечисляете их вручную?

Извините, должно было быть яснее.

Я имел в виду запрос функции для автоматической обработки типов, реализующих интерфейсы, определенные в текущем пакете в godoc.

(Я считаю, что где-то есть запрос функции для связывания перечисленных вручную имен, но у меня сейчас нет времени искать его).

Я не хочу заниматься этим (уже очень длинным) потоком, поэтому создал отдельную проблему - см. Выше.

@Merovius Я отвечаю на https://github.com/golang/go/issues/19814#issuecomment -298833986 в этом выпуске, поскольку материал AST больше относится к типам сумм, чем к перечислениям. Приносим извинения за то, что втянули вас в другую проблему.

Во-первых, я хотел бы повторить, что я не уверен, принадлежат ли типы сумм к Go. Я еще не убедил себя, что они определенно не принадлежат. Я работаю в предположении, что они это делают, чтобы изучить идею и посмотреть, подходят ли они. Однако я готов быть убежденным в любом случае.

Во-вторых, в своем комментарии вы упомянули постепенное восстановление кода. Добавление нового члена к типу суммы по определению является критическим изменением наравне с добавлением нового метода в интерфейс или удалением поля из структуры. Но это правильное и желаемое поведение.

Давайте рассмотрим пример AST, реализованного с помощью интерфейса узла, который добавляет новый тип узла. Допустим, AST определен во внешнем проекте, и вы импортируете его в пакет в своем проекте, который проходит через AST.

Есть несколько случаев:

  1. Ваш код ожидает обхода каждого узла:
    1.1. У вас нет инструкции по умолчанию, ваш код молча неверен
    1.2. У вас есть оператор по умолчанию с паникой, ваш код не работает во время выполнения, а не во время компиляции (тесты не помогают, потому что они знают только об узлах, которые существовали, когда вы писали тесты)
  2. Ваш код проверяет только подмножество типов узлов:
    2.1. Этот новый вид узла все равно не входил бы в подмножество
    2.1.1. Пока этот новый узел никогда не содержит ни одного из интересующих вас узлов, все работает.
    2.1.2. В противном случае вы окажетесь в такой же ситуации, как если бы ваш код ожидал обхода каждого узла.
    2.2. Этот новый вид узла был бы в интересующем вас подмножестве, если бы вы знали о нем.

С интерфейсным AST корректно работает только случай 2.1.1. Это просто совпадение. Постепенное восстановление кода не работает. AST должен поднять свою версию, а ваш код должен поднять свою версию.

Линтер полноты может помочь, но поскольку линтер не может исследовать все типы интерфейсов, необходимо каким-то образом сообщить, что необходимо проверить конкретный интерфейс. Это означает либо комментарий в источнике, либо какой-то файл конфигурации в вашем репо. Если это комментарий в источнике, поскольку по определению AST определен в отдельном проекте, вы находитесь во власти этого проекта, чтобы пометить интерфейс для проверки полноты. Это работает в масштабе только при наличии единственного линтера полноты, с которым все сообщество соглашается и всегда использует.

При использовании AST на основе сумм вам по-прежнему необходимо использовать управление версиями. Единственное отличие в этом случае состоит в том, что линтер полноты встроен в компилятор.

Ни то, ни другое не помогает с 2.2, а что могло?

Есть более простой случай, примыкающий к AST, в котором могут быть полезны типы суммы: токены. Допустим, вы пишете лексический анализатор для более простого калькулятора. Существуют такие токены, как * которыми не связаны никакие значения, и такие токены, как Var , у которых есть строка, представляющая имя, и токены, такие как Val которые содержат float64 .

Вы могли бы реализовать это с помощью интерфейсов, но это было бы утомительно. Хотя вы, вероятно, сделали бы что-то вроде этого:

package token
type Type int
const (
  Times Type = iota
  // ...
  Var
  Val
)
type Value struct {
  Type
  Name string // only valid if Type == Var
  Number float64 // only valid if Type == Val
}

Линтер полноты в перечислениях на основе йоты может гарантировать, что недопустимый тип никогда не будет использоваться, но он не будет работать слишком хорошо против кого-то, кто присваивает Name, когда Type == Times или использует Number, когда Type == Var. По мере того, как количество и виды токенов растут, становится только хуже. На самом деле лучшее, что вы могли бы здесь сделать, - это добавить метод Valid() error , который проверяет все ограничения, и кучу документации, объясняющей, когда вы можете что-то делать.

Тип суммы легко кодирует все эти ограничения, и определение будет всей необходимой документацией. Добавление нового типа токена было бы критическим изменением, но все, что я сказал об AST, все еще применимо здесь.

Я думаю, что необходимо больше инструментов. Я просто не уверен, что этого достаточно.

@jimmyfrasche

Во-вторых, в своем комментарии вы упомянули постепенное восстановление кода. Добавление нового члена к типу суммы по определению является критическим изменением наравне с добавлением нового метода в интерфейс или удалением поля из структуры.

Нет, это не по номиналу. Вы можете сделать оба этих изменения в модели постепенного восстановления (для интерфейсов: 1. Добавить новый метод во все реализации, 2. Добавить метод в интерфейс. Для полей структуры: 1. Удалить все случаи использования поля, 2. Удалить поле). Добавление случая в тип суммы не может работать в модели постепенного ремонта; если вы добавите его сначала, сделайте lib, это сломает всех пользователей, так как они больше не проверяют исчерпывающе, но вы не можете сначала добавить его пользователям, потому что нового случая еще не существует. То же самое и с удалением.

Дело не в том, является ли это критическое изменение или нет, а в том, может ли это критическое изменение быть организовано с минимальным прерыванием.

Но это правильное и желаемое поведение.

Точно. Типы сумм по самому своему определению и по каждой причине, по которой они нужны людям, в корне несовместимы с идеей постепенного восстановления кода.

С интерфейсным AST корректно работает только случай 2.1.1.

Нет, он также работает правильно в случае 1.2 (сбой во время выполнения из-за нераспознанной грамматики - это нормально. Я, вероятно, не хотел бы паниковать, но просто возвращал ошибку), а также во многих случаях 2.1. Остальное - это фундаментальная проблема при обновлении программного обеспечения; если вы добавляете новую функцию в библиотеку, пользователям вашей библиотеки необходимо изменить код, чтобы использовать ее. Однако это не значит, что ваше программное обеспечение некорректно, пока оно не станет

AST должен поднять свою версию, а ваш код должен поднять свою версию.

Я вообще не понимаю, как это следует из того, что вы говорите. На мой взгляд, фраза «эта новая грамматика еще не будет работать со всеми инструментами, но доступна для компилятора» - это нормально. Точно так же, как «если вы запустите этот инструмент на этой новой грамматике, он выйдет из строя во время выполнения» - это нормально. В худшем случае это только добавляет еще один шаг к процессу постепенного восстановления: a) Добавьте новый узел в пакет AST и синтаксический анализатор. б) Исправьте инструменты, использующие пакет AST, чтобы воспользоваться преимуществами нового узла. c) Обновите код, чтобы использовать новый узел. Да, новый узел можно будет использовать только после выполнения а) и б); но на каждом этапе этого процесса, без каких-либо сбоев, все будет компилироваться и работать правильно.

Я не говорю, что вы автоматически будете в порядке в мире постепенного восстановления кода и отсутствия исчерпывающих проверок компилятора. Это по-прежнему потребует тщательного планирования и выполнения, вы все равно, вероятно, нарушите не поддерживаемые обратные зависимости, и все еще могут быть изменения, которые вы вообще не сможете сделать (хотя я не могу придумать ни одного). Но, по крайней мере, а) существует постепенный путь обновления и б) решение о том, должно ли это сломать ваш инструмент во время выполнения или нет, остается за автором инструмента. Они могут решить, что делать в неизвестном случае.

Линтер полноты может помочь, но поскольку линтер не может исследовать все типы интерфейсов, необходимо каким-то образом сообщить, что необходимо проверить конкретный интерфейс.

Почему? Я бы сказал, что для switchlint ™ нормально жаловаться на любое переключение типов без регистра по умолчанию; в конце концов, вы ожидаете, что код будет работать с любым определением интерфейса, поэтому отсутствие кода для работы с неизвестными реализациями в любом случае, вероятно, является проблемой. Да, из этого правила есть исключения, но исключения уже можно вручную игнорировать.

Я, вероятно, был бы более заинтересован в обеспечении соблюдения в компиляторе «каждый переключатель типа должен требовать регистр по умолчанию, даже если он пуст», чем с фактическими типами сумм. Это позволит и заставит людей принимать решение о том, что их код должен делать, когда они сталкиваются с неизвестным выбором.

Вы могли бы реализовать это с помощью интерфейсов, но это было бы утомительно.

пожать плечами, это

И FWIW, в настоящее время я только выступаю против концепции исчерпывающей проверки типов суммы. У меня пока нет твердого мнения о дополнительном удобстве выражения «любой из этих структурно определенных типов».

@Merovius Мне нужно будет подумать над вашими отличными

проверка полноты

В настоящее время я возражаю только против концепции исчерпывающей проверки типов суммы.

Вы можете явно отказаться от проверки полноты с помощью случая по умолчанию (ну, эффективно: значение по умолчанию делает его исчерпывающим, добавляя случай, который охватывает «все остальное, что бы это ни было»). У вас все еще есть выбор, но вы должны сделать это явно.

Я бы сказал, что для switchlint ™ нормально жаловаться на любое переключение типов без регистра по умолчанию; в конце концов, вы ожидаете, что код будет работать с любым определением интерфейса, поэтому отсутствие кода для работы с неизвестными реализациями в любом случае, вероятно, является проблемой. Да, из этого правила есть исключения, но исключения уже можно вручную игнорировать.

Это интересная идея. Хотя он будет попадать в типы сумм, смоделированные с помощью интерфейса, и перечисления, смоделированные с помощью const / iota, он не говорит вам, что вы пропустили известный случай, просто вы не справились с неизвестным случаем. Тем не менее, это кажется шумным. Рассмотреть возможность:

switch {
case n < 0:
case n == 0:
case n > 0:
}

Это исчерпывающе, если n целое (для чисел с плавающей запятой отсутствует n != n ), но без кодирования большого количества информации о типах, вероятно, проще просто пометить это как отсутствующее значение по умолчанию. Для чего-то вроде:

switch {
case p[0](a, b):
case p[1](a, b):
//...
case p[N](a, b):
}

даже если p[i] образуют отношение эквивалентности для типов a и b он не сможет это доказать, поэтому он должен пометить переключатель как отсутствующий по умолчанию case, что означает способ заглушить его с помощью манифеста, аннотации в источнике, сценария-оболочки для egrep -v из белого списка или ненужного значения по умолчанию на переключателе, которое ложно подразумевает, что p[i] не являются исчерпывающими.

В любом случае, реализовать этот линтер было бы тривиально, если выбрать вариант «всегда жаловаться на отсутствие дефолта при любых обстоятельствах». Было бы интересно сделать это и запустить его на ходу и посмотреть, насколько это шумно и / или полезно на практике.

жетоны

Альтернативные реализации токена:

//Type defined as before
type SimpleToken { Type }
type StringToken { Type; Value string }
type NumberToken { Type; Value float64 }
type Interface interface {
  //some method common to all these types, maybe just token() Interface
}

Это избавляет от возможности определения недопустимого состояния токена, когда что-то имеет как строковое, так и числовое значение, но не запрещает создание StringToken с типом, который должен быть SimpleToken или наоборот. наоборот.

Чтобы сделать это с интерфейсами, вам необходимо определить один тип для каждого токена ( type Plus struct{} , type Mul struct{} и т. Д.), И большинство определений в точности совпадают с именем типа. Одноразовое усилие или нет, это много работы (хотя в этом случае хорошо подходит для генерации кода).

Я предполагаю, что у вас может быть «иерархия» интерфейсов токенов для разделения типов токенов на основе допустимых значений: (Предполагая в этом примере, что существует более одного вида токенов, которые могут содержать число или строку и т. Д.)

type SimpleToken int //implements token.Interface
const (
  Plus SimpleToken = iota
  // ...
}
type NumericToken interface {
  Interface
  Value() float64
  nt() NumericToken
}
type IntToken struct { //implements NumericToken, and a FloatToken
type StringToken interface { // for Var and Func and Const, etc.
  Interface
  Value() string
  st() StringToken
}

Тем не менее, это означает, что каждый токен требует указания указателя для доступа к его значению, в отличие от типа struct или sum, который требует указателей только при использовании строк. Таким образом, с соответствующими линтерами и улучшениями в godoc большой выигрыш для типов суммы в этом случае связан с минимизацией выделения памяти при запрете недопустимых состояний и количества набора текста (в смысле клавиатуры), что не кажется неважным.

Вы можете явно отказаться от проверки полноты с помощью случая по умолчанию (ну, эффективно: значение по умолчанию делает его исчерпывающим, добавляя случай, который охватывает «все остальное, что бы это ни было»). У вас все еще есть выбор, но вы должны сделать это явно.

Таким образом, похоже, что в любом случае у нас обоих будет выбор: согласиться или отказаться от исчерпывающей проверки :)

он не говорит вам, что вы пропустили известный случай, просто вы не занимались неизвестным случаем.

По сути, я считаю, что компилятор уже проводит анализ всей программы, чтобы определить, какие конкретные типы используются в каких интерфейсах, как я думаю ? Я бы по крайней мере ожидал, что он, по крайней мере, для утверждений типа, не относящегося к интерфейсу (то есть для утверждений типа, которые не утверждают тип интерфейса, а конкретный тип), сгенерирует таблицы функций, используемые в интерфейсах во время компиляции.
Но, честно говоря, это утверждается из первых принципов, я понятия не имею о реальной реализации.

В любом случае должно быть довольно легко: а) перечислить любой конкретный тип, определенный во всей программе, и б) для любого переключателя типа отфильтровать их по тому, реализуют ли они этот интерфейс. Если вы воспользуетесь чем-то вроде этого , у вас будет надежный список. Думаю.

Я не уверен на 100% в том, что можно написать инструмент, который будет столь же надежен, как на самом деле явное указание вариантов, но я убежден, что вы можете охватить 90% случаев, и вы определенно можете написать инструмент, который сделает это за пределами компилятор, учитывая правильные аннотации (т.е. сделайте sum-типы комментарием, похожим на прагму, а не фактическим типом). По общему признанию, не лучшее решение.

Тем не менее, это кажется шумным. Рассмотреть возможность:

Я считаю это несправедливым. Упомянутые вами случаи не имеют ничего общего с типами сумм. Если бы я мог написать такой инструмент, я бы ограничился переключателями типов и переключателями с выражением, поскольку, похоже, именно так будут обрабатываться и типы сумм.

Альтернативные реализации токена:

Почему не маркер-метод? Вам не нужно поле типа, вы получаете его бесплатно из представления интерфейса. Если вас беспокоит повторение метода маркера снова и снова; определите неэкспортированную структуру {}, дайте ей этот метод маркера и вставьте его в каждую реализацию без дополнительных затрат и с меньшими затратами на набор параметров, чем ваш метод.

Тем не менее, это означает, что для каждого токена требуется указатель уважения для доступа к его значению.

да. Это реальная цена, но я не думаю, что она перевешивает любые другие аргументы.

Я считаю это несправедливым.

Это правда.

Я написал быструю версию и запустил ее в stdlib. Проверка любого оператора switch имела 1956 совпадений, ограничивая его пропуском формы switch { уменьшила это количество до 1677. Я не проверял ни одно из этих мест, чтобы увидеть, является ли результат значимым.

https://github.com/jimmyfrasche/switchlint

Безусловно, есть еще много возможностей для улучшения. Это не так уж сложно. Запросы на вытягивание приветствуются.

(На остальное отвечу позже)

изменить: неправильный формат разметки

Я думаю, что это (довольно предвзятый) обзор всего до сих пор (и нарциссически принимающий мое второе предложение)

Плюсы

  • лаконично, легко написать ряд ограничений лаконично и самодокументироваться
  • лучший контроль распределения
  • проще оптимизировать (все возможности известны компилятору)
  • исчерпывающая проверка (при желании можно отказаться)

Минусы

  • любое изменение элементов типа суммы является критическим изменением, запрещающим постепенное восстановление кода, если только все внешние пакеты не откажутся от проверок полноты
  • еще одна вещь на языке, которую нужно изучить, некоторое концептуальное совпадение с существующими функциями
  • сборщик мусора должен знать, какие члены являются указателями
  • неудобно для сумм вида 1 + 1 + ⋯ + 1

Альтернативы

  • йота "перечисление" для сумм вида 1 + 1 + ⋯ + 1
  • взаимодействует с методом неэкспортированного тега для более сложных сумм (возможно, сгенерированных)
  • или структура с iota enum и экстралингвистическими правилами о том, какие поля устанавливаются в зависимости от значения enums

Несмотря на

  • лучшая оснастка, всегда лучшая оснастка

Я думаю, что для постепенного ремонта, а это серьезного, единственный вариант для внешних пакетов - отказаться от проверки полноты. Это означает, что должно быть законным иметь "ненужный" случай по умолчанию, связанный только с будущей проверкой, даже если вы в остальном сопоставляете все остальное. Я считаю, что сейчас это неявно верно, и если это не так просто указать.

Может быть сообщение от сопровождающего пакета, что «эй, мы собираемся добавить нового члена к этому типу суммы в следующей версии, убедитесь, что вы можете справиться с этим», а затем инструмент switchlint может найти любые случаи, которые необходимо быть исключенным.

Не так просто, как в других случаях, но все же вполне выполнимо.

При написании программы, использующей внешне определенный тип суммы, вы можете закомментировать значение по умолчанию, чтобы убедиться, что вы не пропустили ни одного известного случая, а затем раскомментируйте его перед фиксацией. Или может быть инструмент, который сообщит вам, что значение по умолчанию «ненужно», что говорит вам, что вы знаете все и защищены от неизвестного в будущем.

Допустим, мы хотим включить проверку полноты с помощью линтера при использовании типов интерфейса, имитирующих типы сумм, независимо от того, в каком пакете они определены.

@Merovius, ваш betterSumType() BetterSumType очень крутой, но это означает, что переключение должно происходить в определяющем пакете (или вы выставляете что-то вроде

func CallBeforeSwitches(b BetterSumType) (BetterSumType, bool) {
    if b == nil {
        return nil, false
    }
    b = b.betterSumType()
    if b == nil {
        return nil, false
    }
    return b, true
}

а также линт, который вызывается каждый раз).

Каковы критерии, необходимые для проверки того, что все переключатели в программе являются исчерпывающими?

Это не может быть пустой интерфейс, потому что в этом случае игра - что угодно. Так что нужен хотя бы один метод.

Если интерфейс не имеет неэкспортированных методов, любой тип может реализовать его, поэтому полнота будет зависеть от всех пакетов в графе вызовов каждого переключателя. Можно импортировать пакет, реализовать его интерфейс, а затем отправить это значение одной из функций пакета; поэтому переключатель в этой функции не может быть исчерпывающим без создания цикла импорта. Так что нужен хотя бы один неэкспортированный метод. (Это включает предыдущий критерий).

Встраивание испортит свойство, которое мы ищем, поэтому нам нужно убедиться, что ни один из импортеров пакета никогда не встраивал интерфейс или какой-либо из типов, реализующих его, в любой момент. По-настоящему модный линтер может сказать, что иногда вложение - это нормально, если мы никогда не вызываем определенную функцию, которая создает встроенное значение, или ни один из встроенных интерфейсов никогда не «выходит за пределы» API-границ пакета.

Чтобы быть тщательными, нам нужно либо проверить, что нулевое значение интерфейса никогда не передается, либо также принудительно выполнить полную проверку переключателя case nil . (Последнее проще, но предпочтительнее первое, поскольку включение nil превращает сумму «типа A, или типа B, или типа C» в сумму «nil, или типа A, или типа B, или типа C»).

Допустим, у нас есть линтер со всеми этими возможностями, даже необязательными, который может проверять эту семантику для любого дерева импорта и любого заданного интерфейса в этом дереве.

Теперь предположим, что у нас есть проект с зависимостью D. Мы хотим убедиться, что интерфейс, определенный в одном из пакетов D, является исчерпывающим в нашем проекте. Скажем так.

Теперь нам нужно добавить новую зависимость в наш проект D ′. Если D 'импортирует пакет в D, который определил рассматриваемый тип интерфейса, но не использует этот линтер, он может легко уничтожить инварианты, которые необходимо сохранить, чтобы мы могли использовать исчерпывающие переключатели.

Если на то пошло, предположим, что D просто передал линтер случайно, а не потому, что его запустил сопровождающий. Обновление до D могло бы так же легко разрушить инварианты, как и D ′.

Даже если линтер может сказать «сейчас это на 100% исчерпывающий», это может измениться без каких-либо действий.

Проверка полноты для "перечислений йоты" кажется проще.

Для всех type t u где u является целым, а t используется как const либо с индивидуально указанными значениями, либо iota , так что ноль значение для u включено в эти константы.

Примечания:

  • Повторяющиеся значения можно рассматривать как псевдонимы и игнорировать в этом анализе. Предположим, что все именованные константы имеют разные значения.
  • 1 << iota может рассматриваться как набор мощности, я считаю, по крайней мере, большую часть времени, но, вероятно, потребует дополнительных условий, особенно окружающих побитовое дополнение. Пока они рассматриваться не будут.

Для краткости, давайте назовем min(t) константой, так что для любой другой константы C , min(t) <= C , и, аналогично, давайте назовем max(t) константу, например что для любой другой константы C , C <= max(t) .

Чтобы гарантировать исчерпывающее использование t , нам необходимо убедиться, что

  • значения t всегда являются именованными константами (или 0 в определенных идиоматических позициях, например, при вызове функции)
  • Не существует сравнений неравенства значений t , v за пределами min(t) <= v <= max(t)
  • значения t никогда не используются в арифметических операциях + , / и т. д. Возможное исключение может быть, когда результат ограничен между min(t) и max(t) сразу после этого, но это может быть трудно обнаружить в целом, поэтому может потребоваться аннотация в комментариях и, вероятно, следует ограничить пакет, который определяет t .
  • переключатели содержат все константы t или регистр по умолчанию.

Это по-прежнему требует проверки всех пакетов в дереве импорта и может быть так же легко признано недействительным, хотя менее вероятно, что оно станет недействительным в идиоматическом коде.

Насколько я понимаю, это, как и псевдонимы типов, не приведет к нарушению изменений, так зачем же откладывать его для Go 2?

Псевдонимы типов не вводят новое ключевое слово, что является несомненным критическим изменением. Также кажется, что существует мораторий даже на незначительные языковые изменения, и это было бы серьезным изменением. Даже простая модификация всех процедур маршалинга / демаршала для обработки значений отраженных сумм была бы огромным испытанием.

Псевдоним типа устраняет проблему, для которой не было решения. Суммарные типы обеспечивают преимущество в безопасности типов, но их отсутствие - не проблема.

Всего один (незначительный) замечание в пользу чего-то вроде оригинального предложения @rogpeppe . В пакете http есть тип интерфейса Handler и тип функции, который его реализует, HandlerFunc . Прямо сейчас, чтобы передать функцию в http.Handle , вы должны явно преобразовать ее в HandlerFunc . Если http.Handle вместо этого принимает аргумент типа HandlerFunc | Handler , он может принимать любую функцию / закрытие, напрямую назначаемую HandlerFunc . Объединение эффективно служит подсказкой типа, сообщающей компилятору, как значения с безымянными типами могут быть преобразованы в тип интерфейса. Поскольку HandlerFunc реализует Handler , в противном случае тип объединения будет вести себя точно так же, как Handler .

@griesemer в ответ на ваш комментарий в ветке перечисления https://github.com/golang/go/issues/19814#issuecomment -322752526, я думаю, мое предложение ранее в этой ветке https://github.com/golang/ go / issues / 19412 # issuecomment -289588569 решает вопрос о том, как типы сумм ("быстрые перечисления стилей") должны работать в Go. Как бы мне они ни нравились, я не знаю, станут ли они необходимым дополнением к Go, но я действительно думаю, что если бы они были добавлены, им пришлось бы выглядеть / работать так же.

Этот пост не завершен, и в этой ветке есть пояснения, до и после, но я не возражаю повторить эти моменты или резюмировать, поскольку эта ветка довольно длинная.

Если у вас есть тип суммы, имитируемый интерфейсом с тегом типа, и его невозможно обойти с помощью встраивания, это лучшая защита, которую я придумал: https://play.golang.org/p/FqdKfFojp-

@jimmyfrasche Я написал это некоторое время назад.

Другой возможный подход: https://play.golang.org/p/p2tFm984S8

@rogpeppe, если вы собираетесь использовать отражение, почему бы просто не использовать отражение?

Я написал исправленную версию своего второго предложения, основанную на комментариях здесь и в других вопросах.

В частности, я убрал проверку полноты. Однако внешнее средство проверки полноты написания для предложенного ниже предложения тривиально, хотя я не верю, что его можно написать для других типов Go, используемых для имитации типа суммы.

Изменить: я удалил возможность вводить assert для динамического значения значения выбора. Это слишком волшебно, и причиной для его разрешения служит генерация кода.

Edit2: разъяснено, как имена полей работают с утверждениями и переключателями, когда выбор определяется в другом пакете.

Edit3: ограниченное встраивание и уточненные неявные имена полей

Edit4: уточнить значение по умолчанию в переключателе

Типы подбора

Выбор - это составной тип, синтаксически похожий на структуру:

pick {
  A, B S
  C, D T
  E U "a pick tag"
}

В приведенном выше примере A , B , C , D и E - это имена полей выбора, а S , T и U - соответствующие типы этих полей. Имена полей могут быть экспортированы или не экспортированы.

Выбор не может быть рекурсивным без косвенного обращения.

Юридический

type p pick {
    //...
    p *p
}

Незаконный

type p pick {
    //...
    p p
}

Для пикировок нет встраивания, но пикировку можно встроить в структуру. Если выборка встроена в структуру, метод выбора повышается до структуры, а поля выбора - нет.

Тип без имени поля - это сокращение для определения поля с тем же именем, что и тип. (Это ошибка, если тип не имеет имени, за исключением *T где имя - T ).

Например,

type p pick {
    io.Reader
    io.Writer
    string
}

имеет три поля Reader , Writer и string с соответствующими типами. Обратите внимание, что поле string не экспортируется, даже если оно находится в области действия юниверса.

Значение типа выбора состоит из динамического поля и значения этого поля.

Нулевое значение типа выбора - это его первое поле в исходном порядке и нулевое значение этого поля.

Учитывая два значения одного и того же типа выбора, a и b , значение выбора может быть присвоено как любое другое значение.

a = b

Назначение значения без выбора, даже одного из типов одного из полей в выборе, является незаконным.

Тип выбора может иметь только одно динамическое поле в любой момент времени.

Синтаксис составного литерала аналогичен синтаксису структур, но с дополнительными ограничениями. А именно, литералы без ключа всегда недействительны, и можно указать только один ключ.

Следующие действительны

pick{A string; B int}{A: "string"} //value is (B, "string")
pick{A, B int}{B: 1} //value is (B, 1)
pick{A, B string}{} //value is (A, "")

Ниже приведены ошибки времени компиляции:

pick{A int; B string}{A: 1, B: "string"} //a pick can only have one value at a time
pick{A int; B uint}{1} //pick composite literals must be keyed

Для значения p типа pick {A int; B string} следующее присвоение

p.B = "hi"

устанавливает динамическое поле p на B и значение B на "привет".

Присвоение текущему динамическому полю обновляет значение этого поля. Присваивание, устанавливающее новое динамическое поле, должно обнулить все неуказанные ячейки памяти. Назначение полю выбора или структуры поля выбора обновляет или устанавливает динамическое поле по мере необходимости.

type P pick {
    A, B image.Point
}

var p P
fmt.Println(P) //{A: {0 0}}

p.A.X = 1 //A is the dynamic field, update
fmt.Println(P) //{A: {1 0}}

p.B.Y = 2 //B is not the dynamic value, create zero image.Point first
fmt.Println(P) //{B: {0 2}}

К значению, содержащемуся в пикировке, можно получить доступ только с помощью утверждения поля или переключателя поля.

x := p.[X] //panics if X is not the dynamic field of p
x, ok := p.[X] //returns the zero value of X and false if X is not the dynamic field of p

switch v := p.[var] {
case A:
case B, C: // v is only defined in this case if fields B and C have identical type names
case D:
default: // always legal even if all fields are exhaustively listed above
}

Имена полей в утверждениях полей и переключателях полей являются свойством типа, а не пакета, в котором он был определен. Они не квалифицируются и не могут быть уточнены именем пакета, определяющим pick .

Это действительно:

_, ok := externalPackage.ReturnsPick().[Field]

Это неверно:

_, ok := externalPackage.ReturnsPick().[externalPackage.Field]

Утверждения полей и переключатели полей всегда возвращают копию значения динамического поля.

Имена неэкспортированных полей могут быть заявлены только в их определяющем пакете.

Утверждения типа и переключатели типа также работают с пиками.

//removed, see note at top
//v, ok := p.(fmt.Stringer) //holds if the type of the dynamic field implements fmt.Stringer
//v, ok := p.(int) //holds if the type of the dynamic field is an int

Утверждения типа и переключатели типа всегда возвращают копию значения динамического поля.

Если выборка хранится в интерфейсе, утверждения типа для интерфейсов совпадают только с набором методов самого выбора. [все еще верно, но является избыточным, поскольку указанное выше было удалено]

Если все типы пикировки поддерживают операторы равенства, то:

  • значения этого выбора могут использоваться как ключи карты
  • два значения одного и того же выбора равны == если они имеют одно и то же динамическое поле и его значения равны ==
  • два значения с разными динамическими полями равны != даже если значения равны == .

Никакие другие операторы не поддерживаются для значений типа выбора.

Значение типа выбора P может быть преобразовано в другой тип выбора Q если набор имен полей и их типы в P является подмножеством имен полей и их типы в Q .

Если P и Q определены в разных пакетах и ​​имеют неэкспортированные поля, эти поля считаются разными независимо от имени и типа.

Пример:

type P pick {A int; B string}
type Q pick {B string; A int; C float64}

//legal
var p P
q := Q(p)

//illegal
var q Q
p := P(Q) //cannot handle field C

Возможность присвоения между двумя типами подбора определяется как конвертируемость, если определено не более одного из этих типов.

Методы могут быть объявлены для определенного типа выбора.

Я создал (и добавил в вики) отчет о впечатлениях https://gist.github.com/jimmyfrasche/ba2b709cdc390585ba8c43c989797325

Изменить: и: heart: @mewmew, который оставил гораздо лучший и более подробный отчет в ответ на эту суть

Что, если бы у нас был способ сказать для данного типа T список типов, которые можно преобразовать в тип T или присвоить переменной типа T ? Например

type T interface{} restrict { string, error }

определяет пустой тип интерфейса с именем T , так что единственными типами, которые могут быть назначены ему, являются string или error . Любая попытка присвоить значение любого другого типа приводит к ошибке времени компиляции. Теперь я могу сказать

func FindOrFail(m map[int]string, key int) T {
    if v, ok := m[key]; ok {
        return v
    }
    return errors.New("no such key")
}

func Lookup() {
    v := FindOrFail(m, key)
    if err, ok := v.(error); ok {
        log.Fatal(err)
    }
    s := v.(string) // This type assertion must succeed.
}

Какие ключевые элементы типов сумм (или типов выбора) не будут удовлетворены таким подходом?

s := v.(string) // This type assertion must succeed.

Это не совсем так, поскольку v также может быть nil . Чтобы исключить эту возможность, потребуется довольно серьезное изменение языка, поскольку это будет означать введение типов, не имеющих нулевых значений, и всего, что влечет за собой. Нулевое значение упрощает части языка, но также затрудняет разработку таких функций.

Интересно, что этот подход очень похож на исходное предложение @rogpeppe . Чего у него нет, так это принуждения к перечисленным типам, что может быть полезно в ситуациях, как я указывал ранее ( http.Handler ). Другое дело, что для этого требуется, чтобы каждый вариант был отдельным типом, поскольку варианты различаются по типу, а не по отдельному тегу. Я думаю, что это строго так выразительно, но некоторые люди предпочитают, чтобы теги вариантов и типы были разными.

@ianlancetaylor

плюсы

  • можно ограничиться закрытым набором типов - и это определенно главное
  • можно написать точную проверку полноты
  • вы получите свойство «вы можете присвоить этому свойству значение, удовлетворяющее условиям контракта». (Меня это не волнует, но я полагаю, что другие заботятся).

минусы

  • они просто взаимодействуют с преимуществами, а не совсем другого типа (хотя и приятные преимущества!)
  • у вас все еще есть nil, так что это не совсем тип суммы в теоретико-типовом смысле. Независимо A + B + C вы укажете 1 + A + B + C о котором у вас нет выбора. Как отметил @stevenblenkinsop , когда я работал над этим.
  • что еще более важно, из-за этого неявного указателя у вас всегда есть косвенное обращение. В предложении выбора вы можете выбрать p или *p дает вам больший контроль над компромиссами памяти. Вы не могли реализовать их как дискриминируемые объединения (в смысле C) в качестве оптимизации.
  • нет выбора нулевого значения, что является действительно хорошим свойством, тем более что в Go очень важно иметь как можно более полезное нулевое значение
  • по-видимому, вы не могли определить методы для T (но, предположительно, у вас были бы методы интерфейса, изменяемого ограничением, но типы в ограничении должны были бы удовлетворить его? В противном случае я не вижу смысла не просто имея type T restrict {string, error} )
  • если вы потеряете метки для полей / слагаемых / чего-то, то это сбивает с толку при взаимодействии с типами интерфейсов. Вы теряете сильное свойство типа суммы «именно то или иное». Вы можете вставить io.Reader и вытащить io.Writer . Это имеет смысл для (неограниченных) интерфейсов, но не для типов суммы.
  • Если вы хотите, чтобы два одинаковых типа означали разные вещи, вам нужно использовать типы-оболочки для устранения неоднозначности; такой тег должен быть во внешнем пространстве имен, а не ограничиваться типом, как поле структуры
  • это может быть слишком много для вашей конкретной формулировки, но похоже, что это меняет правила назначения в зависимости от типа правопреемника (я читаю это так, будто вы не можете назначить что-то, что можно назначить для error to T должно быть ошибкой).

Тем не менее, он проверяет основные параметры (первые два плюса, которые я перечислил), и я бы сразу принял его, если бы это все, что я мог получить. Но я надеюсь на лучшее.

Я предположил, что применяются правила утверждения типа. Таким образом, тип должен быть либо идентичен конкретному типу, либо присваиваться типу интерфейса. По сути, он работает точно так же, как интерфейс, но любое значение (кроме nil ) должно быть утверждено хотя бы для одного из перечисленных типов.

@jimmyfrasche
В вашем обновленном предложении возможно ли следующее присвоение, если все элементы типа относятся к разным типам:

type p pick {
    A int
    B string
}

func Foo(P p) {
}

var P p = 42
var Q p = "foo"

Foo(42)
Foo("foo")

Когда такие присваивания возможны, удобство использования типов сумм намного больше.

В предложении выбора вы можете выбрать p или *p дает вам больший контроль над компромиссами памяти.

Причина, по которой интерфейсы выделяют для хранения скалярных значений, заключается в том, что вам не нужно читать типовое слово, чтобы решить, является ли другое слово указателем; см. # 8405 для обсуждения. Те же соображения реализации, скорее всего, применимы к типу выбора, что на практике может означать, что p конечном итоге все равно будет распределяться и быть нелокальным.

@urandom нет, учитывая ваши определения, это должно быть написано

var p P = P{A: 42} // p := P{A: 42}
var q P = P{B: "foo")
Foo(P{A: 42}) // or Foo({A: 42}) if types can be elided here
Foo(P{B: "foo"})

Лучше думать о них как о структуре, в которой одновременно может быть установлено только одно поле.

Если у вас его нет, а затем вы добавляете C uint к p что происходит с p = 42 ?

Вы можете составить множество правил на основе порядка и возможности назначения, но они всегда означают, что изменения в определении типа могут иметь тонкие и драматические последствия для всего кода, использующего этот тип.

В лучшем случае изменение ломает весь код, полагаясь на отсутствие двусмысленности, и говорит, что вам нужно изменить его на p = int(42) или p = uint(42) перед повторной компиляцией. Изменение одной строки не должно требовать исправления сотни строк. Особенно, если эти строки находятся в пакетах людей в зависимости от вашего кода.

Вы должны либо быть на 100% откровенным, либо иметь очень хрупкий тип лица, к которому никто не сможет прикоснуться, потому что он может сломать все.

Это применимо к любому предложению типа суммы, но если есть явные метки, у вас все еще есть возможность присвоения, потому что метка явно указывает, какой тип назначается.

@josharian, поэтому, если я правильно это читаю, причина iface теперь всегда (*type, *value) вместо того, чтобы хранить значения размером с слово во втором поле, как это делал ранее Go, так что параллельному сборщику мусора не нужно проверять оба поля, чтобы увидеть, является ли второй указателем - он может просто предположить, что это всегда так. Я правильно понял?

Другими словами, если бы тип выбора был реализован (с использованием нотации C), например

struct {
    int which;
    union {
         A a;
         B b;
         C c;
    } summands;
}

сборщику мусора потребуется блокировка (или что-то необычное, но эквивалентное), чтобы проверить which чтобы определить, нужно ли сканировать summands ?

причина, по которой iface теперь всегда (* тип, * значение) вместо того, чтобы хранить значения размером со слово во втором поле, как это делал ранее Go, заключается в том, что параллельному сборщику мусора не нужно проверять оба поля, чтобы увидеть, является ли второе указателем - можно просто предположить, что так было всегда.

Верно.

Конечно, ограниченный характер типов выбора допускает некоторые альтернативные реализации. Тип выбора можно расположить так, чтобы всегда существовал согласованный образец указатель / не указатель; например, все скалярные типы могут перекрываться, а строковое поле может перекрываться с началом поля среза (потому что оба начинают «указатель, не указатель»). Так

pick {
  a uintptr
  b string
  c []byte
}

можно было бы выложить примерно:

[ word 1 (ptr) ] [ word 2 (non-ptr) ] [ word 3 (non-ptr) ]
[    <nil>         ] [                 a           ] [                              ]
[       b.ptr      ] [            b.len          ] [                              ]
[       c.ptr      ] [             c.len         ] [        c.cap             ]

но другие типы подборщиков могут не позволить такую ​​оптимальную упаковку. (Извините за сломанный ASCII, я не могу заставить GitHub правильно его отображать. Надеюсь, вы поняли суть.)

Эта способность делать статический макет может даже быть аргументом в пользу производительности в пользу включения типов выбора; Моя цель здесь - просто отметить для вас соответствующие детали реализации.

@josharian, и спасибо за это. Я не подумал об этом (честно говоря, я просто погуглил, существуют ли исследования о том, как GC дискриминирующие объединения, увидел, что да, вы можете это сделать, и решил, что это день - по какой-то причине мой мозг не ассоциировал "параллелизм" с "Go" в тот день: facepalm!).

Было бы меньше выбора, если бы один из типов был определенной структурой, у которой уже был макет.

Один из вариантов - не «уплотнять» слагаемые, если они содержат указатели, что означает, что размер будет таким же, как у эквивалентной структуры (+ 1 для дискриминатора int). Может быть, если возможно, использовать гибридный подход, чтобы все типы, которые могут использовать общий макет, работали.

Было бы обидно потерять хорошие свойства размера, но на самом деле это всего лишь оптимизация.

Даже если бы она всегда была 1 + размер эквивалентной структуры, даже если бы она не содержала указателей, она все равно имела бы все другие приятные свойства самого типа, включая контроль над распределениями. Со временем могут быть добавлены дополнительные оптимизации, которые, по крайней мере, будут возможны, как вы указываете.

type p pick {
    A int
    B string
}

Нужны ли А и Б там? Выбор выбирается из набора типов, так почему бы не выбросить полностью их имена идентификаторов:

type p pick {
    int
    string
}
q := p{string: "hello"}

Я считаю, что эта форма уже действительна для struct. Может быть ограничение, которое требуется для выбора.

@as, если имя поля опущено, оно совпадает с типом, поэтому ваш пример работает, но поскольку эти имена полей не экспортируются, они могут быть установлены / доступны только из определяющего пакета.

Имена полей должны быть там, даже если они неявно сгенерированы на основе имени типа, или есть плохие взаимодействия с возможностью назначения и типами интерфейса. Имена полей - это то, что заставляет его работать с остальной частью Go.

@ извиняюсь, я только что понял, что вы имели в виду нечто иное, чем то, что я читал.

Ваша формулировка работает, но тогда у вас есть вещи, которые выглядят как поля структуры, но ведут себя по-другому из-за обычных экспортируемых / неэкспортированных вещей.

Доступна ли строка извне пакета, определяющая p потому что она находится во вселенной?

Что о

type t struct {}
type P pick {
  t
  //other stuff
}

?

Отделив имя поля от имени типа, вы можете делать такие вещи, как

pick {
  unexported Exported
  Exported unexported
}

или даже

pick { Recoverable, Fatal error }

Если поля выбора ведут себя как поля структуры, вы можете использовать многое из того, что вы уже знаете о полях структуры, чтобы подумать о полях выбора. Единственная реальная разница в том, что единовременно может быть установлено только одно поле для выбора.

@jimmyfrasche
Go уже поддерживает встраивание анонимных типов внутри структур, поэтому ограничение области действия уже существует в языке, и я считаю, что проблема решается с помощью псевдонимов типов. Но признаю, я не придумал всех возможных вариантов использования. Кажется, все зависит от того, распространена ли эта идиома в Go:

package p
type T struct{
    Exported t
}
type t struct{}

Маленький _t_ существует в пакете, где он встроен в большой T , и только через такие экспортируемые типы.

@в качестве

Однако я не уверен, что полностью следую:

//with the option to have field names
pick { //T is in the namespace of the pick and the type isn't exposed to other packages
  T t
  //...
}

//without
type T = t //T has to be defined in the outer scope and now t is exposed to other packages
pick {
  T
  //...
}

Кроме того, если у вас есть только имя типа для метки, чтобы включить, скажем, []string вам нужно будет сделать type Strings = []string .

Именно так я хочу видеть реализованные типы выбора. В
в частности, именно так работают Rust и C ++ (золотые стандарты производительности).
Это.

Если бы я просто хотел проверить полноту, я мог бы использовать чекер. я хочу
победа в производительности. Это означает, что типы выбора также не могут быть нулевыми.

Получение адреса члена элемента выбора не должно быть разрешено (это
небезопасен для памяти даже в однопоточном случае, как это хорошо известно в
сообщество Rust.). Если для этого требуются другие ограничения на тип выбора,
Так тому и быть. Но для меня типы выбора всегда выделяются в куче
было бы плохо.

18 августа 2017 г. в 12:01 "jimmyfrasche" [email protected] написал:

@josharian https://github.com/josharian, поэтому, если я правильно это читаю
причина, по которой iface теперь всегда (* тип, * значение), а не прячется
значения размера слова во втором поле, как в Go ранее, так что
параллельному сборщику мусора не нужно проверять оба поля, чтобы убедиться, что второе
является указателем - он может просто предполагать, что это всегда так. Я правильно понял?

Другими словами, если бы тип выбора был реализован (с использованием нотации C), например

struct {
int который;
union {
А а;
B b;
C c;
} слагаемые;
}

сборщику мусора потребуется блокировка (или что-то необычное, но эквивалентное) для
проверить, что, чтобы определить, нужно ли сканировать слагаемые?

-
Вы получаете это, потому что вы являетесь автором темы.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/golang/go/issues/19412#issuecomment-323393003 или отключить звук
нить
https://github.com/notifications/unsubscribe-auth/AGGWB3Ayi31dYwotewcfgmCQL-XVrfxIks5sZbVrgaJpZM4MTmSr
.

@DemiMarie

Получение адреса члена элемента выбора не должно быть разрешено (это небезопасно для памяти, даже в однопоточном случае, как хорошо известно в сообществе Rust). Если для этого требуются другие ограничения на тип выбора, пусть будет так.

Неплохо подмечено. Это было у меня там, но, должно быть, потерялось при редактировании. Я добавил, что когда вы получаете доступ к значению из пика, оно всегда возвращает копию по той же причине.

В качестве примера того, почему это правда, для потомков рассмотрим

v := pick{ A int; B bool }{A: 5}
p := &v.[A] //(this would be illegal but pretending it's not for a second)
v.B = true

Если v оптимизирован так, что поля A и B занимают одну и ту же позицию в памяти, тогда p не указывает на int: он указывает до булевого. Нарушена безопасность памяти.

@jimmyfrasche

Вторая причина, по которой вы не хотите, чтобы содержимое было адресуемым, - это семантика мутации. Если значение сохраняется косвенно при определенных обстоятельствах, то

v := pick{ A int; ... }{A: 5}
v2 := v

v2.[A] = 6 // (this would be illegal under the proposal, but would be 
           // permitted if `v2.[A]` were addressable)

fmt.Println(v.[A]) // would print 6 if the contents of the pick are stored indirectly

Одно место, где pick похоже на интерфейсы, - это то, что вы хотите сохранить семантику значений, если вы храните в ней значения. Если вам может потребоваться косвенная адресация в качестве детали реализации, единственный вариант - сделать содержимое неадресуемым (или, точнее,

Изменить: Ой (см. Ниже)

@jimmyfrasche

Нулевое значение типа выбора - это его первое поле в исходном порядке и нулевое значение этого поля.

Обратите внимание, что это не сработает, если первое поле необходимо сохранить косвенно, если только вы не укажете особый случай нулевого значения, чтобы v.[A] и v.(error) поступали правильно.

@stevenblenkinsop Я не уверен, что вы имеете в виду,

Данный

var p pick { A error; B int }

нулевое значение p имеет динамическое поле A а значение A равно нулю.

Я не имел в виду значение, хранящееся в pick , содержащее указатель, я имел в виду значение, не являющееся указателем, которое хранится косвенно из-за ограничений макета, наложенных сборщиком мусора, как описано @josharian .

В вашем примере p.B - не указатель - не сможет совместно использовать перекрывающееся хранилище с p.A , которое состоит из двух указателей. Скорее всего, он должен быть сохранен косвенно (т.е. быть представлен как *int который автоматически разыменовывается при доступе к нему, а не как int ). Если бы p.B было первым полем, нулевое значение pick было бы new(int) , что не является приемлемым нулевым значением, поскольку требует инициализации. Вам понадобится особый случай, чтобы нулевое значение *int обрабатывалось как new(int) .

@jimmyfrasche
Ой, извини. Возвращаясь к разговору, я понял, что вы рассматривали возможность использования смежного хранилища для хранения вариантов с несовместимыми макетами, а не копирования интерфейсного механизма косвенного хранения типов, не являющихся указателями. В таком случае мои последние три комментария не имеют смысла.

Изменить: упс, состояние гонки. Написал потом увидел твой комментарий.

@stevenblenkinsop ах, хорошо, я понимаю, что ты имеешь в виду. Но это не проблема.

Совместное использование перекрывающегося хранилища - это оптимизация. Он никогда не смог бы этого сделать: семантика типа - важная часть.

Если компилятор может оптимизировать хранилище и решит это сделать, это будет приятным бонусом.

В вашем примере компилятор может сохранить его точно так же, как эквивалентную структуру (добавив тег, чтобы узнать, какое поле является активным). Это было бы

struct {
  which_field int // 0 = A, 1 = B
  A error
  B int
}

Нулевым значением по-прежнему являются все байты 0, и в особом случае нет необходимости тайно выделять их.

Важной частью является обеспечение того, чтобы в определенный момент времени играло только одно поле.

Мотивация к разрешению утверждений / переключателей типов для выбора была такова, что, например, если каждый тип в выборе удовлетворял fmt.Stringer вы могли написать метод для выбора, например

func (p P) String() string {
  return p.(fmt.Stringer).String()
}

Но поскольку типы полей выбора могут быть интерфейсами, это создает тонкость.

Если бы выбор P в предыдущем примере имел поле, тип которого сам по себе fmt.Stringer , метод String вызвал бы панику, если бы это было динамическое поле и его значение равно nil . Вы не можете ввести assert интерфейс nil ни для чего, даже для самого себя. https://play.golang.org/p/HMYglwyVbl Хотя это всегда было правдой, оно просто не появляется регулярно, но может появляться более регулярно с пиками.

Однако закрытый характер типов сумм позволит линтеру полноты находить везде, где это может произойти (возможно, с некоторыми ложными срабатываниями), и сообщать о случае, который необходимо обработать.

Также было бы удивительно, если бы вы могли реализовать методы по выбору, что эти методы не использовались для удовлетворения утверждения типа.

type Num pick { A int; B float32 }

func (n Num) String() string {
      switch v := n.[var] {
      case A:
          return fmt.Sprint(v)
      case B:
          return fmt.Sprint(v)
      }
}
...
n := Num{A: 5}
s1, ok := p.(fmt.Stringer) // ok == false
var i interface{} = p
s2, ok := i.(fmt.Stringer) // ok == true

Вы можете использовать утверждение типа для продвижения методов из текущего поля, если они удовлетворяют интерфейсу, но при этом возникают свои собственные проблемы, например, следует ли продвигать методы из значения в поле интерфейса, которые не определены в самом интерфейсе (или даже как это эффективно реализовать). Кроме того, тогда можно было бы ожидать, что методы, общие для всех полей, будут продвинуты до самого выбора, но тогда они должны будут отправляться через выбор варианта при каждом вызове, в дополнение, возможно, к виртуальной отправке, если выбор хранится в интерфейсе. , и / или в виртуальную отправку, если поле является интерфейсом.

Изменить: Кстати, оптимальная упаковка пика - это пример самой короткой общей проблемы

Правило состоит в том, что если это значение выбора, утверждение типа утверждает в динамическом поле значения выбора, но если значение выбора хранится в интерфейсе, утверждение типа находится в наборе методов для типа выбора. Поначалу это может показаться удивительным, но довольно последовательное.

Было бы не проблемой просто отказаться от утверждений разрешающего типа для значения выбора. Это было бы обидно, так как это позволяет очень легко продвигать методы, которые все типы в выборе разделяют, без необходимости выписывать все случаи или использовать отражение.

Хотя было бы довольно просто использовать генерацию кода для написания

func (p Pick) String() string {
  switch v := p.[var] {
  case A:
    return v.String()
  case B:
    return v.String()
  //etc
  }
}

Просто пошел дальше и отбросил утверждения типа. Может быть, их стоит добавить, но они не являются необходимой частью предложения.

Я хочу вернуться к предыдущему комментарию @ianlancetaylor , потому что у меня появился новый взгляд на него после того, как я подумал еще об обработке ошибок (в частности, https://github.com/golang/go/issues/21161# issuecomment-320294933).

В частности, что дает нам новый тип типа, чего мы не получаем от типов интерфейса?

На мой взгляд, главное преимущество типов суммы состоит в том, что они позволяют нам различать возврат нескольких значений и возврат одного из нескольких значений, особенно когда одно из этих значений является экземпляром интерфейса ошибок.

В настоящее время у нас есть много функций в форме

func F(…) (T, error) {
    …
}

Некоторые из них, такие как io.Reader.Read и io.Reader.Write , возвращают T вместе с error , тогда как другие возвращают либо T или error но не оба сразу. Для API прежнего стиля игнорирование T в случае ошибки часто является ошибкой (например, если ошибка io.EOF ); для последнего стиля возврат ненулевого значения T является ошибкой.

Автоматизированные инструменты, включая lint , могут проверять использование определенных функций, чтобы гарантировать, что значение игнорируется (или не игнорируется) правильно, когда ошибка не равна нулю, но такие проверки, естественно, не распространяются на произвольные функции.

Например, proto.Marshal предназначен для стиля «значение и ошибка», если ошибка имеет вид RequiredNotSetError , но в противном случае кажется стилем «значение или ошибка». Поскольку система типов не делает различий между ними, легко случайно ввести регрессию: либо не возвращать значение, когда мы должны, либо возвращать значение, когда мы не должны. А реализации proto.Marshaler еще больше усложняют дело.

С другой стороны, если бы мы могли выразить тип как объединение, мы могли бы быть более точными:

type PartialMarshal struct {
    Data []byte // The marshalled value, ignoring unset required fields.
    MissingFields []string
}

func Marshal(pb Message) []byte | PartialMarshal | error

@ianlancetaylor , я

Данный

var r interface{} restrict { uint, int } = 1

динамический тип r - int , и

var _ interface{} restrict { uint32, int32 } = 1

незаконно.

Данный

type R interface{} restrict { struct { n int }, etc }
type S struct { n int }

тогда var _ R = S{} будет незаконным.

Но учитывая

type R interface{} restrict { int, error }
type A interface {
  error
  foo()
}
type C struct { error }
func (C) foo() {}

и var _ R = C{} и var _ R = A(C{}) будут законными.

Оба

interface{} restrict { io.Reader, io.Writer }

а также

interface{} restrict { io.Reader, io.Writer, *bytes.Buffer }

эквивалентны.

Так же,

interface{} restrict { error, net.Error }

эквивалентно

interface { Error() string }

Данный

type IO interface{} restrict { io.Reader, io.Writer }
type R interface{} restrict {
  interface{} restrict { int, uint },
  IO,
}

тогда базовый тип R эквивалентен

interface{} restrict { io.Writer, uint, io.Reader, int }

Изменить: небольшое исправление курсивом

@jimmyfrasche Я бы не стал говорить, что то, что я написал выше, было предложением. Это было больше похоже на идею. Я бы подумал над вашими комментариями, но на первый взгляд они выглядят правдоподобно.

Предложение @jimmyfrasche в значительной степени похоже на то, как я интуитивно ожидал, что тип выбора будет вести себя в Go. Я думаю, что особенно стоит отметить, что его предложение использовать нулевое значение первого поля для нулевого значения выбора интуитивно понятно: «нулевое значение означает обнуление байтов», при условии, что значения тегов начинаются с нуля (возможно, это уже было отмечено; эта ветка к настоящему времени очень длинная ...). Мне также нравится влияние на производительность (отсутствие ненужных аллоков) и то, что пикировки полностью ортогональны интерфейсам (нет ничего удивительного в переключении поведения пикировки, содержащей интерфейс).

Единственное, что я хотел бы изменить, - это изменить тег: foo.X = 0 кажется, что это может быть foo = Foo{X: 0} ; еще несколько символов, но более ясно, что он сбрасывает тег и обнуляет значение. Это мелочь, и я был бы очень рад, если бы его предложение было принято как есть.

@ ns-cweber спасибо, но я не могу поверить в поведение нулевого значения. Идеи циркулировали некоторое время и были в предложении

Что касается foo.X = 0 против foo = Foo{X: 0} , мое предложение на самом деле позволяет и то, и другое. Последнее полезно, если это поле выбора является структурой, поэтому вы можете сделать foo.X.Y = 0 вместо foo = Foo{X: image.Point{X: foo.[X].X, 0}} что помимо подробного описания может дать сбой во время выполнения.

Я также думаю, что это помогает сохранить его как таковой, потому что он усиливает шаг лифта для его семантики: это структура, в которой время может быть установлено только одно поле.

Одна вещь, которая может помешать его принятию как есть, - это то, как будет работать встраивание выбора в структуру. На днях я понял, что упустил из виду различные эффекты, которые могут иметь место при использовании структуры. Я думаю, что его можно отремонтировать, но не совсем уверен, что лучше всего отремонтировать. Самым простым было бы то, что он наследует только методы, и вам нужно напрямую ссылаться на встроенный выбор по имени, чтобы добраться до его полей, и я склоняюсь к этому, чтобы избежать структуры, имеющей как поля структуры, так и поля выбора.

@jimmyfrasche Спасибо, что

Наконец, ваше простое предложение по встраиванию пика кажется мне интуитивным. Даже если мы передумаем, мы можем перейти от простого предложения к сложному, не нарушая существующий код, но обратное неверно.

@ ns-cweber

Я мог видеть, как устанавливаю foo.XY, не понимая, что это автоматически изменит поле выбора

Это справедливый аргумент, но вы могли бы рассказать о многих вещах на этом языке или любом другом языке, если на то пошло. В общем, в Go есть поручни безопасности, но нет ножниц безопасности.

Он обычно защищает вас от множества серьезных вещей, если вы не изо всех сил стараетесь их ниспровергнуть, но вам все равно нужно знать, что вы делаете.

Это может раздражать, когда вы совершаете такую ​​ошибку, но, ох, это не так уж сильно отличается от «Я установил bar.X = 0 но я хотел установить bar.Y = 0 », поскольку гипотеза полагается на то, что вы не понимаете что foo - это тип выбора.

Аналогично i.Foo() , p.Foo() и v.Foo() выглядят одинаково, но если i - это интерфейс nil , p является нулевым указателем, и Foo не обрабатывает этот случай, первые два могут вызвать панику, тогда как, если v использует приемник метода значения, он не может (по крайней мере, не из самого вызова, во всяком случае) .

-

Что касается встраивания, хорошее замечание о том, что его легко ослабить позже, поэтому я просто пошел дальше и отредактировал предложение.

Типы суммы часто имеют пустое поле. Например, в пакете database/sql у нас есть:

type NullString struct {
    String string
    Valid  bool // Valid is true if String is not NULL
}

Если бы у нас были типы сумм / выборы / объединения, это можно было бы выразить как:

type NullString pick {
  Null   struct{}
  String string
}

В этом случае тип суммы имеет очевидные преимущества перед структурой. Я думаю, что это достаточно распространенное использование, и его стоит включить в качестве примера в любое предложение.

Bikeshedding (извините), я бы сказал, что это стоит синтаксической поддержки и несогласованности с синтаксисом встраивания полей структуры:

type NullString union {
  Null
  String string
}

@neild

Нажатие сначала на последнюю точку: в качестве последнего изменения перед тем, как я опубликовал (что не является строго обязательным в любом смысле), я добавил, что если есть именованный тип (или указатель на именованный тип) без имени поля, выбор создает неявное поле с тем же именем, что и тип. Возможно, это не лучшая идея, но казалось, что она охватывает один из распространенных случаев «любого из этих типов» без особой суеты. Учитывая, что ваш последний пример можно было бы написать:

type Null = struct{} //though this puts Null in the same scope as NullString
type NullString pick {
  Null
  String string
}

Но вернемся к главному: да, это отличное применение. Фактически, вы можете использовать его для построения перечислений: type Stoplight pick { Stop, Slow, Go struct{} } . Это было бы похоже на faux-enum const / iota. Он даже компилируется с тем же результатом. Основным преимуществом в этом случае является то, что число, представляющее состояние, полностью инкапсулировано, и вы не можете указать какое-либо состояние, кроме трех перечисленных.

К сожалению, существует несколько неудобный синтаксис для создания и установки значений Stoplight который в этом случае усугубляется:

light := Stoplight{Slow: struct{}{}}
light.Go = struct{}{}

Разрешение {} или _ быть сокращенным для struct{}{} , как предложено в другом месте, может помочь.

Многие языки, особенно функциональные, обходят это ограничение, помещая метки в ту же область видимости, что и тип. Это создает большую сложность и не позволяет двум выбранным объектам, определенным в одной области, совместно использовать имена полей.

Однако это легко обойти с помощью генератора кода, который создает функцию с тем же именем для каждого поля в выборе, которая принимает тип поля в качестве аргумента. Если бы он также, как особый случай, не принимал аргументов, если тип был нулевого размера, тогда выходные данные для примера Stoplight будут выглядеть следующим образом

func Stop() Stoplight {
  return Stoplight{Stop: struct{}{}}
}
func Slow() Stoplight {
  return Stoplight{Slow: struct{}{}}
}
func Go() Stoplight {
  return Stoplight{Go: struct{}{}}
}

и для вашего примера NullString это будет выглядеть так:

func Null() NullString {
  return NullString{Null: struct{}{}}
}
func String(s string) NullString {
  return NullString{String: s}
}

Это некрасиво, но это всего за go generate и, вероятно, очень легко встроить.

Это не сработало бы в случае, когда он создавал неявные поля на основе имен типов (если типы не были из других пакетов) или он запускался на двух пиках в том же пакете, который разделял имена полей, но это нормально. Предложение не делает все из коробки, но оно позволяет многое и дает программисту гибкость, чтобы решить, что лучше для данной ситуации.

Больше синтаксиса bikeshedding:

type NullString union {
  Null
  Value string
}

var _ = NullString{Null}
var _ = NullString{Value: "some value"}
var _ = NullString{Value} // equivalent to NullString{Value: ""}.

Конкретно, литерал со списком элементов, который не содержит ключей, интерпретируется как имя устанавливаемого поля.

Это было бы синтаксически несовместимым с другим использованием составных литералов. С другой стороны, это использование кажется разумным и интуитивно понятным в контексте типов объединения / выбора / суммы (по крайней мере, для меня), поскольку нет разумной интерпретации инициализатора объединения без ключа.

@neild

Это было бы синтаксически несовместимым с другим использованием составных литералов.

Мне это кажется огромным минусом, хотя в данном контексте это имеет смысл.

Также обратите внимание, что

var ns NullString // == NullString{Null: struct{}{}} == NullString{}
ns.String = "" // == NullString{String: ""}

Для работы с struct{}{} когда я использую map[T]struct{} я бросаю

var set struct{}

где-нибудь и используйте theMap[k] = set , аналогично будет работать с выбором

Дальнейший байкешеддинг: пустой тип (в контексте типов суммы) условно называется «единица», а не «нуль».

@bcmills Sorta .

В функциональных языках, когда вы создаете тип суммы, его метки на самом деле являются функциями, которые создают значения этого типа (хотя и специальные функции, известные как «конструкторы типов» или «тиконы», о которых знает компилятор, чтобы разрешить сопоставление с образцом), поэтому

data Bool = False | True

создает тип данных Bool и две функции в одной области: True и False , каждая с подписью () -> Bool .

Здесь () - это то, как вы пишете тип произносится как unit - тип только с одним значением. В Go этот тип можно записать разными способами, но идиоматически он записывается как struct{} .

Таким образом, тип аргумента конструктора будет называться единицей. Соглашение для имени конструктора обычно None при использовании в качестве типа параметра, подобного этому, но его можно изменить в соответствии с доменом. Null было бы хорошим именем, например, если бы значение было получено из базы данных.

@bcmills

На мой взгляд, главное преимущество типов суммы состоит в том, что они позволяют нам различать возврат нескольких значений и возврат одного из нескольких значений, особенно когда одно из этих значений является экземпляром интерфейса ошибок.

С другой стороны, я считаю это основным недостатком типов суммы в Go.

Конечно, во многих языках типы сумм используются именно в случае возврата некоторого значения или ошибки, и для них это хорошо работает. Если бы в Go были добавлены типы сумм, возникло бы большое искушение использовать их таким же образом.

Однако в Go уже есть обширная экосистема кода, в которой для этой цели используется несколько значений. Если новый код использует типы суммы для возврата кортежей (значение, ошибка), то эта экосистема станет фрагментированной. Некоторые авторы будут продолжать использовать многократные возвраты для согласованности с их существующим кодом; некоторые авторы будут использовать типы сумм; некоторые попытаются преобразовать свои существующие API. Авторы, которые по какой-то причине застряли на старых версиях Go, будут лишены доступа к новым API. Это будет беспорядок, и я не думаю, что выгода окупится затраченными средствами.

Если новый код использует типы суммы для возврата кортежей (значение, ошибка), то эта экосистема станет фрагментированной.

Если мы добавим типы сумм в Go 2 и будем использовать их единообразно, тогда проблема сведется к миграции, а не фрагментации: должна быть возможность преобразовать API Go 1 (значение, ошибка) в Go 2 (значение | ошибка ) API и наоборот, но они могут быть разными типами в частях Go 2 программы.

Если мы добавим типы сумм в Go 2 и будем использовать их единообразно

Обратите внимание, что это предложение сильно отличается от тех, что мы видели здесь до сих пор: стандартную библиотеку необходимо будет тщательно переработать, необходимо будет определить перевод между стилями API и т. Д. Идите по этому пути, и это станет довольно большим и сложное предложение по переходу API с небольшими поправками, касающимися дизайна типов суммы.

Намерение состоит в том, чтобы Go 1 и Go 2 могли беспрепятственно сосуществовать в одном проекте, поэтому я не думаю, что проблема заключается в том, что кто-то может застрять в компиляторе Go 1 «по какой-то причине» и не сможет использовать Идите 2 библиотеки. Однако, если у вас есть зависимость A которая, в свою очередь, зависит от обновлений B и B для использования новой функции, такой как pick в своем API, тогда это нарушит зависимость A если не обновится для использования новой версии B . A может просто продавать B и продолжать использовать старую версию, но если старая версия не поддерживается из-за ошибок безопасности и т. Д ... или если вам нужно использовать новую версию B напрямую, и по какой-то причине у вас не может быть двух версий в вашем проекте, это может создать проблему.

В конечном итоге проблема здесь не в языковых версиях, а больше в изменении сигнатур существующих экспортируемых функций. Тот факт, что это будет новая функция, дающая импульс, немного отвлекает от этого. Если намерение состоит в том, чтобы разрешить изменение существующих API-интерфейсов для использования pick без нарушения обратной совместимости, тогда может потребоваться какой-то синтаксис моста. Например (полностью как соломинку):

type ReadResult pick(N int, Err error) {
    N
    PartialResult struct { N; Err }
    Err
}

Компилятор может просто выдать ReadResult когда к нему обращается устаревший код, используя нулевые значения, если поле отсутствует в конкретном варианте. Не знаю, как пойти другим путем и стоит ли это того. API, такие как template.Must могут просто продолжать принимать несколько значений, а не pick и полагаться на сплаттинг, чтобы компенсировать разницу. Или можно использовать что-то вроде этого:

type ReadResult pick(N int, Err error) {
case Err == nil:
    N
default:
    PartialResult struct { N; Err }
case N == 0:
    Err
}

Это усложняет вещи, но я могу видеть , как введение функции , которая изменяет , как API - интерфейсы должны быть написаны требует рассказ о том , как с переходом , не нарушая мира. Может быть, есть способ сделать это, не требующий синтаксиса моста.

Переход от типов суммы к типам продукта (структуры, множественные возвращаемые значения) тривиален - просто установите все, что не является значением, равным нулю. В целом, переход от типов продуктов к типам сумм не имеет четкого определения.

Если API хочет плавно и постепенно перейти от реализации на основе типа продукта к реализации на основе типа суммы, самым простым путем было бы иметь две версии всего необходимого, где версия типа суммы имеет фактическую реализацию, а версия типа продукта вызывает Суммарная версия, выполнение любых необходимых проверок во время выполнения и любое проектирование вниз в пространство продукта.

Это действительно абстрактно, поэтому вот пример

версия 1 без сумм

func Take(i interface{}) error {
  switch i.(type) {
  case int: //do something
  case string:
  default: return fmt.Errorf("invalid %T", i)
  }
}
func Give() (interface{}, error) {
   i := f() //something
   if i == nil {
     return nil, errors.New("whoops v:)v")
  }
  return i
}

версия 2 с суммами

type Value pick {
  I int
  S string
}
func TakeSum(v Value) {
  // do something
}
// Deprecated: use TakeSum
func Take(i interface{}) error {
  switch x := i.(type) {
  case int: TakeSum(Value{I: x})
  case string: TakeSum(Value{S: x})
  default: return fmt.Errorf("invalid %T", i)
  }
}
type ErrValue pick {
  Value
  Err error
}
func GiveSum() ErrValue { //though honestly (Value, error) is fine
  return f()
}
// Deprecated: use GiveSum
func Give() (interface{}, error) {
  switch v := GiveSum().(var) {
  case Value:
    switch v := v.(var) {
    case I: return v, nil
    case S: return v, nil
    }
  case Err:
    return nil, v
  }
}

версия 3 удалит Give / Take

версия 4 переместит реализацию GiveSum / TakeSum в Give / Take, заставит GiveSum / TakeSum просто вызывать Give / Take и отказаться от GiveSum / TakeSum.

версия 5 удалит GiveSum / TakeSum

Это не красиво и не быстро, но это то же самое, что и любое другое крупномасштабное нарушение аналогичного характера и не требует ничего лишнего от языка.

Я думаю (большая часть) полезность типа суммы может быть реализована с помощью механизма, ограничивающего присваивание типу интерфейса типа {} во время компиляции.

В моих снах это выглядит так:

type T1 switch {T2,T3} // only nil, T2 and T3 may be assigned to T1
type T2 struct{}
type U switch {} // only nil may be assigned to U
type V switch{interface{} /* ,... */} // V equivalent to interface{}
type Invalid switch {T2,T2} // only uniquely named types
type T3 switch {int,uint} // switches can contain switches but... 

... также было бы ошибкой времени компиляции утверждать, что тип переключателя является типом, который явно не определен:

var t1 T1
i,ok := t1.(int) // T1 can't be int, only T2 or T3 (but T3 could be int)
switch t := t1.(type) {
    case int: // compile error, T1 is just nil, T2 or T3
}

и go vet будет придираться к неоднозначному назначению констант таким типам, как T3, но для всех намерений и целей (во время выполнения) var x T3 = 32 будет var x interface{} = 32 . Возможно, некоторые предопределенные типы переключателей для встроенных функций в пакете, названные чем-то вроде переключателей или пони, тоже будут отличными.

@ j7b , @ianlancetaylor предложил аналогичную идею в https://github.com/golang/go/issues/19412#issuecomment -323256891

Я опубликовал то, что, по моему мнению, будет логическими последствиями этого позже, на https://github.com/golang/go/issues/19412#issuecomment -325048452.

Похоже, что многие из них применимы одинаково, учитывая сходство.

Было бы здорово, если бы что-то подобное сработало. Было бы легко перейти от интерфейсов к интерфейсам + ограничениям (особенно с синтаксисом Яна: просто прикрепите restrict к концу существующих псевдосумм, построенных с помощью интерфейсов). Это было бы легко реализовать, поскольку во время выполнения они были бы по существу идентичны интерфейсам, и большая часть работы заключалась бы в том, чтобы компилятор выдавал дополнительные ошибки при нарушении их инвариантов.

Но я не думаю, что это возможно.

Все выстраивается так близко, что кажется подходящим, но вы увеличиваете масштаб, и это просто не совсем правильно, поэтому вы слегка толкаете его, а затем что-то еще выскакивает из выравнивания. Вы можете попытаться отремонтировать его, но тогда вы получите что-то, что очень похоже на интерфейсы, но ведет себя иначе в странных случаях.

Может я что-то упускаю.

Нет ничего плохого в предложении ограниченного интерфейса, если вы согласны с тем, что случаи не обязательно являются несвязанными. Я не думаю, что это так же удивительно, как вы это делаете, что объединение между двумя типами интерфейса (например, io.Reader / io.Writer ) не является непересекающимся. Это полностью согласуется с тем фактом, что вы не можете определить, было ли значение, присвоенное interface{} было сохранено как io.Reader или io.Writer если оно реализует оба. Тот факт, что вы можете построить непересекающееся объединение, если каждый случай является конкретным типом, кажется совершенно адекватным.

Компромисс заключается в том, что, если объединения являются ограниченными интерфейсами, вы не можете определять методы непосредственно на них. И если это ограниченные типы интерфейса, вы не получите гарантированного прямого хранилища, которое предоставляют типы pick . Я не уверен, стоит ли добавлять в язык что-то особенное, чтобы получить эти дополнительные преимущества.

@jimmyfrasche для type T switch {io.Reader,io.Writer} нормально назначить ReadWriter для T, но вы можете только утверждать, что T является io.Reader или Io.Writer, вам понадобится другое утверждение, чтобы утверждать io.Reader или io.Writer ReadWriter, который должен поощрять добавление его в switchtype, если это полезное утверждение.

@stevenblenkinsop Вы можете определить предложение выбора без методов. Фактически, если вы избавитесь от методов и неявных имен полей, вы можете разрешить встраивание выбора. (Хотя, очевидно, я думаю, что методы и, в гораздо меньшей степени, неявные имена полей, здесь более полезный компромисс).

И, с другой стороны, синтаксис @ianlancetaylor позволит

type IR interface {
  Foo()
  Bar()
} restrict { A, B, C }

которые будут компилироваться, пока A , B и C имеют методы Foo и Bar (хотя вам придется беспокоиться около nil значений).

редактировать: пояснение курсивом

Я думаю, что была бы полезна какая-то форма _restricted interface_, но я не согласен с синтаксисом. Вот что я предлагаю. Он действует аналогично алгебраическому типу данных, который группирует связанные с предметной областью объекты, которые не обязательно имеют общее поведение.

//MyGroup can be any of these. It can contain other groups, interfaces, structs, or primitive types
type MyGroup group {
   MyOtherGroup
   MyInterface
   MyStruct
   int
   string
   //..possibly some other types as well
}

//type definitions..
type MyInterface interface{}
type MyStruct struct{}
//etc..

func DoWork(item MyGroup) {
   switch t:=item.(type) {
      //do work here..
   }
}

У этого подхода есть несколько преимуществ по сравнению с обычным подходом с пустым интерфейсом interface{} :

  • проверка статического типа при использовании функции
  • пользователь может сделать вывод, какой тип аргумента требуется только по сигнатуре функции, без необходимости смотреть на реализацию функции

Пустой интерфейс interface{} полезен, когда количество задействованных типов неизвестно. У вас действительно нет выбора, кроме как полагаться на проверку во время выполнения. С другой стороны, если количество типов ограничено и известно во время компиляции, почему бы не попросить компилятор помочь нам?

@henryas Я думаю, что в настоящее время рекомендуется использовать более полезное сравнение для (открытых) типов сумм: непустые интерфейсы (если нет четкого интерфейса, который нельзя очистить, используя функции неэкспортированных маркеров).
Я не думаю, что ваши аргументы в значительной степени применимы к этому.

Вот отчет об опыте использования Go protobufs:

  • Синтаксис proto2 допускает использование «необязательных» полей, то есть типов, в которых существует различие между нулевым значением и неустановленным значением. Текущее решение - использовать указатель (например, *int ), где нулевой указатель указывает на неустановленное значение, а указатель набора указывает на фактическое значение. Желание - это подход, который позволяет проводить различие между нулем и неустановленным, не усложняя общий случай, когда требуется только доступ к значению (где нулевое значение нормально, если не установлено).

    • Это неэффективно из-за дополнительного выделения (хотя союзы могут пострадать от той же участи в зависимости от реализации).
    • Это болезненно для пользователей, потому что необходимость постоянно проверять указатель ухудшает читаемость (хотя ненулевые значения по умолчанию в protos могут означать, что необходимость проверки - это хорошо ...).
  • Протоязык допускает "один из", которые являются прото-версией типов суммы. В настоящее время используется следующий подход ( примерный пример ):

    • Определите тип интерфейса с помощью скрытого метода (например, type Communique_Union interface { isCommunique_Union() } )
    • Для каждого из возможных типов Go, разрешенных в объединении, определите структуру-оболочку, единственная цель которой - обернуть каждый разрешенный тип (например, type Communique_Number struct { Number int32 } ), где каждый тип имеет метод isCommunique_Union .
    • Это также неэффективно, поскольку оболочки вызывают выделение. Тип суммы может помочь, поскольку мы знаем, что наибольшее значение (срез) займет не более 24 Байт.

@henryas Я думаю, что в настоящее время рекомендуется использовать более полезное сравнение для (открытых) типов сумм: непустые интерфейсы (если нет четкого интерфейса, который нельзя очистить, используя функции неэкспортированных маркеров).
Я не думаю, что ваши аргументы в значительной степени применимы к этому.

Вы имеете в виду добавление фиктивного неэкспортируемого метода к объекту, чтобы объект можно было передать как интерфейс, как показано ниже?

type MyInterface interface {
   belongToMyInterface() //dummy method definition
}

type MyObject struct{}
func (MyObject) belongToMyInterface(){} //dummy method

Я не думаю, что это вообще следует рекомендовать. Это больше похоже на обходной путь, чем на решение. Я лично предпочел бы отказаться от проверки статического типа, чем иметь пустые методы и ненужные определения методов.

Вот проблемы с подходом _dummy method_:

  • Ненужные методы и определения методов загромождают объект и интерфейс.
  • Каждый раз, когда добавляется новая _group_, вам необходимо изменять реализацию объекта (например, добавляя фиктивные методы). Это неправильно (см. Следующий пункт).
  • Алгебраический тип данных (или группировка на основе _domain_, а не поведения) зависит от домена . В зависимости от домена вам может потребоваться по-разному просматривать отношения между объектами. Бухгалтер группирует документы иначе, чем заведующий складом. Эта группировка касается потребителя объекта, а не самого объекта. Объекту не нужно ничего знать о проблеме потребителя, да и не должно. Нужно ли в счете-фактуре что-нибудь знать о бухгалтерском учете? Если нет, то почему счет-фактура должен изменять свою реализацию _ (например, добавлять новые фиктивные методы) _ каждый раз, когда изменяется правило учета _ (например, применяется новая группировка документов) _? Используя подход _dummy method_, вы связываете свой объект с доменом потребителя и делаете важные предположения о домене потребителя. Вам не нужно этого делать. Это даже хуже, чем подход с пустым интерфейсом interface{} . Доступны лучшие подходы.

@henryas

Я не считаю ваш третий аргумент сильным аргументом. Если бухгалтер хочет по-другому взглянуть на отношения объектов, бухгалтер может создать свой собственный интерфейс, который соответствует их спецификациям. Добавление частного метода к интерфейсу не означает, что конкретные типы, которые ему соответствуют, несовместимы с подмножествами интерфейса, определенными в другом месте.

Парсер Go активно использует эту технику, и, честно говоря, я не могу себе представить, что пикировки сделают этот пакет настолько лучше, чтобы гарантировать реализацию пиков в языке.

@as Я

В итоге к объектам прикрепляется множество пустых методов, и читателям не очевидно, зачем вам нужны эти методы, потому что интерфейсы, которые требуют их, находятся в отдельном домене / пакете / слое.

То, что подход открытых сумм через интерфейсы не позволяет легко использовать суммы, достаточно справедливо. Явные типы сумм, очевидно, упростят получение сумм. Это совсем другой аргумент, чем «типы сумм обеспечивают безопасность типов» - вы все еще можете получить безопасность типов сегодня, если вам это нужно.

Тем не менее, я все еще вижу два недостатка закрытых сумм, реализованных на других языках: во-первых, сложность их развития в крупномасштабном распределенном процессе разработки. И второе: я думаю, что они добавляют мощности системе типов, и мне нравится, что в Go нет очень мощной системы типов, поскольку это препятствует кодированию типов и вместо этого кодирует программы - когда я чувствую, что проблема может выиграть от Более мощная система типов, я перехожу на более мощный язык (например, Haskell или Rust).

При этом, по крайней мере, второй вариант определенно является предпочтительным, и даже если вы согласитесь, то, будут ли недостатки перевешивать достоинства, также зависит от личных предпочтений. Просто хотел отметить, что вы не можете получить типобезопасные суммы без закрытых типов сумм, это не совсем так :)

[1] примечательно, что это непросто, но все же возможно , например, вы можете сделать

type Node interface {
    node()
}

type Foo struct {
    bar.Baz
}

func (foo) node() {}

@Merovius
Я не согласен с вашим вторым недостатком. Тот факт, что в стандартной библиотеке есть множество мест, которые могли бы получить огромную выгоду от типов суммы, но теперь реализованы с использованием пустых интерфейсов и паники, показывает, что этот недостаток вредит кодированию. Конечно, люди могут сказать, что, поскольку такой код изначально был написан, проблем нет и нам не нужны типы сумм, но глупость этой логики в том, что тогда нам не понадобится какой-либо другой тип для функции. подписи, и вместо этого мы должны просто использовать пустые интерфейсы.

Что касается использования интерфейсов с некоторыми методами для представления типов сумм прямо сейчас, есть один большой недостаток. Вы не знаете, какие типы можно использовать для этого интерфейса, поскольку они реализованы неявно. При правильном типе суммы сам тип точно описывает, какие типы фактически могут использоваться.

Я не согласен с вашим вторым недостатком.

Вы не согласны с утверждением «типы суммы поощряют программирование с типами» или вы не согласны с тем, что это обратная сторона? Поскольку не похоже, что вы не согласны с первым (ваш комментарий, по сути, просто повторное утверждение этого), а в отношении второго я признал, что это зависит от предпочтений, указанных выше.

Тот факт, что в стандартной библиотеке есть множество мест, которые могли бы получить огромную выгоду от типов суммы, но теперь реализованы с использованием пустых интерфейсов и паники, показывает, что этот недостаток вредит кодированию. Конечно, люди могут сказать, что, поскольку такой код изначально был написан, проблем нет и нам не нужны типы сумм, но глупость этой логики в том, что тогда нам не понадобится какой-либо другой тип для функции. подписи, и вместо этого мы должны просто использовать пустые интерфейсы.

Этот тип черно-белого аргумента действительно не помогает . Я согласен, что в некоторых случаях типы суммы уменьшат боль. Каждое изменение, делающее систему типов более мощной, в некоторых случаях уменьшит боль, но в некоторых случаях она также вызовет боль. Итак, вопрос в том, что перевешивает другие (и это в значительной степени вопрос предпочтений).

Обсуждения не должны касаться того, хотим ли мы систему типов в стиле Python (без типов) или систему типов в стиле coq (доказательства правильности для всего). Обсуждение должно быть таким: «перевешивают ли преимущества типов сумм их недостатки», и полезно признать и то, и другое.


FTR, я хочу еще раз подчеркнуть, что лично я бы не был противником открытых типов сумм (т.е. каждый тип суммы имеет неявный или явный случай SomethingElse), поскольку это устранит большинство технических недостатков их (в основном то, что их трудно развить), а также они предоставляют большую часть их технических преимуществ (статическая проверка типов, упомянутая вами документация, вы можете перечислять типы из других пакетов…).

Однако я также предполагаю, что открытые суммы: а) не будут удовлетворительным компромиссом для людей, которые обычно настаивают на типах сумм, и б) вероятно, не будут считаться достаточно большим преимуществом, чтобы гарантировать включение их командой Go. Но я был бы готов оказаться неправым по одному или по обоим из этих предположений :)

Еще один вопрос:

Тот факт, что в стандартной библиотеке есть много мест, которые могли бы получить огромную выгоду от типов суммы

Я могу вспомнить только два места в стандартной библиотеке, где я бы сказал, что для них есть какое-то существенное преимущество: размышлять и идти / идти. И даже там пакеты, кажется, прекрасно работают без них. С этой точки зрения слова «много» и «безмерно» кажутся преувеличением - но, конечно, я могу не увидеть кучу законных мест.

database/sql/driver.Value может выиграть от того, чтобы быть типом суммы (как указано в # 23077).
https://godoc.corp.google.com/pkg/database/sql/driver#Value

Однако более общедоступный интерфейс в database/sql.Rows.Scan не будет без потери функциональности. Сканирование может считывать значения, базовый тип которых, например, int ; изменение целевого параметра на тип суммы потребует ограничения его входных данных конечным набором типов.
https://godoc.corp.google.com/pkg/database/sql#Rows.Scan

@Merovius

Я бы не был так противником открытых типов сумм (т.е. каждый тип суммы имеет неявный или явный случай SomethingElse), поскольку это смягчило бы большинство их технических недостатков (в основном то, что их трудно развить)

Есть как минимум два других варианта, которые облегчат «трудноразвиваемую» проблему замкнутых сумм.

Один из них - разрешить совпадения по типам, которые на самом деле не являются частью суммы. Затем, чтобы добавить член к сумме, вы сначала обновляете его потребителей, чтобы они соответствовали новому члену, и фактически добавляете этот член только после обновления потребителей.

Другой - разрешить «невозможные» члены: то есть члены, которые явно разрешены в совпадениях, но явно запрещены в фактических значениях. Чтобы добавить член к сумме, вы сначала добавляете его как невозможный член, затем обновляете потребителей и, наконец, изменяете новый член, чтобы он стал возможным.

database/sql/driver.Value может выиграть от того, чтобы быть типом суммы

Согласен, не знал об этом. Спасибо :)

Один из них - разрешить совпадения по типам, которые на самом деле не являются частью суммы. Затем, чтобы добавить член к сумме, вы сначала обновляете его потребителей, чтобы они соответствовали новому члену, и фактически добавляете этот член только после обновления потребителей.

Интригующее решение.

Интерфейсы @Merovius - это, по сути, семейство типов с бесконечной суммой. Все типы сумм, бесконечные или другие, имеют регистр default: . Однако без типов с конечной суммой default: означает либо допустимый случай, о котором вы не знали, либо недопустимый случай, который является ошибкой где-то в программе - с конечными суммами это только первое и никогда второе.

Типы json.Token и sql.Null * - другие канонические примеры. go / types выиграют так же, как и go / ast. Я предполагаю, что есть много примеров, которых нет в экспортированных API-интерфейсах, где было бы проще отлаживать и тестировать некоторые сложные системы, ограничивая домен внутреннего состояния. Я считаю их наиболее полезными для внутренних ограничений состояния и приложений, которые не так часто возникают в общедоступных API-интерфейсах для общих библиотек, хотя они также иногда используются там.

Лично я думаю, что типы сумм дают Go достаточно дополнительной мощности, но не слишком большой. Система типов Go и без того очень красивая и гибкая, хотя и имеет свои недостатки. Дополнения Go2 к системе типов просто не дадут столько мощности, сколько уже есть - 80-90% того, что нужно, уже есть. Я имею в виду, что даже дженерики принципиально не позволят вам делать что-то новое: они позволят вам делать то, что вы уже делаете, более безопасно, проще, эффективнее и таким образом, чтобы обеспечить лучший инструментарий. Типы суммы похожи, imo (хотя, очевидно, если бы один или другой дженерики имели приоритет (и они довольно хорошо сочетаются)).

Если вы разрешаете постороннее значение по умолчанию (разрешены все случаи + значение по умолчанию) для переключателей типа суммы и не заставляете компилятор обеспечивать полноту (хотя линтер мог бы), добавить регистр к сумме так же просто (и так же сложно). ) как изменение любого другого общедоступного API.

Типы json.Token и sql.Null * - другие канонические примеры.

Жетон - конечно. Еще один пример проблемы AST (практически любой синтаксический анализатор выигрывает от типов сумм).

Однако я не вижу преимущества sql.Null *. Без дженериков (или добавления некоторых «волшебных» универсальных дополнительных встроенных функций) вам все равно придется иметь типы, и не кажется, что существенной разницы между type NullBool enum { Invalid struct{}; Value Int } и type NullBool struct { Valid bool; Value Int } . Да, я понимаю, что разница есть, но она исчезающе мала.

Если вы разрешаете постороннее значение по умолчанию (разрешены все случаи + значение по умолчанию) для переключателей типа суммы и не заставляете компилятор обеспечивать полноту (хотя линтер мог бы), добавить регистр к сумме так же просто (и так же сложно). ) как изменение любого другого общедоступного API.

См. Выше. Это то, что я называю открытыми суммами, я меньше против них.

Это то, что я называю открытыми суммами, я меньше против них.

Мое конкретное предложение https://github.com/golang/go/issues/19412#issuecomment -323208336, и я считаю, что оно может удовлетворить ваше определение открытого, хотя оно все еще немного грубое, и я уверен, что есть еще кое-что, что нужно удалить и отполировать. В частности, я заметил, что было непонятно, допустим ли случай по умолчанию, даже если были перечислены все случаи, поэтому я просто обновил его.

Согласны с тем, что необязательные типы не являются убийственным приложением для типов сумм. Однако они довольно хороши, и, как вы отметили, с помощью универсальных шаблонов, определяющих

type Nullable(T) pick { // or whatever syntax (on all counts)
  Null struct{}
  Value T
}

один раз и охватить все случаи было бы здорово. Но, как вы также указываете, мы могли бы сделать то же самое с универсальным продуктом (структурой). Имеется недопустимое состояние Valid = false, Value! = 0. В этом сценарии было бы легко искоренить, если бы это вызывало проблемы, поскольку 2 ⨯ T мало, даже если оно не такое маленькое, как 1 + T.

Конечно, если бы это была более сложная сумма с множеством случаев и множеством перекрывающихся инвариантов, было бы легче ошибиться и сложнее обнаружить ошибку даже при защитном программировании, поэтому создание невозможных вещей, просто не компилируемых вообще, может сэкономить много волос. тянущий.

Жетон - конечно. Еще один пример проблемы AST (практически любой синтаксический анализатор выигрывает от типов сумм).

Я пишу множество программ, которые принимают ввод, выполняют некоторую обработку и производят некоторый вывод, и я обычно делю это рекурсивно на множество проходов, которые делят ввод на случаи и преобразуют его на основе этих случаев по мере приближения к желаемый результат. Возможно, я буквально не пишу синтаксический анализатор (по общему признанию, иногда я пишу, потому что это весело!), Но я считаю, что проблема AST, как вы выразились, применима к большому количеству кода, особенно при работе с заумной бизнес-логикой, в которой слишком много странных требования и крайние случаи, чтобы поместиться в моей крошечной головке.

Когда я пишу общую библиотеку, она не появляется в API так часто, как выполнение какого-либо ETL или создание какого-то причудливого отчета или обеспечение того, чтобы пользователи в состоянии X выполняли действие Y, если они не отмечены Z. Даже в общая библиотека, хотя я нахожу места, где возможность ограничить внутреннее состояние может помочь, даже если она просто сокращает 10-минутную отладку до 1 секунды «о, компилятор сказал, что я ошибаюсь».

В Go, в частности, одно место, где я бы использовал типы сумм, - это горутина, выбирающая несколько каналов, где мне нужно дать 3 чана одной горутине и 2 - другой. Это помогло бы мне отслеживать, что происходит, чтобы иметь возможность использовать chan pick { a A; b B; c C } более chan A , chan B , chan C хотя может chan stuct { kind MsgKind; a A; b B; c C } выполнять работу в крайнем случае за счет дополнительного места и меньшего количества проверок.

Как насчет проверки списка типов во время компиляции вместо нового типа в качестве дополнения к существующей функции переключения типа интерфейса?

func main() {
    if FlipCoin() == false {
        printCertainTypes(FlipCoin(), int(5))
    } else {
        printCertainTypes(FlipCoin(), string("5"))
    }
}
// this function compiles with main
func printCertainTypes(flip bool, in interface{}) {
    if flip == false {
        switch v := in.(type) {
        case int:
            fmt.Printf(“integer %v\n”, v)
        default:
            fmt.Println(v)
        }
    } else {
        switch v := in.(type) {
        case int:
            fmt.Printf(“integer %v\n”, v)
        case string:
            fmt.Printf(“string %v\n”, v)
        }
    }
}
// this function compiles with main
func printCertainTypes(flip bool, in interface{}) {
    switch v := in.(type) {
    case int:
        fmt.Printf(“integer %v\n”, v)   
    case bool:
        fmt.Printf(“bool %v\n”, v)
    }
    fmt.Println(flip)
    switch v := in.(type) {
    case string:
        fmt.Printf(“string %v\n”, v)
    case bool:
        fmt.Printf(“bool 2 %v\n”, v)
    }
}
// this function emits a type switch not complete error when compiled with main
func printCertainTypes(flip bool, in interface{}) {
    if flip == false {
        switch v := in.(type) {
        case int:
            fmt.Printf(“integer %v\n”, v)
        case bool:
            fmt.Printf(“bool %v\n”, v)
        }
    } else {
        switch v := in.(type) {
        case string:
            fmt.Printf(“string %v\n”, v)
        case bool:
            fmt.Printf(“bool %v\n”, v)
        }
    }
}
// this function emits a type switch not complete error when compiled with main
func printCertainTypes(flip bool, in interface{}) {
    fmt.Println(flip)
    switch v := in.(type) {
    case int:
        fmt.Printf(“integer %v\n”, v)
    case bool:
        fmt.Printf(“bool %v\n”, v)
    }
}

Честно говоря, мы должны изучить способы аппроксимации типов суммы в текущей системе типов и взвесить их за и против. По крайней мере, это дает основу для сравнения.

Стандартное средство - это интерфейс с неэкспортируемым бездействующим методом в качестве тега.

Одним из аргументов против этого является то, что для каждого типа в сумме должен быть определен этот тег. Это не совсем так, по крайней мере, для членов, которые являются структурами, мы могли бы сделать

type Sum interface { sum() }
type sum struct{}
func (sum) sum() {}

и просто вставьте этот тег шириной 0 в наши структуры.

Мы можем добавить к нашей сумме внешние типы, введя оболочку

type External struct {
  sum
  *pkg.SomeType
}

хотя это немного неуклюже.

Если все члены в сумме имеют общее поведение, мы можем включить эти методы в определение интерфейса.

Подобные конструкции позволяют нам говорить, что тип находится в сумме, но не позволяют нам сказать, чего нет в этой сумме. В дополнение к обязательному кейсу nil , тот же трюк встраивания может использоваться внешними пакетами, такими как

import "p"
var member struct {
  p.Sum
}

Внутри пакета мы должны позаботиться о проверке значений, которые компилируются, но являются недопустимыми.

Есть несколько способов восстановить безопасность типов во время выполнения. Я обнаружил включение метода valid() error в определение интерфейса суммы в сочетании с функцией вроде

func valid(s Sum) error {
  switch s.(type) {
  case nil:
    return errors.New("pkg: Sum must be non-nil")
  case A, B, C, ...: // listing each valid member
    return s.valid()
  }
  return fmt.Errorf("pkg: %T is not a valid member of Sum")
}

быть полезным, так как позволяет одновременно заботиться о двух видах валидации. Для участников, которые всегда действительны, мы можем избежать некоторых шаблонов с

type alwaysValid struct{}
func (alwaysValid) valid() error { return nil }

Одна из наиболее частых претензий к этому шаблону заключается в том, что он не делает ясным членство в сумме в годоке. Поскольку он также не позволяет нам исключать членов и требует от нас проверки в любом случае, есть простой способ обойти это: экспортировать фиктивный метод.
Вместо того,

//A Node is one of (list of types).
type Node interface { node() }

записывать

//A Node is only valid if it is defined in this package.
type Node interface { 
  //Node is a dummy method that signifies that a type is a Node.
  Node()
}

Мы не можем помешать кому-либо удовлетворить Node поэтому мы можем также сообщить им, что делает. Хотя это не дает сразу понять, какие типы удовлетворяют Node (без центрального списка), но дает понять, удовлетворяет ли конкретный тип, на который вы сейчас смотрите, Node .

Этот шаблон полезен, когда большинство типов в сумме определены в одном пакете. Если их нет, обычно возвращаются к interface{} , например json.Token или driver.Value . Мы могли бы использовать предыдущий шаблон с типами оболочек для каждого, но в конце он говорит до interface{} так что смысла нет. Если мы ожидаем, что такие значения будут поступать извне пакета, мы можем проявить вежливость и определить фабрику:

//Sum is one of int64, float64, or bool.
type Sum interface{}
func New(v interface{}) (Sum, error) {
  switch v.(type) {
  case nil:
    return errors.New("pkg: Sum must be non-nil")
  case int64, float64, bool:
     return v
  }
  return fmt.Printf("pkg: %T is not a valid member of Sum")
}

Обычно суммы используются для необязательных типов, когда вам нужно различать «без значения» и «значение, которое может быть нулевым». Есть два способа сделать это.

*T позволяет вам обозначать отсутствие значения как указатель nil и (возможно) нулевое значение в результате разграничения указателя, отличного от нуля.

Подобно предыдущим приближениям на основе интерфейсов и различным предложениям по реализации типов сумм в качестве интерфейсов с ограничениями, это требует дополнительного разыменования указателя и возможного выделения кучи.

Для дополнительных опций этого можно избежать, используя технику из пакета sql.

type OptionalT struct {
  Valid bool
  Value T
}

Основным недостатком этого является то, что он позволяет кодировать недопустимое состояние: Valid может иметь значение false, а Value может быть ненулевым. Также можно получить значение Value, когда Valid имеет значение false (хотя это может быть полезно, если вы хотите получить нулевое значение T, если оно не было указано). Случайная установка Valid на false без обнуления Value с последующей установкой Valid на true (или игнорированием) без присвоения Value приводит к случайному повторному появлению ранее отброшенного значения. Это можно обойти, предоставив сеттеры и геттеры для защиты инвариантов типа.

Самая простая форма типов суммы - это когда вы заботитесь об идентичности, а не о значении: перечисления.

Традиционный способ справиться с этим в Go - const / iota:

type Enum int
const (
  A Enum = iota
  B
  C
)

Как и в случае с типом OptionalT здесь нет ненужного косвенного обращения. Как и суммы интерфейса, он не ограничивает домен: есть только три допустимых значения и много недопустимых значений, поэтому нам нужно проверять во время выполнения. Если есть ровно два значения, мы можем использовать bool.

Также существует проблема фундаментальной численности этого типа. A+B == C . Мы можем слишком легко преобразовать нетипизированные интегральные константы в этот тип. Есть много мест, где это желательно, но мы получаем это несмотря ни на что. Немного поработав, мы можем ограничить это только идентификацией:

type Enum struct { v int }
var (
  A = Enum{0}
  B = Enum{1}
  C = Enum{2}
)

Теперь это просто непрозрачные ярлыки. Их можно сравнить, но это все. К сожалению, теперь мы потеряли постоянство, но мы могли бы вернуть это, приложив немного больше усилий:

func A() Enum { return Enum{0} }
func B() Enum { return Enum{1} }
func C() Enum { return Enum{2} }

Мы восстановили неспособность внешнего пользователя изменять имена за счет некоторого шаблона и некоторых вызовов функций, которые имеют высокую степень встроенности.

Однако в некотором смысле это лучше, чем сумма интерфейса, поскольку мы почти полностью закрыли тип. Внешний код может использовать только A() , B() или C() . Они не могут менять местами метки, как в примере с var, и они не могут делать A() + B() и мы можем определять любые методы, которые мы хотим, для Enum . Код в том же пакете по-прежнему может ошибочно создать или изменить значение, но если мы позаботимся о том, чтобы этого не произошло, это первый тип суммы, который не требует кода проверки: если он существует, он действителен .

Иногда у вас есть много ярлыков, и некоторые из них имеют дополнительную дату, а те, которые имеют такие же данные. Скажем, у вас есть значение, которое имеет три состояния без значения (A, B, C), два со строковым значением (D, E) и одно со строковым значением и значением типа int (F). Мы могли бы использовать несколько комбинаций вышеперечисленных тактик, но самый простой способ - это

type Value struct {
  Which int // could have consts for A, B, C, D, E, F
  String string
  Int int
}

Это очень похоже на тип OptionalT выше, но вместо bool у него есть перечисление и несколько полей, которые могут быть установлены (или нет) в зависимости от значения Which . При валидации необходимо следить за тем, чтобы они были установлены (или нет) правильно.

В Go есть много способов выразить «одно из следующего». Некоторые требуют большего ухода, чем другие. Они часто требуют проверки инварианта «один из» во время выполнения или посторонних разыменований. Главный недостаток, который они все разделяют, заключается в том, что, поскольку они моделируются на языке, а не являются его частью, инвариант «один из» не отображается в отражении или переходе / типах, что затрудняет метапрограммирование с помощью их. Чтобы использовать их в метапрограммировании, вам обоим необходимо уметь распознавать и подтверждать правильный вид суммы, а также знать, что это именно то, что вы ищете, поскольку все они очень похожи на действительный код без инварианта «один из».

Если бы типы суммирования были частью языка, их можно было бы отразить и легко извлечь из исходного кода, что привело бы к созданию лучших библиотек и инструментов. Компилятор мог бы сделать ряд оптимизаций, если бы он знал об инварианте "один из". Программисты могут сосредоточиться на важном коде проверки, а не на тривиальной проверке того, что значение действительно находится в правильном домене.

Подобные конструкции позволяют нам говорить, что тип находится в сумме, но не позволяют нам сказать, чего нет в этой сумме. В дополнение к обязательному случаю nil, тот же трюк с встраиванием может использоваться внешними пакетами, такими как
[…]
Внутри пакета мы должны позаботиться о проверке значений, которые компилируются, но являются недопустимыми.

Почему? Мне как автору пакета это кажется «вашей проблемой». Если вы передадите мне io.Reader , чей метод Read вызывает панику, я не собираюсь оправляться от этого и просто позволяю ему паниковать. Точно так же, если вы изо всех сил стараетесь создать недопустимое значение типа, который я объявил - кто я такой, чтобы спорить с вами? Т.е. я считаю, что "я встроил эмулированную замкнутую сумму" проблема, которая редко (если вообще когда-либо) возникает случайно.

При этом вы можете предотвратить эту проблему, изменив интерфейс на type Sum interface { sum() Sum } и каждое значение будет возвращать само себя. Таким образом, вы можете просто использовать возврат sum() , который будет корректно работать даже при встраивании.

Одна из наиболее частых претензий к этому шаблону заключается в том, что он не делает ясным членство в сумме в годоке.

Это может вам помочь .

Основным недостатком этого является то, что он позволяет кодировать недопустимое состояние: Valid может иметь значение false, а Value может быть ненулевым.

Для меня это не недействительное состояние. Нулевые значения не волшебны. Нет разницы, IMO, между sql.NullInt64{false,0} и NullInt64{false,42} . Оба являются допустимыми и эквивалентными представлениями SQL NULL. Если весь код проверяет Valid перед использованием Value, разница не заметна для программы.

Это справедливо и правильно критика , что компилятор не навязывает делать эту проверку (что это , вероятно , было бы , для «реальной» ОПЦИИ / типов сумм), что делает его легче не делать этого. Но если вы все же забудете об этом, я бы не подумал, что лучше случайно использовать нулевое значение, чем случайно использовать ненулевое значение (с возможным исключением типов в форме указателей, поскольку они паникуют при использовании, таким образом громко терпит неудачу - но для них вы все равно должны просто использовать тип в форме голого указателя и использовать nil качестве "неустановленного").

Также существует проблема фундаментальной численности этого типа. A + B == C. Мы можем слишком легко преобразовать нетипизированные интегральные константы в этот тип.

Это теоретическая проблема или она возникла на практике?

Программисты могут сосредоточиться на важном коде проверки, а не на тривиальной проверке того, что значение действительно находится в правильном домене.

Просто FTR, в тех случаях, когда я использую суммы-типы-как-типы-суммы (т.е. проблема не может быть более элегантно смоделирована с помощью интерфейсов золотого разнообразия), я никогда не пишу никакого кода проверки. Точно так же, как я не проверяю нулевое значение указателей, переданных как получатели или аргументы (если это не задокументировано как допустимый вариант). Там, где компилятор вынуждает меня иметь дело с этим (например, проблемы стиля «нет возврата в конце функции»), я паникую в случае по умолчанию.

Лично я считаю Go прагматичным языком, который не просто добавляет функции безопасности ради них самих или потому, что «все знают, что они лучше», но основывается на продемонстрированной потребности. Я думаю, что использовать его прагматично - это нормально.

Стандартное средство - это интерфейс с неэкспортируемым бездействующим методом в качестве тега.

Между интерфейсами и типами сумм есть фундаментальная разница (я не видел, чтобы это упоминалось в вашем сообщении). Когда вы приближаете тип суммы через интерфейс, на самом деле нет способа обработать значение. Как потребитель, вы не знаете, что в нем содержится, и можете только догадываться. Это не лучше, чем просто использовать пустой интерфейс. Полезно только в том случае, если какая-либо реализация может исходить только из того же пакета, который определяет интерфейс, поскольку только тогда вы можете контролировать то, что можете получить.

С другой стороны, иметь что-то вроде:

func foo(val string|int|error) {
    switch v:= val.(type) {
    case string:
        ...
    }
}

Предоставляет потребителю полную возможность использовать значение типа суммы. Его ценность конкретна, она не подлежит интерпретации.

@Merovius
У этих «открытых сумм», которые вы упомянули, есть то, что некоторые люди могут классифицировать как существенный недостаток, поскольку они позволяют злоупотреблять ими для «расползания функций». Именно по этой причине были отклонены необязательные аргументы функции как функция.

У этих «открытых сумм», которые вы упомянули, есть то, что некоторые люди могут классифицировать как существенный недостаток, поскольку они позволяют злоупотреблять ими для «расползания функций». Именно по этой причине были отклонены необязательные аргументы функции как функция.

Мне это кажется довольно слабым аргументом - если ничего другого, то потому, что они существуют, поэтому вы уже разрешаете все, что они позволяют. Действительно, у нас уже есть необязательные аргументы для всех намерений и целей (не то чтобы мне нравился этот шаблон, но очевидно, что он уже возможен в языке).

Между интерфейсами и типами сумм есть фундаментальная разница (я не видел, чтобы это упоминалось в вашем сообщении). Когда вы приближаете тип суммы через интерфейс, на самом деле нет способа обработать значение. Как потребитель, вы не знаете, что в нем содержится, и можете только догадываться.

Я пробовал разобрать это второй раз, но все равно не могу. Почему бы вам не использовать их? Они могут быть обычными, экспортными. Да, они должны быть типами, созданными в вашем пакете (очевидно), но кроме этого, похоже, нет никаких ограничений в том, как вы можете их использовать, по сравнению с фактическими закрытыми суммами.

Я пробовал разобрать это второй раз, но все равно не могу. Почему бы вам не использовать их? Они могут быть обычными, экспортными. Да, они должны быть типами, созданными в вашем пакете (очевидно), но кроме этого, похоже, нет никаких ограничений в том, как вы можете их использовать, по сравнению с фактическими закрытыми суммами.

Что происходит в случае, когда фиктивный метод экспортируется и любая третья сторона может реализовать «тип суммы»? Или вполне реалистичный сценарий, когда член команды не знаком с различными потребителями интерфейса, решает добавить еще одну реализацию в тот же пакет, и экземпляр этой реализации в конечном итоге передается этим потребителям с помощью различных средств кода? Рискну повторить мое кажущееся «неразборчивое» утверждение: «Как потребитель, вы не знаете, что [итоговое значение] на самом деле содержится, и можете только догадываться». Вы знаете, поскольку это интерфейс, и он не говорит вам, кто его реализует.

@Merovius

Просто FTR, в тех случаях, когда я использую суммы-типы-как-типы-суммы (т.е. проблема не может быть более элегантно смоделирована с помощью интерфейсов золотого разнообразия), я никогда не пишу никакого кода проверки. Точно так же, как я не проверяю нулевое значение указателей, переданных как получатели или аргументы (если это не задокументировано как допустимый вариант). Там, где компилятор вынуждает меня иметь дело с этим (например, проблемы стиля «нет возврата в конце функции»), я паникую в случае по умолчанию.

Я не отношусь к этому как к « всегда или никогда» .

Если кто-то, передающий неверный ввод, немедленно взорвется, я не забочусь о проверочном коде.

Но если кто-то, передающий неверный ввод, может в конечном итоге вызвать панику, но он не появится какое-то время, я пишу код проверки, чтобы неправильный ввод был отмечен как можно скорее, и никто не должен был выяснять, что ошибка была внесена 150 фреймы в стеке вызовов (тем более, что им затем, возможно, придется подняться еще на 150 кадров в стеке вызовов, чтобы выяснить, где было введено это неправильное значение).

Потратить полминуты сейчас, чтобы потенциально сэкономить полчаса на отладку позже, прагматично. Особенно для меня, так как я все время совершаю глупые ошибки, и чем раньше я получу образование, тем скорее я смогу совершить следующую глупую ошибку.

Если у меня есть функция, которая берет читателя и сразу начинает его использовать, я не буду проверять nil, но если функция является фабрикой для структуры, которая не будет вызывать читателя, пока не будет вызван определенный метод, я проверьте его на ноль и паникуйте или верните ошибку с чем-то вроде «читатель не должен быть нулем», чтобы причина ошибки была как можно ближе к источнику ошибки.

godoc -analysis

Я в курсе, но не считаю это полезным. Он работал в моей рабочей области 40 минут, прежде чем я нажал ^ C, и его нужно обновлять каждый раз при установке или изменении пакета. Но есть # 20131 (разветвленный именно из этой ветки!).

При этом вы можете предотвратить эту проблему, изменив интерфейс на type Sum interface { sum() Sum } и каждое значение будет возвращать само себя. Таким образом, вы можете просто использовать возврат sum() , который будет корректно работать даже при встраивании.

Я не нашел это таким полезным. Он не дает больше преимуществ, чем явная проверка, и обеспечивает меньшую проверку.

Является ли [тот факт, что вы можете добавлять члены перечисления const / iota] теоретической проблемой, или она возникла на практике?

Этот конкретный вопрос был теоретическим: я пытался перечислить все плюсы и минусы, которые я мог придумать, теоретические и практические. Тем не менее, моя главная мысль заключалась в том, что было много способов попытаться выразить инвариант «один из» в языке, которые используются довольно часто, но ни один из них не был таким простым, как просто использовать его как своего рода тип в языке.

Является ли [тот факт, что вы можете назначить нетипизированный интеграл перечислению const / iota] теоретической проблемой, или она возникла на практике?

Такой появился на практике. На то, чтобы выяснить, что пошло не так, потребовалось немного времени, но на это ушло бы еще меньше времени, если бы компилятор сказал: «вот, эта строка - это та, что не так». Говорят о других способах решения этого конкретного случая, но я не понимаю, как они будут использоваться в целом.

Для меня это не недействительное состояние. Нулевые значения не волшебны. Нет разницы, IMO, между sql.NullInt64{false,0} и NullInt64{false,42} . Оба являются допустимыми и эквивалентными представлениями SQL NULL. Если весь код проверяет Valid перед использованием Value, разница не заметна для программы.

Справедливая и правильная критика в отношении того, что компилятор не требует выполнения этой проверки (что, вероятно, было бы для «реальных» типов опций / сумм), что упрощает выполнение этой проверки. Но если вы все же забудете об этом, я бы не подумал, что лучше случайно использовать нулевое значение, чем случайно использовать ненулевое значение (с возможным исключением типов в форме указателей, поскольку они паникуют при использовании, таким образом громко терпит неудачу - но для них вы все равно должны просто использовать тип в виде голого указателя и использовать nil как "unset").

«Если весь код проверяет действительность перед использованием значения» - вот в чем проскальзывают ошибки и что компилятор может обеспечить. У меня были такие ошибки (хотя и с более крупными версиями этого шаблона, где было более одного поля значений и более двух состояний для дискриминатора). Я верю / надеюсь, что нашел все это во время разработки и тестирования, и ни один не ускользнул в дикую природу, но было бы неплохо, если бы компилятор мог просто сказать мне, когда я сделал эту ошибку, и я мог быть уверен, что единственный способ один из этих Проскользнул мимо, если в компиляторе была ошибка, точно так же, как он сказал бы мне, если бы я попытался присвоить строку переменной типа int.

И, конечно, я предпочитаю *T для необязательных типов, хотя с этим связаны ненулевые затраты, как в пространстве-времени выполнения, так и в удобочитаемости кода.

(В этом конкретном примере код для получения фактического значения или правильного нулевого значения с предложением выбора будет v, _ := nullable.[Value] что является кратким и безопасным.)

Это совсем не то, что я хотел бы. Типы выбора должны быть типами значений,
как в Rust. Их первое слово должно быть указателем на метаданные сборщика мусора, если это необходимо.

В противном случае их использование приведет к снижению производительности, которое может быть
неприемлемо. Для меня проход 10:41 утра, "Джош Бличер Снайдер" <
[email protected]> написал:

С предложением выбора вы можете выбрать, чтобы p или * p давали вам больше
больший контроль над компромиссами памяти.

Причина, по которой интерфейсы выделяют для хранения скалярных значений, заключается в том, что вы не
должны прочитать типовое слово, чтобы решить, является ли другое слово
указатель; см. # 8405 https://github.com/golang/go/issues/8405 для
обсуждение. Те же соображения по реализации, вероятно, применимы к
выбрать тип, что на практике может означать, что p в конечном итоге распределяет и
в любом случае неместные.

-
Вы получаете это, потому что вы являетесь автором темы.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/golang/go/issues/19412#issuecomment-323371837 или отключить звук
нить
https://github.com/notifications/unsubscribe-auth/AGGWB-wQD75N44TGoU6LWQhjED_uhKGUks5sZaKbgaJpZM4MTmSr
.

@urandom

Что происходит в случае, когда фиктивный метод экспортируется и любая третья сторона может реализовать «тип суммы»?

Существует разница между экспортируемым методом и экспортируемым типом. Кажется, мы разговариваем друг с другом. Мне кажется, что это работает нормально, без разницы между открытой и закрытой суммами:

type X interface { x() X }
type IntX int
func (v IntX) x() X { return v }
type StringX string
func (v StringX) x() X { return v }
type StructX struct{
    Foo bool
    Bar int
}
func (v StructX) x() X { return v }

Расширение вне пакета невозможно, но потребители пакета могут использовать, создавать и передавать значения, как и любые другие.

Вы можете встроить X или один из локальных типов, которые ему удовлетворяют, извне, а затем передать его функции в вашем пакете, которая принимает X.

Если эта функция вызывает x, она либо паникует (если сам X был встроен и не установлен ни на что), либо возвращает значение, с которым может работать ваш код, но это не то, что было передано вызывающим, что было бы немного удивительно для вызывающего. (и их код уже вызывает подозрение, если они пытаются что-то подобное, потому что они не читали документацию).

Вызов валидатора, который паникует с сообщением «не делайте этого», кажется наименее удивительным способом справиться с этим и позволяет вызывающему исправить свой код.

Если эта функция вызывает x, она либо вызывает панику […], либо возвращает значение, с которым может работать ваш код, но это не то, что было передано вызывающим, что было бы немного удивительно для вызывающего.

Как я сказал выше: если вы удивлены, что ваше намеренное построение недопустимого значения недействительно, вам необходимо переосмыслить свои ожидания. Но в любом случае это не то, о чем идет речь в этой конкретной дискуссии, и было бы полезно держать отдельные аргументы отдельно. Это было о том, что @urandom сказал, что открытые суммы через интерфейсы с методами тегов не будут подвергаться самоанализу или использоваться другими пакетами. Я считаю это утверждение сомнительным, было бы здорово, если бы его можно было прояснить.

Проблема в том, что кто-то может создать тип, которого нет в сумме, которая компилируется, и который может быть передан вашему пакету.

Без добавления правильных типов сумм в язык есть три варианта обработки.

  1. игнорировать ситуацию
  2. проверить и паниковать / вернуть ошибку
  3. попробуйте «делать то, что вы имеете в виду», неявно извлекая встроенное значение и используя его

3 мне кажется странным сочетанием 1 и 2: я не вижу, что он покупает.

Я согласен с тем, что «Если вы удивлены, что ваше намеренное построение недопустимого значения недействительно, вам нужно переосмыслить свои ожидания», но с 3 может быть очень трудно заметить, что что-то пошло не так, и даже когда вы это делаете было бы трудно понять почему.

2 кажется лучшим, потому что он защищает код от перехода в недопустимое состояние и посылает сигнал, если кто-то испортил ошибку, давая понять, почему они ошибаются и как это исправить.

Я неправильно понимаю цель паттерна или мы просто подходим к этому, исходя из разных философий?

@urandom Я также был бы признателен за разъяснения; Я тоже не уверен на 100% в том, что вы пытаетесь сказать.

Проблема в том, что кто-то может создать тип, которого нет в сумме, которая компилируется, и который может быть передан вашему пакету.

Вы всегда можете это сделать; если есть сомнения, вы всегда можете использовать unsafe, даже с проверяемыми компилятором типами сумм (и я не вижу в этом качественно другого способа построения недопустимых значений из встраивания чего-то, что явно предназначено как сумма, а не инициализации его как действительное значение). Вопрос в том, «как часто это будет создавать проблему на практике и насколько серьезной будет эта проблема». На мой взгляд, с решением, приведенным выше, ответ будет «почти никогда и очень мало» - вы, по-видимому, не согласны, и это нормально. Но в любом случае, похоже, нет особого смысла работать над этим - аргументы и взгляды с обеих сторон этого конкретного пункта должны быть достаточно ясными, и я стараюсь избегать слишком шумных повторений и сосредоточиться на искреннем новые аргументы. Я привел вышеупомянутую конструкцию, чтобы продемонстрировать, что нет никакой разницы в возможности экспорта между первоклассными типами сумм и эмулированными суммами через интерфейсы. Не для того, чтобы показать, что они строго лучше во всех отношениях.

если есть сомнения, вы всегда можете использовать unsafe, даже с проверяемыми компилятором типами сумм (и я не вижу в этом качественно другого способа построения недопустимых значений из встраивания чего-то, что явно предназначено как сумма, а не инициализации его как действительное значение).

Я думаю, что это качественно другое: когда люди злоупотребляют встраиванием таким образом (по крайней мере, с proto.Message и конкретными типами, которые его реализуют), они обычно не думают о том, безопасно ли это и какие инварианты это может нарушить . (Пользователи предполагают, что интерфейсы полностью описывают требуемое поведение, но когда интерфейсы используются как типы объединения или суммы, они часто этого не делают. См. Также https://github.com/golang/protobuf/issues/364.)

Напротив, если кто-то использует пакет unsafe для установки переменной в тип, на который она обычно не может ссылаться, они более или менее явно заявляют, что хотя бы думали о том, что они могут сломать и почему.

@Merovius Возможно, я не совсем

Самым большим преимуществом функции безопасности является то, что она будет отражена и представлена ​​в go / types. Это дает инструментам и библиотекам больше информации для работы. Есть много способов имитировать типы сумм в Go, но все они идентичны коду типа без суммы, поэтому инструментам и библиотеке требуется внешняя информация, чтобы знать, что это тип суммы и должен быть в состоянии распознать конкретный шаблон. используются, но даже эти шаблоны допускают значительные вариации.

Это также сделало бы небезопасным единственный способ создания недопустимого значения: теперь у вас есть обычный код, сгенерированный код и отражение - последние два с большей вероятностью вызовут проблему, поскольку в отличие от человека, они не могут прочитать документацию.

Еще одно побочное преимущество безопасности означает, что компилятор имеет больше информации и может генерировать более быстрый код.

Также существует тот факт, что помимо возможности замены псевдосуммы интерфейсами вы можете заменить псевдосумму «один из этих обычных типов», например json.Token или driver.Value . Таких немного, но там будет на одно место меньше, где необходимо interface{} .

Это также сделало бы небезопасным единственный способ создать недопустимое значение

Я не думаю, что понимаю определение «недопустимое значение», которое приводит к этому утверждению.

@neild, если бы у тебя был

var v pick {
  None struct{}
  A struct { X int; Y *T}
  B int
}

это будет выложено в памяти как

struct {
  activeField int //which of None (0), A (1), or B (2) is the current field
  theInt int // If None always 0
  thePtr *T // If None or B, always nil
}

а с небезопасным вы можете установить thePtr даже если activeField было 0 или 2, или установить значение theInt даже если activeField было 0.

В любом случае это сделало бы недействительными предположения, которые делал бы компилятор, и допустило бы те же теоретические ошибки, которые мы можем иметь сегодня.

Но, как указал @bcmills , если вы используете unsafe, вам лучше знать, что вы делаете, потому что это ядерный вариант.

Я не понимаю, почему unsafe - единственный способ создать недопустимое значение.

var t time.Timer

t - недопустимое значение; t.C не задано, вызов t.Stop вызовет панику и т. Д. Небезопасные действия не требуются.

В некоторых языках есть системы типов, которые делают все возможное, чтобы предотвратить создание «недопустимых» значений. Го не входит в их число. Я не вижу, как профсоюзы значительно продвигают эту иглу. (Конечно, есть и другие причины для поддержки профсоюзов.)

@neild да, извините, я

Я бы сказал, что неверно по отношению к инвариантам типа суммы .

Конечно, отдельные типы в сумме могут находиться в недопустимом состоянии.

Однако поддержание инвариантов типа суммы означает, что они доступны для отражения и перехода / типов, а также для программиста, поэтому манипулирование ими в библиотеках и инструментах поддерживает эту безопасность и предоставляет больше информации для метапрограммиста.

@jimmyfrasche , я говорю, что в отличие от типа суммы, который сообщает вам все возможные типы, интерфейс непрозрачен в том switch чем-то вроде догадки:

func F(sum SumInterface) {
    switch v := sum {
    case Screwdriver:
             ...
    default:
           panic ("Someone implementing a new type which gets passed to F and causes a runtime panic 3 weeks into production")
    }
}

Итак, мне кажется, что большинство проблем, с которыми люди сталкиваются с эмуляцией типа суммы на основе интерфейса, могут быть решены путем взимания платы и / или соглашения. Например, если интерфейс содержит неэкспортированный метод, было бы тривиально выяснить все возможные (да, преднамеренные обходы) реализации. Точно так же для решения большинства проблем с перечислениями на основе йоты простое соглашение «перечисление представляет собой type Foo int с объявлением формы const ( FooA Foo = iota; FooB; FooC ) » позволило бы написать обширные и точные инструменты. для них тоже.

Да, это не эквивалентно реальным типам сумм (среди прочего, они не получат первоклассной поддержки отражения, хотя я действительно не понимаю, насколько это важно), но это означает, что существующие решения кажется, с моей точки зрения, лучше, чем их часто рисуют. И ИМО, стоит изучить это пространство дизайна, прежде чем фактически помещать их в Go 2 - по крайней мере, если они действительно так важны для людей.

(и я хочу еще раз подчеркнуть, что я знаю о преимуществах типов суммы, поэтому нет необходимости повторять их для моей выгоды. Я просто не взвешиваю их так сильно, как другие люди, также вижу недостатки и, следовательно, приходят к разным выводам на одних и тех же данных)

@Merovius , это прекрасная позиция.

Поддержка отражения позволит библиотекам, а также автономным инструментам - линтерам, генераторам кода и т. Д. - получить доступ к информации и запретить ей изменять ее ненадлежащим образом, что не может быть обнаружено статически с какой-либо точностью.

Тем не менее, это хорошая идея для изучения, так что давайте исследуем ее.

Напомним, что наиболее распространенные семейства псевдосумм в Go следующие: (примерно в порядке появления)

  • const / iota перечисление.
  • Интерфейс с методом тега для суммирования по типам, определенным в одном пакете.
  • *T для необязательного T
  • структура с перечислением, значение которого определяет, какие поля могут быть установлены (когда перечисление является логическим и есть только одно другое поле, это еще один вид необязательного T )
  • interface{} который ограничен сумкой для захвата конечного набора типов.

Все они могут использоваться как для типов суммы, так и для типов, не являющихся суммой. Первые два настолько редко используются для чего-либо еще, что может иметь смысл просто предположить, что они представляют собой типы сумм, и принять случайные ложные срабатывания. Для сумм интерфейса он может ограничить его неэкспортируемым методом без параметров или возвратов и без тела на каких-либо членах. Для перечислений имеет смысл распознавать их только тогда, когда они всего лишь Type = iota чтобы не сработать, когда йота используется как часть выражения.

*T для необязательного T будет очень сложно отличить от обычного указателя. Это можно было бы дать условному обозначению type O = *T . Это можно было бы обнаружить, хотя и немного сложно, поскольку псевдоним не является частью типа. type O *T было бы легче обнаружить, но с ним сложнее работать в коде. С другой стороны, все, что нужно сделать, по сути встроено в тип, поэтому от распознавания этого мало что можно получить от инструментария. Давайте просто проигнорируем это. (Дженерики, вероятно, позволят что-то вроде type Optional(T) *T что упростит их "тегирование").

Структуру с перечислением было бы сложно понять в инструментарии, какие поля соответствуют какому значению для перечисления? Мы могли бы упростить это до соглашения о том, что должно быть одно поле для каждого члена в перечислении и что значение перечисления и значение поля должны быть одинаковыми, например:

type Which int
const (
  A Which = iota
  B
  C
)
type Sum struct {
  Which
  A struct{} // has to be included to line up with the value of Which
  B struct { X int; Y float64 }
  C struct { X int; Y int } 
}

Это не получило бы необязательных типов, но мы могли бы использовать специальный случай «2 поля, первое - bool» в распознавателе.

Использование interface{} в качестве суммы захвата было бы невозможно обнаружить без волшебного комментария вроде //gosum: int, float64, string, Foo

В качестве альтернативы может быть специальный пакет со следующими определениями:

package sum
type (
  Type struct{}
  Enum int
  OneOf interface{}
)

и распознают перечисления только в том случае, если они имеют форму type MyEnum sum.Enum , распознают интерфейсы и структуры только в том случае, если они встраивают sum.Type , и распознают только interface{} захватывающие пакеты, такие как type GrabBag sum.OneOf (но для объяснения этого все равно потребуется машинно-узнаваемый комментарий). У этого были бы следующие плюсы и минусы:
Плюсы

  • явно в коде: если он отмечен таким образом, это на 100% тип суммы, без ложных срабатываний.
  • эти определения могут иметь документацию, объясняющую, что они означают, и документация пакета может содержать ссылки на инструменты, которые могут использоваться с этими типами.
  • некоторые будут иметь некоторую видимость в отражении
    Минусы
  • Множество ложных негативов из старого кода и stdlib (который их не использует).
  • Они должны быть использованы, чтобы быть полезными, поэтому внедрение будет медленным и, вероятно, никогда не дойдет до 100%, а эффективность инструментов, распознающих этот специальный пакет, будет зависеть от принятия, так что интересно, хотя эксперимент, но, вероятно, нереально.

Независимо от того, какой из этих двух способов используется для определения типов сумм, давайте предположим, что они были распознаны, и перейдем к использованию этой информации, чтобы увидеть, какие инструменты мы можем создать.

Мы можем грубо разделить инструменты на генеративные (например, стрингер) и интроспективные (например, голинт).

Простейшим генеративным кодом был бы инструмент для заполнения оператора switch пропущенными регистрами. Это может быть использовано редакторами. Когда тип суммы идентифицируется как тип суммы, это тривиально (немного утомительно, но фактическая логика генерации будет такой же, с поддержкой языка или без нее).

Во всех случаях можно было бы сгенерировать функцию, которая проверяет инвариант "один из".

Для перечислений может быть больше инструментов, таких как стрингер. В https://github.com/golang/go/issues/19814#issuecomment -291002852 я упомянул некоторые возможности.

Самым большим генеративным инструментом является компилятор, который мог бы производить лучший машинный код с этой информацией, но ну ладно.

Я не могу сейчас думать ни о ком другом. Есть ли что-нибудь в чьем-либо списке желаний?

Очевидным кандидатом для интроспекции является анализ полноты. Без языковой поддержки на самом деле требуется два разных типа линтинга.

  1. убедиться, что все возможные состояния обрабатываются
  2. убедитесь, что не созданы недопустимые состояния (что сделало бы недействительной работу, выполненную 1)

1 тривиально, но для этого потребуются все возможные состояния и случай по умолчанию, потому что 2 нельзя проверить на 100% (даже игнорируя небезопасно), и вы не можете ожидать, что весь код, использующий ваш код, в любом случае запускает этот линтер.

2 не мог действительно отслеживать значения с помощью отражения или идентифицировать весь код, который мог бы сгенерировать недопустимое состояние для суммы, но он мог бы поймать множество простых ошибок, например, если вы вставляете тип суммы, а затем вызываете с ним функцию, он мог бы сказать «вы написали pkg.F (v), но имели в виду pkg.F (v.EmbeddedField)» или «вы передали 2 в pkg.F, используйте pkg.B». Для структуры он не мог ничего сделать, чтобы обеспечить инвариант, что одно поле устанавливается за раз, за ​​исключением действительно очевидных случаев, таких как «вы включаете What, и в случае X вы устанавливаете поле F на ненулевое значение. ". Он может настаивать на использовании сгенерированной функции проверки при приеме значений извне пакета.

Другая важная вещь появится в Годоке. godoc уже группирует const / iota и # 20131 поможет с псевдосуммами интерфейса. На самом деле нет ничего общего с версией структуры, которая не была бы явной в определении, кроме как указать инвариант.

а также автономные инструменты - линтеры, генераторы кода и т. д.

Нет. Статическая информация присутствует, для этого вам не нужна система типов (или рефлексия), соглашение работает нормально. Если ваш интерфейс содержит неэкспортированные методы, любой статический инструмент может рассматривать это как замкнутую сумму (потому что это действительно так) и выполнять любой анализ / генерацию кода, который вам может понадобиться. То же самое с соглашением о йота-перечислениях.

Отражение предназначено для информации о типе среды выполнения - и в некотором смысле компилятор стирает необходимую информацию, чтобы здесь работали суммы по соглашению (поскольку он не дает вам доступа к списку функций или объявленных типов или объявленных констант), которые поэтому я согласен с тем, что фактические суммы позволяют это сделать.

(также, FTR, в зависимости от варианта использования, у вас все еще может быть инструмент, который использует статически известную информацию для генерации необходимой информации о времени выполнения - например, он может перечислять типы, которые имеют требуемый метод тега, и генерировать таблицу поиска для них. Но я не понимаю, каков будет вариант использования, поэтому трудно оценить практичность этого).

Итак, мой вопрос был намеренно задан: каков будет вариант использования, если эта информация будет доступна во время выполнения?

Тем не менее, это хорошая идея для изучения, так что давайте исследуем ее.

Когда я сказал «исследуйте это», я не имел в виду «перечислите их и спорите о них в вакууме», я имел в виду «реализовать инструменты, использующие эти соглашения, и посмотреть, насколько они полезны / необходимы / практичны».

Преимущество отчетов об

Вы пропускаете часть «попытки использовать существующие механизмы для этого». Вы хотите иметь статические проверки полноты сумм (проблема). Напишите инструмент, который находит интерфейсы с неэкспортированными методами, выполняет проверки полноты для любого переключателя типов, в котором он используется, какое-то время используйте этот инструмент (используйте для него существующие механизмы). Напишите, где не удалось.

Я думал вслух и начал работу над статическим распознавателем на основе тех мыслей, которые могут использовать инструменты. Полагаю, я неявно искал обратную связь и новые идеи (и это окупилось повторным созданием информации, необходимой для размышлений).

FWIW, если бы я был там, где вы, я бы просто проигнорировал сложные случаи и сосредоточился на вещах, которые работают: а) неэкспортируемые методы в интерфейсах и б) простые const-iota-enums, которые имеют int в качестве базового типа и единственную const- объявление ожидаемого формата. Использование инструмента потребует использования одного из этих двух обходных путей, но ИМО это нормально (чтобы использовать инструмент компилятора, вам также необходимо явно использовать суммы, так что это кажется нормальным).

Это определенно хорошее место для начала, и его можно набрать после запуска по большому набору пакетов и просмотра количества ложных срабатываний / отрицаний.

https://godoc.org/github.com/jimmyfrasche/closed

Все еще очень большая работа. Я не могу обещать, что мне не придется добавлять дополнительные параметры в конструктор. Вероятно, в нем больше ошибок, чем тестов. Но с ним можно поиграть.

Есть пример использования в cmds / closed-exporer, который также перечислит все закрытые типы, обнаруженные в пакете, указанном его путем импорта.

Я начал просто обнаруживать все интерфейсы с помощью неэкспортированных методов, но они довольно распространены, и хотя некоторые явно были типами сумм, другие явно не были. Если бы я просто ограничился соглашением о методе пустых тегов, я потерял много типов сумм, поэтому решил записать их отдельно и немного обобщить пакет за пределы типов сумм до закрытых типов.

С перечислениями я пошел другим путем и просто записал все небитовые константы определенного типа. Я также планирую раскрыть обнаруженные битовые наборы.

Он еще не обнаруживает необязательные структуры или определенные пустые интерфейсы, поскольку для них требуется какой-то комментарий маркера, но он делает особый случай с теми, что в stdlib.

Я начал просто обнаруживать все интерфейсы с помощью неэкспортированных методов, но они довольно распространены, и хотя некоторые явно были типами сумм, другие явно не были.

Было бы полезно, если бы вы могли привести некоторые примеры, которых не было.

@Merovius, извини, я не сохранил список. Я нашел их, запустив stdlib.sh (в cmds / closed-explorer). Если в следующий раз я наткнусь на хороший пример, я опубликую его.

Те, которые я не рассматриваю как типы суммы, были неэкспортируемыми интерфейсами, которые использовались для подключения одной из нескольких реализаций: ничего не заботило то, что было в интерфейсе, просто было что-то, что удовлетворяло его. Они очень часто использовались как интерфейсы, а не как суммы, а просто были закрыты, потому что они не были экспортированы. Возможно, это различие без разницы, но я всегда могу передумать после дальнейшего расследования.

@jimmyfrasche Я бы сказал, что к ним следует относиться как к закрытым суммам. Я бы сказал, что если они не заботятся о динамическом типе (т.е. вызывают только методы в интерфейсе), то статический линтер не будет жаловаться, поскольку «все переключатели исчерпывающие» - так что нет никаких недостатков в их лечении. в виде закрытых сумм. Если, Ото, они иногда типа-переключатель и оставить из дела, жалуясь бы правильно - что бы точно быть такой вещью ЛИНТЕР предполагается поймать.

Я хотел бы замолвить пару слов об изучении того, как типы объединения могут уменьшить использование памяти. Я пишу интерпретатор на Go, и у меня есть тип значения, который обязательно реализуется как интерфейс, потому что значения могут быть указателями на разные типы. Предположительно это означает, что значение [] занимает в два раза больше памяти по сравнению с упаковкой указателя с помощью небольшого битового тега, чем вы могли бы сделать в C. Кажется, много?

В спецификации языка нет необходимости упоминать об этом, но похоже, что сокращение использования памяти массива вдвое для некоторых небольших типов объединений может быть довольно убедительным аргументом в пользу объединений? Он позволяет вам делать то, что, насколько я знаю, сегодня невозможно сделать в Go. Напротив, реализация объединений поверх интерфейсов может помочь с правильностью и понятностью программы, но не делает ничего нового на машинном уровне.

Я не тестировал производительность; просто указываю направление для исследования.

Вместо этого вы можете реализовать Value как unsafe.Pointer.

6 февраля 2018 г. в 15:54 «Брайан Слесинский» [email protected] написал:

Я хотел бы замолвить пару слов об изучении того, как типы объединения могут уменьшить
использование памяти. Я пишу интерпретатор на Go и имею тип значения, который
обязательно реализуется как интерфейс, потому что значения могут быть указателями
к разным типам. Предположительно это означает, что значение [] занимает вдвое больше
память по сравнению с упаковкой указателя с помощью небольшого битового тега, как вы могли бы
в C. Вроде много?

В спецификации языка нет необходимости упоминать об этом, но это похоже на сокращение памяти.
использование массива пополам для некоторых небольших типов объединения может быть довольно приятным
веский аргумент в пользу профсоюзов? Это позволяет вам делать то, что, насколько я
знать, что сегодня в го невозможно. Напротив, внедрение профсоюзов на
верх интерфейсов может помочь с корректностью программы и
понятность, но не делает ничего нового на машинном уровне.

Я не тестировал производительность; просто указывая направление для
исследовать.

-
Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/golang/go/issues/19412#issuecomment-363561070 или отключить звук
нить
https://github.com/notifications/unsubscribe-auth/AGGWBz-L3t0YosVIJmYNyf2iQ-YgIXLGks5tSLv9gaJpZM4MTmSr
.

@skybrian Это кажется довольно самонадеянным в отношении реализации типов суммы. Это требует не только типов суммы, но и того, что компилятор распознает особый случай только указателей в сумме и оптимизирует их как упакованный указатель - и требует, чтобы GC знал, сколько бит-тегов требуется в указателе. , чтобы замаскировать их. Типа, я действительно не вижу, чтобы это происходило, ТБХ.

Это оставляет вас с: типы сумм, вероятно, будут помечены объединениями и, вероятно, будут занимать столько же места в срезе, как сейчас. Если срез не однороден, но тогда вы также можете использовать более конкретный тип среза прямо сейчас.

Так что да. В очень особых случаях вы можете сэкономить немного памяти, если вы специально оптимизируете для них, но, похоже, вы также можете просто вручную оптимизировать для этого, если вам это действительно нужно.

@DemiMarie unsafe.Pointer не работает в App Engine, и в любом случае он не позволит вам упаковывать биты, не испортив сборщик мусора. Даже если бы это было возможно, он не был бы портативным.

@Merovius: да, это требует изменения среды выполнения и сборщика мусора, чтобы понять структуру упакованной памяти. В том-то и дело; указатели управляются средой выполнения Go, поэтому, если вы хотите добиться большего, чем интерфейсы, безопасным способом, вы не можете сделать это в библиотеке или компиляторе.

Но я с готовностью признаю, что написание быстрого интерпретатора - необычный вариант использования. Может, есть и другие? Похоже, что хороший способ стимулировать языковую функцию - это находить вещи, которые сегодня нелегко сделать в Go.

Это правда.

Я считаю, что Go - не лучший язык для написания интерпретатора,
из-за дикой динамики такого программного обеспечения. Если вам нужна высокая производительность,
ваши горячие петли должны быть написаны на ассемблере. Есть ли у тебя какая-то причина?
нужно написать интерпретатор, работающий на App Engine?

6 февраля 2018 г. в 18:15 «Брайан Слесинский» [email protected] написал:

@DemiMarie https://github.com/demimarie unsafe.Pointer не работает в приложении
Двигатель, и в любом случае он не даст вам упаковывать биты без
испортил сборщик мусора. Даже если бы это было возможно, не было бы
портативный.

@metrovius да, это требует изменения среды выполнения и сборщика мусора
чтобы понять схемы упакованной памяти. В том-то и дело; указатели
управляется средой выполнения Go, поэтому, если вы хотите добиться большего, чем интерфейсы в
безопасный способ, вы не можете сделать это в библиотеке или в компиляторе.

Но я с готовностью признаю, что написание быстрого интерпретатора - редкость.
кейс. Может, есть и другие? Кажется, это хороший способ мотивировать
языковая функция заключается в том, чтобы находить вещи, которые сегодня нелегко сделать в Go.

-
Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/golang/go/issues/19412#issuecomment-363598572 или отключить звук
нить
https://github.com/notifications/unsubscribe-auth/AGGWB65jRKg_qVPWTiq8LbGk3YM1RUasks5tSN0tgaJpZM4MTmSr
.

Я считаю предложение @rogpeppe весьма привлекательным. Мне также интересно, есть ли возможность получить дополнительные преимущества, помимо тех, которые уже определены @griesemer.

В предложении говорится: «Набор методов типа сумма содержит пересечение набора методов
всех его типов компонентов, исключая любые методы, которые имеют одинаковые
имя, но разные подписи. ".

Но тип - это больше, чем просто набор методов. Что, если бы тип суммы поддерживал пересечение операций, поддерживаемых его типами компонентов?

Например, рассмотрим:

var x int|float64

Идея в том, что следующее будет работать.

x += 5

Это было бы эквивалентно написанию переключателя полного типа:

switch i := x.(type) {
case int:
    x = i + 5
case float64:
    x = i + 5
}

Другой вариант включает переключение типа, в котором тип компонента сам является типом суммы.

type Num int | float64
type StringOrNum string | Num 
var x StringOrNum

switch i := x.(type) {
case string:
    // Do string stuff.
case Num:
    // Would be nice if we could use i as a Num here.
}

Кроме того, я думаю, что потенциально существует действительно хорошая синергия между типами сумм и системой обобщений, которая использует ограничения типов.

var x int|float64

А как насчет var x, y int | float64 ? Какие здесь правила при их добавлении? Какое преобразование с потерями выполняется (и почему)? Какой будет тип результата?

Go не выполняет автоматические преобразования в выражениях (как это делает C) специально - на эти вопросы нелегко ответить, и это приводит к ошибкам.

И для еще большего удовольствия:

var x, y, z int|string|rune
x = 42
y = 'a'
z = "b"
fmt.Println(x + y + z)
fmt.Println(x + z + y)
fmt.Println(y + x + z)
fmt.Println(y + z + x)
fmt.Println(z + x + y)
fmt.Println(z + y + x)

Все int , string и rune имеют оператор + ; что это за печать, почему и, главное, как результат не может полностью сбить с толку?

А как насчет var x, y int | float64 ? Какие здесь правила при их добавлении? Какое преобразование с потерями выполняется (и почему)? Какой будет тип результата?

@Merovius не выполняется неявное преобразование с потерями, хотя я вижу, как моя формулировка может создать такое впечатление, извините. Здесь простой x + y не будет компилироваться, потому что он подразумевает возможное неявное преобразование. Но компилируется одно из следующего:

z = int(x) + int(y)
z = float64(x) + float64(y)

Точно так же ваш пример xyz не будет компилироваться, потому что он требует возможных неявных преобразований.

Я думаю, что «поддержал пересечение поддерживаемых операций» звучит неплохо, но не совсем передает то, что я имел в виду. Добавление чего-то вроде «компилируется для всех типов компонентов» помогает описать, как, на мой взгляд, это могло бы работать.

Другой пример: все типы компонентов представляют собой срезы и карты. Было бы неплохо иметь возможность вызывать len для типа суммы без переключения типа.

Все int, string и rune имеют оператор +; что это за печать, почему и, главное, как результат не может полностью сбить с толку?

Просто хотел добавить, что мой "Что, если бы тип суммы поддерживал пересечение операций, поддерживаемых его типами компонентов?" был вдохновлен описанием типа в Go Spec: «Тип определяет набор значений вместе с операциями и методами, специфичными для этих значений».

Я пытался подчеркнуть, что тип - это больше, чем просто значения и методы, и поэтому тип суммы может попытаться уловить общность этого другого материала из своих типов компонентов. Этот «другой материал» более сложен, чем просто набор операторов.

Другой пример - сравнение с nil:

var x []int | []string
fmt.Println(x == nil)  // Prints true
x = []string(nil)
fmt.Println(x == nil)  // Still prints true

Оба типа компонентов имеют значение По крайней мере, один тип сопоставим с nil, поэтому мы позволяем сравнивать тип суммы с nil без переключения типа. Конечно, это несколько расходится с тем, как в настоящее время ведут себя интерфейсы, но это может быть неплохо для https://github.com/golang/go/issues/22729

Изменить: тестирование на равенство является здесь плохим примером, поскольку я думаю, что оно должно быть более разрешительным и требовать только потенциального соответствия от одного или нескольких типов компонентов. Назначение зеркал в этом отношении.

Проблема в том, что результат будет либо а) иметь те же проблемы, что и автоматические преобразования, или б) будет чрезвычайно (и IMO сбивает с толку) ограниченным по объему, а именно, все операторы будут работать только с нетипизированными литералами, в лучшем случае.

У меня также есть еще одна проблема, которая заключается в том, что это еще больше ограничит их устойчивость к эволюции составляющих их типов - теперь единственные типы, которые вы могли бы когда-либо добавлять, сохраняя обратную совместимость, - это те, которые разрешают все операции составляющих их типов.

Мне все это кажется действительно беспорядочным, с очень небольшой (если вообще есть) ощутимой пользой.

теперь единственные типы, которые вы могли бы когда-либо добавлять, сохраняя обратную совместимость, - это те, которые разрешают все операции составляющих их типов.

Да, и еще одно уточнение: это означает, что вы никогда не сможете решить, хотите ли вы расширить параметр, или возвращаемый тип, или переменную, или… от одноэлементного типа до суммы. Поскольку добавление любого нового типа приведет к тому, что некоторые операции (например, присваивания) не будут скомпилированы.

@Merovius обратите внимание, что вариант проблемы совместимости уже существует с исходным предложением, потому что «набор методов типа суммы содержит пересечение набора методов
всех его типов компонентов ". Поэтому, если вы добавите новый тип компонента, который не реализует этот набор методов, это будет несовместимое изменение.

Да, и еще одно уточнение: это подразумевает, что вы никогда не сможете решить, хотите ли вы расширить параметр, возвращаемый тип или переменную, или… от одноэлементного типа до суммы. Поскольку добавление любого нового типа приведет к тому, что некоторые операции (например, присваивания) не будут скомпилированы.

Поведение при назначении останется таким, как описано в @rogpeppe, но в целом я не уверен, что понимаю этот момент.

По крайней мере, я думаю, что первоначальное предложение rogpeppe должно быть уточнено в отношении поведения типа суммы вне переключателя типа. Присвоение и набор методов описаны, но это все. А как насчет равенства? Я думаю, мы можем добиться большего, чем то, что делает интерфейс {}:

var x int | float64
fmt.Println(x == "hello")  // compilation error?
x = 0.0
fmt.Println(x == 0) // true or false?  I vote true :-)

Поэтому, если вы добавите новый тип компонента, который не реализует этот набор методов, это будет несовместимое изменение.

Вы всегда можете добавить методы, но не можете перегружать операторы для работы с новыми типами. В этом и заключается разница - в их предложении вы можете вызывать только общие методы для значения суммы (или присваивать ему), если вы не развернете его с помощью утверждения типа / -switch. Таким образом, пока добавляемый вами тип имеет необходимые методы, это не будет критическим изменением. В вашем предложении это все равно будет критическим изменением, потому что пользователи могут использовать операторы, которые вы не можете перегрузить.

(вы можете указать, что добавление типов к сумме все равно будет критическим изменением, потому что переключатели типов не будут содержать в себе новый тип. Именно поэтому я тоже не поддерживаю исходное предложение - я не хочу закрытых сумм именно по этой причине)

Поведение присваивания останется таким, как описано в @rogpeppe

Их предложение говорит только о назначении на сумму стоимость, я говорю о присвоении от суммарной стоимости (на одной из ее составных частей). Я согласен с тем, что их предложение также не позволяет этого, но разница в том, что их предложение не о добавлении такой возможности. то есть мой аргумент состоит в том, что предлагаемая вами семантика не особенно полезна, потому что на практике их использование сильно ограничено.

fmt.Println(x == "hello") // compilation error?

Вероятно, это также будет добавлено к их предложению. У нас уже есть эквивалентный частный случай для интерфейсов , а именно

Значение x неинтерфейсного типа X и значение t типа интерфейса T сопоставимы, если значения типа X сопоставимы и X реализует T. Они равны, если динамический тип t идентичен X, а динамическое значение t равно x .

fmt.Println(x == 0) // true or false? I vote true :-)

Предположительно ложь. Учитывая, что аналогичные

var x int|float64 = 0.0
y := 0
fmt.Println(x == y)

должна быть ошибка компиляции (как мы пришли к выводу выше), этот вопрос действительно имеет смысл только при сравнении с нетипизированными числовыми константами. На этом этапе все зависит от того, как это будет добавлено в спецификацию. Вы можете возразить, что это похоже на присвоение константы типу интерфейса и, следовательно, у него должен быть тип по умолчанию (и тогда сравнение будет ложным). Какая ИМО более чем хороша, мы уже принимаем эту ситуацию сегодня без особой неразберихи. Однако вы также можете добавить в спецификацию случай для нетипизированных констант, который охватывал бы случай присвоения / сравнения их сумм и таким образом решал бы вопрос.

Однако ответ на этот вопрос в любом случае не требует разрешения всех выражений, использующих типы сумм, которые могут иметь смысл для составных частей.

Но повторюсь: я не выступаю за другое предложение по суммам. Я возражаю против этого.

fmt.Println(x == "hello") // compilation error?

Вероятно, это также будет добавлено к их предложению.

Исправление: спецификация уже охватывает эту ошибку компиляции, учитывая, что она содержит инструкцию

При любом сравнении первый операнд должен быть назначен типу второго операнда, или наоборот.

@Merovius, вы

fmt.Println(x == 0) // true or false? I vote true :-)

Предположительно ложь. Учитывая, что аналогичные

var x int|float64 = 0.0
y := 0
fmt.Println(x == y)
должна быть ошибка компиляции (как мы сделали выше),

Я не считаю этот пример очень убедительным, потому что, если вы измените первую строку на var x float64 = 0.0 вы можете использовать те же рассуждения, чтобы утверждать, что сравнение float64 с 0 должно быть ложным. (Незначительные моменты: (a) Я предполагаю, что вы имели в виду float64 (0) в первой строке, поскольку 0.0 присваивается int. (B) x == y не должно быть ошибкой компиляции в вашем примере. Однако он должен печатать false.)

Я думаю, ваша идея о том, что «это похоже на присвоение константы типу интерфейса и, следовательно, у него должен быть тип по умолчанию», более убедительна (при условии, что вы имели в виду тип суммы), поэтому пример будет следующим:

var x,y int|float64 = float64(0), 0
fmt.Println(x == y) // ложь

Я все равно утверждаю, что x == 0 должно быть правдой. Моя ментальная модель такова, что типу присваивается значение 0 как можно позже. Я понимаю, что это противоречит текущему поведению интерфейсов, и именно поэтому я поднял его. Я согласен с тем, что это не привело к «большому беспорядку», но аналогичная проблема сравнения интерфейсов с nil привела к довольно большой путанице. Я считаю, что мы увидим аналогичную путаницу при сравнении с 0, если появятся типы сумм и будет сохранена старая семантика равенства.

Я не считаю этот пример очень убедительным, потому что если вы измените первую строку на var x float64 = 0.0, вы можете использовать те же рассуждения, чтобы утверждать, что сравнение float64 с 0 должно быть ложным.

Я не сказал, что должен , я сказал, что, вероятно, будет , учитывая то, что я считаю наиболее вероятным компромиссом между простотой / полезностью для того, как их предложение будет реализовано. Я не пытался выносить оценочное суждение. Фактически, если бы с такими же простыми правилами мы могли бы заставить его печатать истину, я бы, вероятно, предпочел бы это. Я просто не оптимистичен.

Обратите внимание, что сравнение float64(0) с int(0) (т.е. пример с суммой, замененной на var x float64 = 0.0 ) не является false , хотя это время компиляции ошибка (как и должно быть). Это именно моя точка зрения ; ваше предложение действительно полезно только в сочетании с нетипизированными константами, потому что ни для чего другого оно не будет компилироваться.

(a) Я полагаю, вы имели в виду float64 (0) в первой строке, поскольку 0.0 присваивается int.

Конечно (я предполагал, что семантика ближе к текущему «типу по умолчанию» для константных выражений, но я согласен с тем, что текущая формулировка этого не подразумевает).

(b) x == y не должно быть ошибкой компиляции в вашем примере. Однако он должен печатать false.)

Нет, это должна быть ошибка времени компиляции. Вы сказали, что операция e1 == y , где e1 является выражением типа суммы, должна быть разрешена тогда и только тогда, когда выражение будет компилироваться с любым выбором составляющего типа. Учитывая, что в моем примере x имеет тип int|float64 а y имеет тип int и учитывая, что float64 и int несопоставимы, это условие явно нарушено.

Чтобы выполнить эту компиляцию, вам нужно будет отбросить условие, что замена любого составляющего типизированного выражения тоже должна компилироваться; в этот момент мы оказываемся в ситуации, когда нам необходимо установить правила того, как типы продвигаются или конвертируются при использовании в этих выражениях (также известных как «беспорядок C»).

Прошлый консенсус заключался в том, что типы суммы не очень сильно добавляют к типам интерфейсов.

Они действительно не подходят для большинства случаев использования Go: тривиальные сетевые службы и утилиты. Но как только система вырастет в размерах, есть большая вероятность, что они пригодятся.
В настоящее время я пишу сильно распределенный сервис с гарантиями согласованности данных, реализованными с помощью большого количества логики, и я попал в ситуацию, когда они были бы удобны. Эти NPD стали слишком раздражать по мере роста сервиса, и мы не видим разумного способа разделить его.
Я имею в виду, что гарантии системы типов Go слишком слабы для чего-то более сложного, чем типичные примитивные сетевые службы.

Но история с Rust показывает, что использовать типы сумм для NPD и обработки ошибок, как это делается в Haskell, - плохая идея: существует типичный естественный императивный рабочий процесс, и подход Haskellish не подходит для него.

Пример

рассмотрим iotuils.WriteFile -подобную функцию в псевдокоде. Императивный поток будет выглядеть так

file = open(name, os.write)
if file is error
    return error("cannot open " + name + " writing: " + file.error)
if file.write(data) is error:
    return error("cannot write into " + name + " : " + file.error)
return ok

и как это выглядит в Rust

match open(name, os.write)
    file
        match file.write(data, os.write)
            err
                return error("cannot open " + name + " writing: " + err)
            ok
                return ok
    err
        return error("cannot write into " + name + " : " + err)

это безопасно, но некрасиво.

И мое предложение:

type result[T, Err] oneof {
    default T
    Error Err
}

и как могла бы выглядеть программа ( result[void, string] = !void )

file := os.Open(name, ...)
if !file {
    return result.Error("cannot open " + name + " writing: " + file.Error)
}
if res := file.Write(data); !res {
    return result.Error("cannot write into " + name + " : " + res.Error)
}
return ok

Здесь ветвь по умолчанию является анонимной, а к ветке с ошибкой можно получить доступ с помощью .Error (как только станет известно, что результатом будет ошибка). Как только становится известно, что файл был успешно открыт, пользователь может получить к нему доступ через саму переменную. Во-первых, если мы убедимся, что file был успешно открыт, или завершим работу в противном случае (и, таким образом, дальнейшие операторы знают, что файл не является ошибкой).

Как видите, этот подход сохраняет императивный поток и обеспечивает безопасность типов. Обработка NPD может быть произведена аналогичным образом:

type Reference[T] oneof {
    default T
    nil
}
// Reference[T] = *T

обработка аналогична результату

@sirkon , ваш пример на Rust не убеждает меня в том, что с простыми типами сумм, как в Rust, что-то не так. Скорее, это предполагает, что сопоставление с образцом для типов суммы можно сделать более похожим на Go, используя операторы if . Что-то вроде:

ferr := os.Open(name, ...)
if err(e) := ferr {           // conditional match and unpack, initializing e
  return fmt.Errorf("cannot open %v: %v", name, e)
}
ok(f) := ferr                  // unconditional match and unpack, initializing f
werr := f.Write(data)
...

(В духе типов суммы было бы ошибкой компиляции, если бы компилятор не смог доказать, что безусловное совпадение всегда успешно, потому что остается ровно один регистр.)

Для базовой проверки ошибок это не похоже на улучшение по сравнению с несколькими возвращаемыми значениями, поскольку оно на одну строку длиннее и объявляет еще одну локальную переменную. Однако было бы лучше масштабироваться для нескольких случаев (путем добавления дополнительных операторов if), и компилятор мог бы проверить, что все случаи обрабатываются.

@sirkon

Они действительно не подходят для большинства случаев использования Go: тривиальные сетевые службы и утилиты. Но как только система вырастет в размерах, есть большая вероятность, что они пригодятся.
[…]
Я имею в виду, что гарантии системы типов Go слишком слабы для чего-то более сложного, чем типичные примитивные сетевые службы.

Подобные заявления являются излишне конфронтационными и унизительными. Они также смущают, TBH, потому что на Go написаны очень большие и нетривиальные сервисы. А учитывая, что значительная часть его разработчиков работает в Google, вы должны просто предположить, что они лучше вас знают, подходит ли он для написания больших и нетривиальных сервисов. Go может не охватывать все варианты использования (и не должен, IMO), но эмпирически он работает не только для «примитивных сетевых сервисов».

Обработка NPD может быть произведена аналогичным образом.

Я думаю, это действительно показывает, что ваш подход на самом деле не приносит никакой пользы. Как вы отметили, он просто добавляет другой синтаксис для разыменования. Но AFAICT ничто не мешает программисту использовать этот синтаксис с нулевым значением (что, по-видимому, все еще вызывает панику). т.е. каждая программа, которая действительна с использованием *p , также действительна с использованием p.T (или это p.default ? Трудно сказать, в чем конкретно заключается ваша идея) и наоборот.

Единственное преимущество, которое типы сумм могут добавить к обработке ошибок и разыменованию nil, состоит в том, что компилятор может заставить вас доказать, что операция безопасна, путем сопоставления с образцом. Предложение, которое опускает это принуждение, похоже, не вносит существенных нововведений в таблицу (возможно, это хуже, чем использование открытых сумм через интерфейсы), в то время как предложение, которое действительно включает это, - это именно то, что вы описываете как «уродливое».

@Merovius

А учитывая, что значительная часть его разработчиков работает в Google, вы должны просто предположить, что они знают лучше вас,

Блаженны верующие.

Как вы отметили, он просто добавляет другой синтаксис для разыменования.

опять таки

var written int64
...
res := os.Stdout.Write(data) // Write([]byte) -> Result[int64, string] ≈ !int64
written += res // Will not compile as res is a packed result type
if !res {
    // we are living on non-default res branch thus the only choice left is the default
    return Result.Error(...)
}
written += res // is OK

@skybrian

ferr := os.Open(...)

именно эта промежуточная переменная заставила меня отказаться от этой идеи. Как видите, мой подход специально предназначен для обработки ошибок и нуля. Эти крошечные задачи слишком важны и заслуживают особого внимания IMO.

@sirkon Похоже, вы очень мало заинтересованы в разговоре с людьми

Давайте вести беседу вежливо и избегать неконструктивных комментариев. Мы можем не соглашаться друг с другом, но при этом поддерживать респектабельный дискурс. https://golang.org/conduct.

А учитывая, что значительная часть его разработчиков работает в Google, вы должны просто предположить, что они знают лучше вас

Я сомневаюсь, что вы могли бы привести такой аргумент в Google.

@hasufell, этот парень из Германии, где нет крупных ИТ-компаний с дерьмовыми интервью, чтобы накачать эго интервьюера и гигантский менеджмент, вот почему эти слова.

@sirkon то же самое и для вас. Ad-hominem и социальные аргументы бесполезны. Это больше, чем проблема CoC. Я видел, как такого рода «социальные аргументы» всплывают довольно часто, когда речь идет об основном языке: разработчики компиляторов знают лучше, разработчики языков знают лучше, люди Google знают лучше.

Нет, они этого не делают. Нет интеллектуального авторитета. Есть просто власть принятия решений. Преодолей это.

Скрытие нескольких комментариев для сброса разговора (и спасибо @agnivade за попытку вернуть его на рельсы).

Ребята, пожалуйста, рассмотрите свою роль в этих обсуждениях в свете наших ценностей Gopher : у каждого в сообществе есть перспектива, и мы должны стремиться проявлять уважение и милосердие в том, как мы интерпретируем и реагируем друг на друга.

Разрешите, пожалуйста, добавить мои 2 цента к этому обсуждению:

Нам нужен способ сгруппировать различные типы вместе по функциям, отличным от их наборов методов (как в случае с интерфейсами). Новая функция группировки должна позволять включать примитивные (или базовые) типы, у которых нет никаких методов, и типы интерфейсов, которые должны быть отнесены к категории аналогичных. Мы можем сохранить примитивные типы (логические, числовые, строковые и даже [] byte, [] int и т. Д.) Такими, какие они есть, но позволить абстрагироваться от различий между типами, когда определение типа группирует их в семейство.

Я предлагаю добавить в язык что-то вроде конструкции типа _family_.

Синтаксис

Семейство типов можно определить так же, как и любой другой тип:

type theFamilyName family {
    someType
    anotherType
}

Формальный синтаксис будет примерно таким:
FamilyType = "family" "{" { TypeName ";" } "}" .

Семейство типов может быть определено внутри сигнатуры функции:

func Display(s family{string; fmt.Stringer}) { /* function body */ }

То есть однострочное определение требует точки с запятой между именами типов.

Нулевое значение типа семейства равно nil, как и в случае с интерфейсом nil.

(Под капотом значение, стоящее за семейной абстракцией, реализовано во многом как интерфейс.)

Рассуждение

Нам нужно что-то более точное, чем пустой интерфейс, в котором мы хотим указать, какие типы допустимы в качестве аргументов функции или как возвращаемые значения функции.

Предлагаемое решение обеспечит лучшую безопасность типов, полную проверку во время компиляции и отсутствие дополнительных накладных расходов во время выполнения.

Дело в том, что _Go-код должен быть более самодокументированным_. То, что функция может принимать в качестве аргумента, должно быть встроено в сам код.

Слишком большой объем кода неправильно использует тот факт, что «интерфейс {} ничего не говорит». Немного смущает, что такая широко используемая (и злоупотребляемая) конструкция в Go, без которой мы не смогли бы многое сделать, говорит _nothing_.

Некоторые примеры

Документация для функции sql.Rows.Scan включает большой блок с подробным описанием того, какие типы могут быть переданы в функцию:

Scan converts columns read from the database into the following common Go types and special types provided by the sql package:
 *string
 *[]byte
 *int, *int8, *int16, *int32, *int64
 *uint, *uint8, *uint16, *uint32, *uint64
 *bool
 *float32, *float64
 *interface{}
 *RawBytes
 any type implementing Scanner (see Scanner docs)

А для функции sql.Row.Scan в документации есть предложение «Подробности см. В документации по Rows.Scan». См. Документацию по _другим функциям_ для получения подробной информации? Это не похоже на Go - и в данном случае это предложение неверно, потому что на самом деле Rows.Scan может принимать значение *RawBytes а Row.Scan нет.

Проблема в том, что мы часто вынуждены полагаться на комментарии для гарантий и контрактов поведения, которые компилятор не может обеспечить.

Когда в документации для функции говорится, что функция работает так же, как и какая-то другая функция - «так что посмотрите документацию для этой другой функции» - вы почти можете гарантировать, что функция иногда будет использоваться неправильно. Готов поспорить, что большинство людей, как и я, только узнают, что *RawBytes не допускается в качестве аргумента в Row.Scan только после получения ошибки из Row.Scan ( говоря "sql: RawBytes не разрешен в Row.Scan"). Печально, что система шрифтов допускает такие ошибки.

Вместо этого мы могли бы иметь:

type Value family {
    *string
    *[]byte
    *int; *int8; *int16; *int32; *int64
    *uint; *uint8; *uint16; *uint32; *uint64
    *bool
    *float32; *float64
    *interface{}
    *RawBytes
    Scanner
}

Таким образом, передаваемое значение должно быть одним из типов в данном семействе, и переключателю типа внутри функции Rows.Scan не нужно будет иметь дело с какими-либо неожиданными случаями или случаями по умолчанию; для функции Row.Scan будет другое семейство.

Также обратите внимание на то, что структура cloud.google.com/go/datastore.Property имеет поле «Значение» типа interface{} и требует всей этой документации:

// Value is the property value. The valid types are:
// - int64
// - bool
// - string
// - float64
// - *Key
// - time.Time
// - GeoPoint
// - []byte (up to 1 megabyte in length)
// - *Entity (representing a nested struct)
// Value can also be:
// - []interface{} where each element is one of the above types
// This set is smaller than the set of valid struct field types that the
// datastore can load and save. A Value's type must be explicitly on
// the list above; it is not sufficient for the underlying type to be
// on that list. For example, a Value of "type myInt64 int64" is
// invalid. Smaller-width integers and floats are also invalid. Again,
// this is more restrictive than the set of valid struct field types.
//
// A Value will have an opaque type when loading entities from an index,
// such as via a projection query. Load entities into a struct instead
// of a PropertyLoadSaver when using a projection query.
//
// A Value may also be the nil interface value; this is equivalent to
// Python's None but not directly representable by a Go struct. Loading
// a nil-valued property into a struct will set that field to the zero
// value.

Это должно быть:

type PropertyVal family {
  int64
  bool
  string
  float64
  *Key
  time.Time
  GeoPoint
  []byte
  *Entity
  nil
  []int64; []bool; []string; []float64; []*Key; []time.Time; []GeoPoint; [][]byte; []*Entity
}

(Вы можете представить, как это можно разделить на две семьи.)

Выше упоминался тип json.Token . Это определение типа:

type Token family {
    Delim
    bool
    float64
    Number
    string
    nil
}

Еще один пример, который меня недавно укусил:
При вызове таких функций, как sql.DB.Exec или sql.DB.Query , или любой функции, которая принимает переменный список interface{} где каждый элемент должен иметь тип в определенном наборе и _не сам быть slice_, важно помнить об использовании оператора «распространения» при передаче аргументов из []interface{} в такую ​​функцию: неправильно говорить DB.Exec("some query with placeholders", emptyInterfaceSlice) ; правильный способ: DB.Exec("the query...", emptyInterfaceSlice...) где emptyInterfaceSlice имеет тип []interface{} . Элегантный способ сделать такие ошибки невозможными - заставить эту функцию принимать переменный аргумент Value , где Value определяется как семейство, как описано выше.

Смысл этих примеров в том, что совершаются _реальные ошибки_ из-за неточности interface{} .

var x int | float64 | string | rune
z = int(x) + int(y)
z = float64(x) + float64(y)

Это определенно должно быть ошибкой компилятора, потому что тип x самом деле несовместим с тем, что можно передать в int() .

Мне нравится идея иметь family . По сути, это был бы интерфейс, ограниченный (суженный?) Для перечисленных типов, и компилятор может гарантировать, что вы все время сопоставляете, и изменяет тип переменной в локальном контексте соответствующего case .

Проблема в том, что мы часто вынуждены полагаться на комментарии для гарантий и
контракты поведения, которые компилятор не может обеспечить.

На самом деле это причина, по которой я начал немного не любить такие вещи, как

func foo() (..., error) 

потому что вы не знаете, какую ошибку он возвращает.

и несколько других вещей, которые возвращают интерфейс вместо конкретного типа. Некоторые функции
return net.Addr и иногда бывает сложно разобраться в исходном коде, чтобы выяснить, какой тип net.Addr он действительно возвращает, а затем использовать его соответствующим образом. В возвращении конкретного типа нет особых недостатков (потому что он реализует интерфейс и, таким образом, может использоваться везде, где может использоваться интерфейс), кроме случаев, когда вы
позже планирую расширить ваш метод, чтобы он возвращал другой вид net.Addr . Но если твой
API упоминает, что возвращает OpError тогда почему бы не сделать эту часть спецификации "времени компиляции"?

Например:

 OpError is the error type usually returned by functions in the net package. It describes the operation, network type, and address of an error. 

Как правило? Не сообщает вам, какие именно функции возвращают эту ошибку. И это документация для типа, а не для функции. В документации для Read нигде не упоминается, что он возвращает OpError. Кроме того, если вы это сделаете

err := blabla.(*OpError)

он выйдет из строя, как только вернет ошибку другого типа. Вот почему мне бы очень хотелось видеть это как часть объявления функции. По крайней мере, *OpError | error сообщит вам, что он возвращается
такая ошибка, и компилятор позаботится о том, чтобы вы не выполнили неконтролируемое утверждение типа, приводящее к сбою вашей программы в будущем.

BTW: Рассматривалась ли еще система, подобная полиморфизму типов Haskell? Или система типов, основанная на признаках, то есть:

func calc(a < add(a, a) a >, b a) a {
   return add(a, b)
}

func drawWidgets(widgets []< widgets.draw() error >) error {
  for _, widgets := range widgets {
    err := widgets.draw()
    if err != nil {
      return err
    }
  }
  return nil
}

a < add(a, a) a означает "какой бы ни был тип a, должна существовать функция add (typeof a, typeof a) typeof a)". < widgets.draw() error> означает, что «независимо от типа виджета, он должен предоставлять метод отрисовки, который возвращает ошибку». Это позволит создавать более общие функции:

func Sum(a []< add(a,a) a >) a {
  sum := a[0]
  for i := 1; i < len(a); i++ {
    sum = add(sum,a[i])
  }
  return sum
}

(Обратите внимание, что это не равно традиционным «дженерикам»).

В возврате конкретного типа нет особых недостатков (потому что он реализует интерфейс и, таким образом, может использоваться везде, где может использоваться интерфейс), за исключением случаев, когда вы позже планируете расширить свой метод, чтобы вернуть другой вид net.Addr .

Кроме того, в Go нет вариантов подтипов, поэтому вы не можете использовать func() *FooError в качестве func() error там, где это необходимо. Что особенно важно для удовлетворения интерфейса. И, наконец, это не компилируется:

func Foo() (FooVal, FooError) {
    // ...
}

func Bar(f FooVal) (BarVal, BarError) {
    // ...
}

func main() {
    foo, err := Foo()
    if err != nil {
        log.Fatal(err)
    }
    bar, err := Bar(foo) // Type error: Can not assign BarError to err (type FooError)
    if err != nil {
        log.Fatal(err)
    }
}

т.е. для того, чтобы эта работа работала (я бы хотел, чтобы мы могли как-нибудь), нам понадобится гораздо более сложный вывод типов - в настоящее время Go использует информацию о локальном типе только из одного выражения. По моему опыту, такие алгоритмы вывода типов не только значительно медленнее (замедляют компиляцию и, как правило, даже не ограничивают время выполнения), но также создают гораздо менее понятные сообщения об ошибках.

Кроме того, в Go нет вариантов подтипов, поэтому вы не можете использовать func () * FooError в качестве ошибки func () там, где это необходимо. Что особенно важно для удовлетворения интерфейса. И, наконец, это не компилируется:

Я ожидал, что это нормально работает в Go, но я никогда не сталкивался с этим, потому что в настоящее время практика состоит в том, чтобы просто использовать error . Но да, в этом случае эти ограничения практически вынуждают вас использовать error в качестве возвращаемого типа.

func main() {
    foo, err := Foo()
    if err != nil {
        log.Fatal(err)
    }
    bar, err := Bar(foo) // Type error: Can not assign BarError to err (type FooError)
    if err != nil {
        log.Fatal(err)
    }
}

Я не знаю ни одного языка, который позволяет это (ну, кроме эсолангов), но все, что вам нужно сделать, это сохранить «мир типов» (который в основном представляет собой карту variable -> type ), и если вы - присвоить переменной вы просто обновите ее тип в «мире типов».

Я не думаю, что для этого вам нужен сложный вывод типов, но вам нужно отслеживать типы переменных, но я предполагаю, что вам все равно нужно это делать, потому что

var int i = 0;
i = "hi";

вам наверняка нужно каким-то образом запомнить, какие переменные / объявления имеют какие типы, и для i = "hi" вам нужно выполнить «поиск типа» в i чтобы проверить, можете ли вы присвоить ему строку.

Существуют ли практические проблемы, которые усложняют присвоение func () *ConcreteError func() error кроме проверки типов, не поддерживающей его (например, причины выполнения / причины скомпилированного кода)? Думаю, в настоящее время вам придется обернуть его такой функцией:

type MyFunc func() error

type A struct {
}

func (_ *A) Error() string { return "" }

func NewA() *A {
    return &A{}
}

func main() {
    var err error = &A{}
    fmt.Println(err.Error())
    var mf MyFunc = MyFunc(func() error { return NewA() }) // type checks fine
        //var mf MyFunc = MyFunc(NewA) // doesn't type check
    _ = mf
}

Если вы столкнулись с func (a, b) c но получили func (x, y) z все, что нужно сделать, это проверить, можно ли присвоить z ca , b должны быть присвоены x , y ), что, по крайней мере, на уровне типа не связано со сложным выводом типа (это просто включает проверку того, тип может быть назначен / совместим с другим типом). Конечно, вызывает ли это проблемы со средой выполнения / компиляцией ... Я не знаю, но, по крайней мере, строго глядя на уровень типа, я не понимаю, почему это может включать сложный вывод типов. Средство проверки типов уже знает, можно ли присвоить x a поэтому оно также легко знает, можно ли func () x назначить func () a . Конечно, могут быть практические причины (размышления о представлениях среды выполнения), почему это будет нелегко. (Я подозреваю, что суть здесь в этом, а не в проверке типов).

Теоретически вы могли бы обойти проблемы времени выполнения (если они есть) с помощью автоматической упаковки функций (как в приведенном выше фрагменте) с _ потенциально огромным_ недостатком, заключающимся в том, что он портит сравнение функций с функциями (поскольку завернутый func не будет равен func это заворачивает).

Я не знаю ни одного языка, который позволяет это (ну, кроме esolangs)

Не совсем так, но я бы сказал, что это потому, что языки с мощными системами типов обычно являются функциональными языками, которые на самом деле не используют переменные (и поэтому на самом деле не нуждаются в возможности повторно использовать идентификаторы). FWIW, я бы сказал, что, например, система типов Haskell могла бы справиться с этим отлично - по крайней мере, пока вы не используете какие-либо другие свойства FooError или BarError , она должна можно сделать вывод, что err относится к типу error и разобраться с этим. Конечно, опять же, это гипотетически, потому что эту точную ситуацию нелегко перенести на функциональный язык.

но я предполагаю, что вам все равно нужно это сделать, потому что

Разница в том, что в вашем примере i имеет четкий и понятный тип после первой строки, то есть int и вы затем сталкиваетесь с ошибкой типа, когда назначаете string к нему. Между тем, для чего-то вроде того, что я упоминал, каждое использование идентификатора по существу создает набор ограничений для используемого типа, а затем средство проверки типов пытается вывести наиболее общий тип, удовлетворяющий всем заданным ограничениям (или жаловаться на то, что нет типа, выполняющего это договор). Вот для чего нужны теории формальных типов.

Существуют ли практические проблемы, которые усложняют присвоение func () *ConcreteError func() error кроме проверки типов, не поддерживающей его (например, причины выполнения / причины скомпилированного кода)?

Есть практические проблемы, но я считаю, что для func они, вероятно, разрешимы (путем выдачи кода un / -wrapping, аналогично тому, как работает передача интерфейса). Я немного написал о дисперсии в Go и объяснил некоторые практические проблемы, которые вижу внизу. Я не совсем уверен, что стоит добавить. Т.е. я не уверен, что он решает важные проблемы сам по себе.

с потенциально огромным недостатком, заключающимся в том, что он портит сравнение функций с функциями (поскольку обернутая функция не будет равна функции, которую она обертывает).

функции не сопоставимы.

Во всяком случае, TBH, все это кажется немного не по теме для этого номера :)

К вашему сведению: Я только что это сделал . Это неприятно, но определенно типобезопасно. (То же самое можно сделать для # 19814 FWIW)

Я немного опоздал на вечеринку, но я тоже хотел бы поделиться с вами своими ощущениями после 4 лет игры в го:

  • Возврат нескольких значений был огромной ошибкой.
  • Обнуление интерфейсов было ошибкой.
  • Указатели не являются синонимами «необязательного», вместо них следовало использовать размеченные объединения.
  • Unmarshaller JSON должен был вернуть ошибку, если обязательное поле не включено в документ JSON.

За последние 4 года я обнаружил много связанных с этим проблем:

  • мусорные данные возвращаются в случае ошибки.
  • беспорядок в синтаксисе (возвращение обнуленных значений в случае ошибки).
  • возвращает много ошибок (запутанные API, пожалуйста, не делайте этого!).
  • ненулевые интерфейсы, указывающие на указатели, указывающие на nil (чертовски смущает людей, которые делают заявление «Go - простой язык» звучит как плохая шутка).
  • непроверенные поля JSON приводят к сбою серверов (да!).
  • непроверенные возвращаемые указатели приводят к сбою серверов, но никто не задокументировал, что возвращаемый указатель представляет собой необязательный (возможно, тип) и, следовательно, может быть nil (да!)

Однако изменения, необходимые для исправления всех этих проблем, потребуют действительно обратно несовместимой версии Go 2.0.0 (не Go2), которая, я полагаю, никогда не будет реализована. В любом случае...

Вот так должна была выглядеть обработка ошибок:

// Divide returns either a float64 or an arbitrary error
func Divide(dividend, divisor float64) float64 | error {
  if dividend == 0 {
    return errors.New("dividend is zero")
  }
  if divisor == 0 {
    return errors.New("divisor is zero")
  }
  return dividend / divisor
}

func main() {
  // type-switch statements enforce completeness:
  switch v := Divide(1, 0).(type) {
  case float64:
    log.Print("1/0 = ", v)
  case error:
    log.Print("1/0 = error: ", v)
  }

  // type-assertions, however, do not:
  divisionResult := Divide(3, 1)
  if v, ok := divisionResult.(float64); ok {
    log.Print("3/1 = ", v)
  }
  if v, ok := divisionResult.(error); ok {
    log.Print("3/1 = error: ", v.Error())
  }
  // yet they don't allow asserting types not included in the union:
  if v, ok := divisionResult.(string); ok { // compile-time error!
    log.Print("3/1 = string: ", v)
  }
}

Интерфейсы не заменяют дискриминируемые союзы , это два совершенно разных животных. Компилятор следит за тем, чтобы переключатели типов для размеченных объединений были полными, что означает, что случаи охватывают все возможные типы, если вы этого не хотите, вы можете использовать оператор утверждения типа.

Слишком часто я видел, как люди полностью сбиваются с толку по поводу _ ненулевых интерфейсов к нулевым значениям_ : https://play.golang.org/p/JzigZ2Q6E6F. Обычно люди путаются, когда интерфейс error указывает на указатель настраиваемого типа ошибки, указывающий на nil , это одна из причин, по которой, я думаю, создание интерфейсов с нулевым статусом было ошибкой.

Интерфейс похож на секретаря, вы знаете, что это человек, когда вы разговариваете с ним, но в Go это может быть картонная фигура, и мир внезапно рухнет, если вы попытаетесь с ней поговорить.

Дискриминирующие объединения следовало использовать для опций (возможно-типов), а передача указателей nil интерфейсам должна была вызвать панику:

type CustomErr struct {}
func (err *CustomErr) Error() string { return "custom error" }

func CouldFail(foo int) error | nil {
  var err *customErr
  if foo > 10 {
    // you can't return a nil pointer as an interface value
    return err // this will panic!
  }
  // no error
  return nil
}

func main() {
  // assume no error
  if err, ok := CouldFail().(error); ok {
    log.Fatalf("it failed, Jim! %s", err)
  }
}

Указатели и типы не взаимозаменяемы. Использование указателей для необязательных типов - это плохо, потому что это приводит к путанице в API:

// P returns a pointer to T, but it's not clear whether or not the pointer
// will always reference a T instance. It might be an optional T,
// but the documentation usually doesn't tell you.
func P() *T {}

// O returns either a pointer to T or nothing, this implies (but still doesn't guarantee)
// that the pointer is always expected to not be nil, in any other case nil is returned.
func O() *T | nil {}

Тогда есть еще JSON. Однако с объединениями этого никогда не случится, потому что компилятор заставляет вас проверять их перед использованием . Unmarshaller JSON должен выйти из строя, если обязательное поле (включая поля типа указателя) не включено в документ JSON:

type DataModel struct {
  // Optional needs to be type-checked before use
  // and is therefore allowed to no be included in the JSON document
  Optional string | nil `json:"optional,omitempty"`
  // Required won't ever be nil
  // If the JSON document doesn't include it then unmarshalling will return an error
  Required *T `json:"required"`
}

PS
В настоящее время я также работаю над дизайном функционального языка, и вот как я использую размеченные объединения для обработки ошибок:

read = (s String) -> (Array<Byte> or Error) => match s {
  "A" then Error<NotFound>
  "B" then Error<AccessDenied>
  "C" then Error<MemoryLimitExceeded>
  else Array<Byte>("this is fine")
}

main = () -> ?Error => {
  // assume the result is a byte array
  // otherwise throw the error up the stack wrapped in a "type-assertion-failure" error
  r = read("D") as Array<Byte>
  log::print("data: %s", r)
}

Я хотел бы, чтобы однажды это сбылось. Итак, давайте посмотрим, смогу ли я немного помочь:

Может быть, проблема в том, что мы пытаемся утаить в предложении слишком много. Мы могли бы пойти с упрощенной версией, которая принесет большую часть ценности, так что было бы намного проще добавить ее к языку в краткосрочной перспективе.

С моей точки зрения, эта упрощенная версия будет связана только с nil . Вот основные идеи (почти все они уже упоминались в комментариях):

  1. Разрешить только |
    <any pointer type> | nil
    Где любой тип указателя будет: указатели, функции, каналы, срезы и карты (типы указателей Go)
  2. Запретить присвоение nil типу простого указателя. Если вы хотите присвоить nil, тогда тип должен быть <pointer type> | nil . Например:
var n *int       = nil // Does not compile, wrong type
var n *int | nil = nil // Ok!

var set map[string] bool       = nil // Does not compile
var set map[string] bool | nil = nil // Ok!

var myFunc func(int) err       = nil // Nope!
var myFunc func(int) err | nil = nil // All right.

Это основные идеи. Следующие идеи являются производными от основных:

  1. Вы не можете объявить переменную типа «голый указатель» и оставить ее неинициализированной. Если вы хотите это сделать, вам нужно добавить дискриминированный тип | nil
var maybeAString *string       // Wrong: invalid initial value
var maybeAString *string | nil // Good
  1. Вы можете назначить пустой указатель типу указателя с нулевым значением, но не наоборот:
var value int = 42
var barePointer *int = &value          // Valid
var nilablePointer *int | nil = &value // Valid

nilablePointer = barePointer // Valid
barePointer = nilablePointer // Invalid: Incompatible types
  1. Единственный способ получить значение из типа указателя, допускающего нулевое значение, - использовать переключатель типа, как указывали другие. Например, следуя приведенному выше примеру, если мы действительно хотим присвоить значение nilablePointer barePointer , тогда нам нужно будет сделать:
switch val := nilablePointer.(type) {
  case *int:
    barePointer = val // Yeah! Types are compatible now. It is imposible that "val = nil"
  case nil:
    // Do what you need to do when nilablePointer is nil
}

Вот и все. Я знаю, что размеченные союзы можно использовать для гораздо большего (особенно в случае возврата ошибок), но я бы сказал, что, придерживаясь того, что я написал выше, мы принесем ОГРОМНУЮ ценность языку с меньшими усилиями и без усложняя это более чем необходимо.
Преимущества, которые я вижу в этом простом предложении:

  • а) Отсутствие ошибок нулевого указателя . Ладно, никогда 4 слова не значили так много. Вот почему я чувствую необходимость сказать это с другой точки зрения: программа No Go _EVER_ снова будет иметь ошибку nil pointer dereference ! 💥
  • б) Вы можете передавать указатели на параметры функции, не торгуя «производительность против намерения» .
    Я имею в виду, что в некоторых случаях я хочу передать структуру функции, а не указатель на нее, потому что я не хочу, чтобы эта функция беспокоилась о нулевом значении и заставляла ее проверять параметры. . Однако обычно я передаю указатель, чтобы избежать накладных расходов на копирование.
  • c) Больше никаких нулевых карт! АГА! Мы закончим несогласованностью в отношении «безопасных ниль-срезов» и «небезопасных ниль-карт» (это вызовет панику, если вы попытаетесь им написать). Карта будет либо инициализирована, либо иметь тип map | nil , и в этом случае вам нужно будет использовать переключатель типа 😃

Но здесь есть еще одна нематериальная вещь, которая приносит большую пользу: душевное спокойствие разработчика . Вы можете работать и играть с указателями, функциями, каналами, картами и т. Д. С расслабленным чувством, что вам не нужно беспокоиться о том, что они равны нулю. _Я заплачу за это! _ 😂

Преимущество начала работы с этой более простой версией предложения заключается в том, что это не помешает нам перейти к полному предложению в будущем или даже продвигаться шаг за шагом (для меня это следующий естественный шаг, позволяющий разрешить дискриминационные возвраты ошибок. , но давайте сейчас об этом забудем).

Одна из проблем заключается в том, что даже эта простая версия предложения обратно несовместима, но ее можно легко исправить с помощью gofix : просто замените все объявления типа указателя на <pointer type> | nil .

Что вы думаете? Я надеюсь, что это может пролить свет и ускорить включение нуль-безопасности в язык. Кажется, что этот способ (через «размеченные объединения») является более простым и ортогональным способом его достижения.

@alvaroloes

Вы не можете объявить переменную типа «голый указатель» и оставить ее неинициализированной.

В этом суть дела. Это просто не то, что делает Go - каждый тип имеет нулевое значение, точку. В противном случае вам пришлось бы ответить, что, например, make([]T, 100) делает? Другие вещи, которые вы упомянули (например, срабатывание нулевых карт при записи), являются следствием этого основного правила. (И в стороне, я не думаю, что будет действительно правдой сказать, что nil-срезы безопаснее, чем карты - запись в nil-slice вызовет панику так же, как запись в nil-map).

Другими словами: ваше предложение на самом деле не так просто, поскольку оно довольно значительно отклоняется от довольно фундаментального дизайнерского решения на языке Go.

Я думаю, что более важная вещь, которую делает Go, - это делает нулевые значения полезными, а не просто придает нулевое значение всему. Nil map - это нулевое значение, но это бесполезно. На самом деле это вредно. Так почему бы не запретить нулевое значение, если оно бесполезно. Смена Go в этом отношении была бы полезной, но предложение действительно не так просто.

Приведенное выше предложение больше похоже на необязательные / необязательные вещи, как в Swift и других. Это круто и все, кроме:

  1. Это сломало бы практически все существующие программы, и исправить это было бы нетривиально для gofix. Вы не можете просто заменить все на <pointer type> | nil поскольку, согласно предложению, это потребует переключения типа для распаковки значения.
  2. Для того, чтобы это было действительно пригодным для использования и терпимым, в Go потребуется гораздо больше синтаксического сахара вокруг этих опций. Возьмем, к примеру, Swift. В языке есть много функций, специально предназначенных для работы с опциями - защита, опциональная привязка, опциональная цепочка, объединение нуля и т. Д. Я не думаю, что Go пойдет в этом направлении, но без них работа с опциями была бы утомительной.

Так почему бы не запретить нулевое значение, если оно бесполезно.

См. Выше. Это означает, что некоторые вещи, которые выглядят дешево, связаны с очень нетривиальными затратами.

Смена Go в этом отношении была бы полезной.

У него есть преимущества, но это не то же самое, что польза. Это тоже имеет вред. Какой вес тяжелее, зависит от предпочтений и компромисса. Дизайнеры Go выбрали это.

FTR, это общий шаблон в этом потоке и один из основных контраргументов против любой концепции типов суммы - вам нужно сказать, что такое нулевое значение. Вот почему любая новая идея должна прямо адресовать это. Но несколько разочаровывает то, что большинство людей, которые пишут здесь в наши дни, не прочитали оставшуюся часть обсуждения и склонны игнорировать эту часть.

🤔 Ага! Я знал, что мне не хватало чего-то очевидного. Дох! Слово «простой» имеет сложные значения. Хорошо, не стесняйтесь убирать "простое" слово из моего предыдущего комментария.

Извините, если кого-то из вас это расстроило. Я намеревался немного помочь. Я стараюсь не отставать от темы, но у меня нет на это много свободного времени.

Вернемся к делу: похоже, что основная причина, сдерживающая это, - это нулевое значение.
Подумав некоторое время и отбросив множество вариантов, я думаю, единственное, что могло бы добавить ценность и о чем стоит упомянуть, - это следующее:

Если я правильно помню, нулевое значение любого типа состоит из заполнения его памяти нулями.
Как вы уже знаете, это нормально для типов, не являющихся указателями, но является источником ошибок для типов указателей:

type S struct {
    n int
}
var s S 
s.n  // Fine

var s *S
s.n // runtime error

var f func(int)
f() // runtime error

Итак, что если мы:

  • Определите полезное нулевое значение для каждого типа указателя
  • Инициализируйте его только при первом использовании (ленивая инициализация).

Я думаю, что это было предложено в другом выпуске, не уверен. Я просто пишу его здесь, потому что он обращается к основной сдерживающей стороне этого предложения.

Ниже может быть список нулевых значений для типов указателей. Обратите внимание, что эти нулевые значения будут использоваться только при доступе к значению . Мы могли бы назвать это «динамическим нулевым значением», и это только свойство типов указателей:

| Тип указателя | Нулевое значение | Динамическое нулевое значение | Комментарий |
| --- | --- | --- | --- |
| * T | nil | новый (T) |
| [] T | nil | [] T {} |
| карта [T] U | nil | карта [T] U {} |
| func | nil | нет | Таким образом, динамическое нулевое значение функции ничего не делает и возвращает нулевые значения. Если список возвращаемых значений заканчивается на error , то возвращается ошибка по умолчанию, в которой говорится, что функция не выполняет операцию |
| chan T | nil | сделать (чан Т) |
| interface | nil | - | реализация по умолчанию, в которой все методы инициализируются функцией noop описанной выше |
| размеченный союз | nil | динамическое нулевое значение первого типа | |

Теперь, когда эти типы инициализированы, они будут иметь вид nil , как и сейчас. Разница заключается в момент обращения к nil . В этот момент будет использоваться динамическое нулевое значение. Несколько примеров:

type S struct {
    n int
}
var s *S
if s == nil { // true. Nothing different happens here
...
}
s.n = 1       // At this moment the go runtime would check if it is nil, and if it is, 
              // do "s = new(S)". We could say the code would be replaced by:
/*
if s == nil {
    s = new(S)
}
s.n = 1
*/

// -------------
var pointers []*S = make([]*S, 100) // Everything as usual
for _,p := range pointers {
    p.n = 1 // This is translated to:
    /*
        if p == nil {
            p = new(S)
        }
        p.n = 1
    */
}

// ------------
type I interface {
    Add(string) (int, error)
}

var i I
n, err := i.Add("yup!") // This method returns 0, and the default error "Noop"
if err != nil { // This condition is true and the error is returned
    return err
}

Вероятно, мне не хватает деталей реализации и возможных трудностей, но сначала я хотел сосредоточиться на идее.

Главный недостаток заключается в том, что мы добавляем дополнительную проверку на ноль каждый раз, когда вы обращаетесь к значению типа указателя. Но я бы сказал:

  • Это хороший компромисс для получаемых нами преимуществ. Та же самая ситуация происходит с связанными проверками при доступе к массиву / срезу, и мы соглашаемся уплатить этот штраф за производительность за безопасность, которую он приносит.
  • Нулевых проверок можно избежать так же, как и проверок привязки массивов: если тип указателя был инициализирован в текущей области, компилятор мог бы знать об этом и избежать добавления нулевой проверки.

Благодаря этому у нас есть все преимущества, описанные в предыдущем комментарии, с плюсом в том, что нам не нужно использовать переключатель типа для доступа к значению (это будет только для размеченных объединений), сохраняя код go таким же чистым, как сейчас.

Что вы думаете? Приносим извинения, если это уже обсуждалось. Кроме того, я знаю, что это предложение-комментарий больше связано с nil чем с дискриминируемыми союзами. Я мог бы переместить это в проблему, связанную с nil, но, как я уже сказал, я разместил ее здесь, потому что она пытается исправить основную проблему дискриминируемых объединений: полезные нулевые значения.

Вернемся к делу: похоже, что основная причина, сдерживающая это, - это нулевое значение.

Это одна важная техническая причина, которую необходимо устранить. Для меня основная причина в том, что постепенный ремонт делают категорически невозможным (см. Выше). т.е. лично для меня вопрос не столько в том, как их реализовать, а в том, что я принципиально против этой концепции.
В любом случае, какая из причин является «основной», на самом деле дело вкуса и предпочтений.

Итак, что если мы:

  • Определите полезное нулевое значение для каждого типа указателя
  • Инициализируйте его только при первом использовании (ленивая инициализация).

Это не сработает, если вы передадите тип указателя. например

func F(p *T) {
    *p = 42 // same as if p == nil { p = new(T) } *p = 42
}

func G() {
    var p *T
    F(p)
    fmt.Println(p == nil) // Has to be true, as F can't modify p. But now F is silently misbehaving
}

Это обсуждение совсем не ново. Есть причины, по которым ссылочные типы ведут себя так, как они делают, и дело не в том, что разработчики Go об этом не подумали :)

В этом суть дела. Это просто не то, что делает Go - каждый тип имеет нулевое значение, точку. В противном случае вам пришлось бы ответить, что, например, делает make ([] T, 100)?

Это (и new(T) ) должно быть запрещено, если T не имеет нулевого значения. Вам нужно будет выполнить make([]T, 0, 100) а затем использовать append для заполнения среза. Повторное использование большего размера ( v[:0][:100] ) также должно быть ошибкой. [10]T в основном был бы невозможным типом (если только в язык не добавлена ​​возможность утверждать фрагмент указателя массива). И вам понадобится способ пометить существующие типы, допускающие нулевое значение, как ненулевые, чтобы поддерживать обратную совместимость.

Это может создать проблему, если будут добавлены универсальные типы, поскольку вам нужно будет рассматривать все параметры типа как не имеющие нулевого значения, если они не удовлетворяют некоторой границе. Подмножество типов также может нуждаться в отслеживании инициализации практически повсюду. Это было бы довольно большим изменением само по себе, даже без добавления типов суммы поверх него. Это, безусловно, выполнимо, но это значительно влияет на стоимость анализа затрат и выгод. Сознательный выбор сохранения простой инициализации («всегда есть нулевое значение») вместо этого приведет к тому, что инициализация станет более сложной, чем если бы отслеживание инициализации осуществлялось на языке с первого дня.

Это одна важная техническая причина, которую необходимо устранить. Для меня основная причина в том, что постепенный ремонт делают категорически невозможным (см. Выше). т.е. лично для меня вопрос не столько в том, как их реализовать, а в том, что я принципиально против этой концепции.
В любом случае, какая из причин является «основной», на самом деле дело вкуса и предпочтений.

Хорошо, я понимаю это. Мы просто должны также видеть точку зрения других людей (я не говорю, что вы этого не делаете, я просто подчеркиваю: wink :), где они видят в этом что-то мощное для написания своих программ. Подходит ли оно к Go? Это зависит от того, как идея реализована и интегрирована в язык, и это то, что мы все пытаемся сделать в этом потоке (я думаю)

Это не сработает, если вы передадите тип указателя. например (...)

Я не совсем понимаю. Почему это неудача? Вы просто передаете значение в параметр функции, который оказывается указателем со значением nil . Затем вы изменяете это значение внутри функции. Ожидается, что вы не увидите этих эффектов вне функции. Позвольте мне прокомментировать несколько примеров:

// Augmenting your example with more comments:
func FCurrentGo(p *T) {
    // Here "p" is just a value, which happens to be a pointer type. Doing...
    *p = 42
    // ...without checking first for "nil" is the recipe for hiding a bug that will crash the entire program, 
    // which is exactly what is happening in current Go code bases

    // The correct code would be:
    if p == nil {
        // panic or return error
    }
    *p = 42
}

func FWithDynamicZero(p *T) {
    // Here, again, p is just a value of a pointer type. Doing...
    *p = 42
    // would allocate a new T and assign 42. It is true that this doesn't have any effect on the "outside
    // world", which could be considered "incorrect" because you expected the function to do that.
    // If you really want to be sure "p" is pointing to something valid in the "outside world", then
    // check that:
    if p == nil {
        // panic or return error
    }
    *p = 42
}

func main() {
    var p *T
    FCurrentGo(p) // This will crash the program
        FWithDynamicZero(p) // This won't have any effect on "p". This is expected because "p" is not pointing
                            // to anything. No crash here.
    fmt.Println(p == nil) // It is true, as expected
}

Аналогичная ситуация происходит с методами-приемниками без указателей, и это сбивает с толку новичков в Go (но как только вы это поймете, это станет понятным):

type Point struct {
    x, y int
}

func (p Point) SetXY(x, y int) {
    p.x = x
    p.y = y
}

func main() {
    p := Point{x: 1, y: 2}
    p.SetXY(24, 42)

    pointerToP := &Point{x: 1, y: 2}
    pointerToP.SetXY(24, 42)

    fmt.Println(p, pointerToP) // Will print "{1 2} &{1 2}", which could confuse at first
}

Итак, нам нужно выбирать между:

  • А) Отказ с падением
  • Б) Ошибка с молчаливым неизменением значения, на которое указывает указатель, когда этот указатель передается в функцию.

Исправление для обоих случаев одно и то же: проверьте, нет ли nil, прежде чем что-либо делать. Но для меня А) гораздо вреднее (все приложение вылетает!).
Б) можно рассматривать как «скрытую ошибку», но я бы не стал рассматривать это как ошибку. Это происходит только тогда, когда вы передаете указатели на функции, и, как я показал, есть случаи, когда структуры ведут себя аналогичным образом. И это без учета огромных преимуществ, которые это приносит.

Примечание: я не пытаюсь слепо защищать «свою» идею, я искренне пытаюсь улучшить Go (что уже действительно хорошо). Если есть какие-то другие моменты, по которым идея не стоит того, то я не хочу отказываться от нее и продолжаю думать в другом направлении.

Примечание 2: В конце концов, эта идея предназначена только для значений «nil» и не имеет ничего общего с дискриминируемыми объединениями. Поэтому я создам другую задачу, чтобы не загрязнять эту

Хорошо, я понимаю это. Мы просто должны также видеть точку зрения других людей (я не говорю, что вы этого не делаете, я просто подчеркиваю)

Однако этот меч режет в обе стороны. Вы сказали, что "основной причиной сдерживания этого было». Это утверждение означает , что мы все согласовав хотим ли мы влияние этого предложения. Я , конечно, согласен , что это техническая деталь сдерживая предложения конкретных работ (или , по крайней мере, что любое предложение следует сказать о этом вопросе Но мне не нравится, когда дискуссию незаметно переводят в параллельный мир, где, как мы предполагаем, все действительно этого хотят .

Почему это неудача?

Потому что функция, принимающая указатель, по крайней мере часто дает обещание изменить указатель. Если функция затем молча ничего не делает, я считаю это ошибкой. Или, по крайней мере, легко привести аргумент, что, предотвращая таким образом нуль-панику, вы вводите новый класс ошибки.

Если вы передадите нулевой указатель функции, которая там чего-то ожидает, это ошибка - и я не вижу фактического значения в том, чтобы заставить такое программное обеспечение с ошибками молча продолжать работу. Я могу видеть ценность в оригинальной идее поймать эту ошибку во время компиляции при наличии поддержки не-nilable указателей, но я не вижу смысла в том, что позволяет ошибка не быть пойманным на всех.

то есть, так сказать, вы решаете проблему, отличную от фактического предложения указателей, не допускающих нулевого значения: для этого предложения паника во время выполнения не является проблемой, а всего лишь симптом - проблема заключается в ошибке при случайном прохождении nil на то, что этого не ожидает и что эта ошибка обнаруживается только во время выполнения.

Аналогичная ситуация происходит с методами приемника без указателя.

Я не верю этой аналогии. ИМО вполне разумно рассмотреть

func Foo(p *int) { *p = 42 }

func main() {
    var v int
    Foo(&v)
    if v != 42 { panic("") }
}

чтобы быть правильным кодом. Я не думаю, что разумно рассматривать

func Foo(v int) { v = 42 }

func main( ){
    var v int
    Foo(v)
    if v != 42 { panic("") }
}

чтобы быть правильным. Может быть, если вы абсолютный новичок в Go и пришли с языка, где каждое значение является ссылкой (хотя мне, честно говоря, трудно найти ее - даже Python и Java делают ссылки только на большинство значений). Но ИМО, оптимизируя для этого случая бесполезно, справедливо предположить , что у людей есть некоторое знакомство с указателями против значений. Я думаю, что даже опытный разработчик Go посмотрит, скажем, на метод с получателем указателя, обращающийся к его полям, как на правильный, а код, вызывающий эти методы, правильный. В самом деле, это весь аргумент в пользу статического предотвращения nil -поинтеров: слишком легко непреднамеренно иметь указатель равным нулю и правильно выглядящий код не работает во время выполнения.

Исправление для обоих случаев одно и то же: проверьте, нет ли nil, прежде чем что-либо делать.

ИМО, исправление в текущей семантике состоит в том, чтобы не проверять ноль и считать это ошибкой, если кто-то передает ноль. Мол, в вашем примере вы пишете

// The correct code would be:
if p == nil {
    // panic or return error
}
*p = 42

Но я не считаю этот код правильным. Проверка nil ничего не делает, потому что разыменование nil уже вызывает панику.

Но для меня А) гораздо вреднее (все приложение вылетает!).

Это нормально, но имейте в виду, что многие люди категорически не согласятся с этим. Лично я считаю, что сбой всегда предпочтительнее продолжения с поврежденными данными и неправильными предположениями. В идеальном мире мое программное обеспечение не содержит ошибок и никогда не дает сбоев. В менее идеальном мире мои программы будут иметь ошибки и безопасно отказывать из-за сбоев при их обнаружении. В худшем мире в моих программах будут ошибки, и они будут продолжать сеять хаос при обнаружении.

Однако этот меч режет в обе стороны. Вы сказали: «Основная причина сдерживания этого была». Это заявление подразумевает, что мы все согласны с тем, хотим ли мы эффекта от этого предложения. Я, конечно, могу согласиться с тем, что это техническая деталь, сдерживающая высказанные конкретные предложения (или, по крайней мере, любое предложение должно что-то сказать по этому вопросу). Но мне не нравится, когда дискуссию незаметно переводят в параллельный мир, где, как мы предполагаем, все действительно этого хотят.

Ну, я не хотел этого подразумевать. Если это было понято, то, возможно, я выбрал неправильные слова и прошу прощения. Я просто хотел предложить несколько идей для возможного решения, вот и все.

Я написал _ "... похоже, что основная причина сдерживания этого ..." _ на основании вашего предложения _ "Это суть дела" _, имея в виду нулевое значение. Вот почему я предположил, что нулевое значение было главным сдерживающим фактором. Так что это было мое неверное предположение.

Что касается обработки nil тихо, а не проверки их во время компиляции: я согласен, что лучше проверять их во время компиляции. «Динамическое нулевое значение» было просто итерацией исходного предложения, когда я сосредоточился на решении проблемы всех типов, которые должны иметь нулевое значение. Дополнительной мотивацией было то, что я _ думал_, что это также было основным сдерживающим фактором предложения о дискриминированных профсоюзах.
Если мы сосредоточимся только на проблеме, связанной с nil, я бы предпочел, чтобы типы указателей, отличные от nil, проверялись во время компиляции.

Я бы сказал, что в какой-то момент нам (под «мы» я имею в виду все сообщество Go) нужно будет принять некоторые изменения. Например: если есть хорошее решение, позволяющее полностью избежать ошибок nil и то, что сдерживает это, - это проектное решение «все типы имеют нулевое значение, и оно состоит из нулей», тогда мы могли бы рассмотреть эту идею о внесении некоторых поправок или изменений в это решение, если оно приносит пользу.

Основная причина, по которой я говорю это, - это ваше предложение _ "каждый тип имеет нулевое значение, точка " _. Обычно я не люблю «писать точки». Не поймите меня неправильно! Я полностью согласен с тем, что вы так думаете, это просто мой образ мышления: я не предпочитаю никаких догм, поскольку они могут скрыть пути, ведущие к лучшим решениям.

Наконец, по поводу этого:

Это нормально, но имейте в виду, что многие люди категорически не согласятся с этим. Лично я считаю, что сбой всегда предпочтительнее продолжения с поврежденными данными и неправильными предположениями. В идеальном мире мое программное обеспечение не содержит ошибок и никогда не дает сбоев. В менее идеальном мире мои программы будут иметь ошибки и безопасно отказывать из-за сбоев при их обнаружении. В худшем мире в моих программах будут ошибки, и они будут продолжать сеять хаос при обнаружении.

Я полностью согласен с этим. Неудача вслух всегда лучше, чем промахи молча. Однако в Go есть одна загвоздка:

  • Если у вас есть приложение с тысячами горутин, необработанная паника в одной из них приведет к сбою всей программы. Это отличается от других языков, где происходит сбой только в панике.

Если отложить это в сторону (хотя это довольно опасно), идея состоит в том, чтобы избежать целой категории отказов (отказов, связанных с nil ).

Так что давайте продолжим и попытаемся найти решение.

Спасибо за ваше время и энергию!

Я хотел бы увидеть синтаксис размеченных объединений ржавчины, а не типы сумм haskell, он позволяет именовать варианты и позволяет лучше предлагать синтаксис сопоставления с образцом.
Реализация может быть выполнена как структура с полем тега (тип uint, зависит от количества вариантов) и полем объединения (содержащим данные).
Эта функция необходима для закрытого набора вариантов (представление состояния было бы намного проще и чище, с проверкой времени компиляции). В соответствии с вопросами об интерфейсах и их представлении, я думаю, что их реализация в типе суммы должна быть не более чем еще одним случаем типа суммы, потому что интерфейс относится к любому типу, который соответствует некоторым требованиям, но вариант использования типа суммы отличается.

Синтаксис:

type Type enum {
         Tuple (int,int),
         One int,
         None,
};

В приведенном выше примере размер будет sizeof ((int, int)).
Сопоставление с образцом может быть выполнено с помощью нового созданного оператора сопоставления или в рамках существующего оператора переключения, например:

var a Type
switch (a) {
         case Tuple{(b,c)}:
                    //do something
         case One{b}:
                    //do something else
         case None:
                    //...
}

Синтаксис создания:
var a Type = Type{One=12}
Обратите внимание, что при построении экземпляра enum может быть указан только один вариант.

Нулевое значение (проблема):
Мы можем сортировать имена в алфавитном порядке, нулевое значение перечисления будет нулевым значением типа первого члена в отсортированном списке элементов.

PS Решение проблемы нулевой стоимости во многом определяется соглашением.

Я думаю, что сохранение нулевого значения суммы в качестве нулевого значения первого определяемого пользователем поля суммы было бы менее запутанным, возможно

Я думаю, что сохранение нулевого значения суммы в качестве нулевого значения первого определяемого пользователем поля суммы было бы менее запутанным, возможно

Но сделать нулевое значение зависимым от порядка объявления полей, я думаю, что это хуже.

Кто-то написал дизайн-документ?

У меня есть такой:
19412-discriminated_unions_and_pattern_matching.md.zip

Я изменил это:

Я думаю, что сохранение нулевого значения суммы в качестве нулевого значения первого определяемого пользователем поля суммы было бы менее запутанным, возможно

Теперь в моем предложении соглашение о нулевом значении (проблеме) перешло на позицию случайности.

UPD: Изменен дизайн-документ, мелкие исправления.

У меня есть два недавних варианта использования, в которых мне нужны встроенные типы сумм:

  1. Представление дерева AST, как и ожидалось. Первоначально нашли библиотеку, которая на первый взгляд была решением, но их подход заключался в большой структуре с множеством полей, допускающих нулевое значение. ИМО, худший из обоих миров. Конечно, никакой безопасности типов. Вместо этого написал наш собственный.
  2. Была очередь предопределенных фоновых задач: у нас есть служба поиска, которая сейчас находится в стадии разработки, и наши поисковые операции могут быть слишком длинными и т. Д. Поэтому мы решили выполнять их в фоновом режиме, отправляя задачи операций поискового индекса в канал. Затем диспетчер решит, что с ними делать дальше. Можно использовать шаблон посетителя, но для простого запроса gRPC это явно перебор. И это, по крайней мере, не очень понятно, поскольку вводит связь между диспетчером и посетителем.

В обоих случаях реализовано примерно следующее (на примере 2-й задачи):

type Task interface {
    task()
}

type SearchAdd struct {
    Ctx   context.Context
    ID    string
    Attrs Attributes
}

func (SearchAdd) task() {}

type SearchUpdate struct {
    Ctx         context.Context
    ID          string
    UpdateAttrs UpdateAttributes
}

func (SearchUpdate) task() {}

type SearchDelete struct {
    Ctx context.Context
    ID  string
}

func (SearchDelete) task() {}

А потом

task := <- taskChannel

switch v := task.(type) {
case tasks.SearchAdd:
    resp, err := search.Add(task.Ctx, &search2.RequestAdd{…}
    if err != nil {
        log.Error().Err(err).Msg("blah-blah-blah")
    } else {
        if resp.GetCode() != search2.StatusCodeSuccess  {
            …
        } 
    }
case tasks.SearchUpdate:
    …
case tasks.SearchDelete:
    …
}

Это почти хорошо. Плохо, что Go не обеспечивает полную безопасность типов, т.е. после добавления новой задачи операции поискового индекса не будет ошибки компиляции.

ИМХО, использование типов сумм является наиболее ясным решением для такого рода задач, которые обычно решаются с помощью посетителя и набора диспетчеров, где функции посетителя немногочисленны и малы, а сам посетитель является фиксированным типом.

Я искренне верю, что есть что-то вроде

type Task oneof {
    // SearchAdd holds a data for a new record in the search index
    SearchAdd {
        Ctx   context.Context
        ID    string
        Attrs Attributes   
    }

    // SearchUpdate update a record
    SearchUpdate struct {
        Ctx         context.Context
        ID          string
        UpdateAttrs UpdateAttributes
    }

    // SearchDelete delete a record
    SearchDelete struct {
        Ctx context.Context
        ID  string
    }
}

+

switch task {
case tasks.SearchAdd:
    // task is tasks.SearchAdd in this scope
case tasks.SearchUpdate:
case tasks.SearchDelete:
}

был бы гораздо более гоишским по духу, чем любой другой подход, допускаемый Go в его нынешнем состоянии. Нет необходимости в сопоставлении с образцом на языке Haskellish, более чем достаточно перехода к определенному типу.

Ой, упустил суть предложения по синтаксису. Почини это.

Две версии, одна для общего типа суммы и тип суммы для перечислений:

Типовые типы сумм

type Sum oneof {
    T₁ TypeDecl₁
    T₂ TypeDecl₂
    …
    Tₙ TypeDeclₙ
}

где T₁Tₙ - это определения типов на том же уровне, что и Sum ( oneof выводит их за пределы своей области видимости), а Sum объявляет некоторый интерфейс, которому удовлетворяет только T₁Tₙ .

Обработка аналогична тому, что у нас есть переключатель (type) за исключением того, что он выполняется неявно над объектами oneof и должна быть проверка компилятора, все ли варианты были перечислены.

Безопасные перечисления реальных типов

type Enum oneof {
    Value = iota
}

очень похоже на iota констант, за исключением того, что только явно перечисленные значения являются перечислениями, а все остальное - нет.

switch task {
case tasks.SearchAdd:
    // task is tasks.SearchAdd in this scope
case tasks.SearchUpdate:
case tasks.SearchDelete:
}

был бы гораздо более гоишским по духу, чем любой другой подход, допускаемый Go в его нынешнем состоянии. Нет необходимости в сопоставлении с образцом на языке Haskellish, более чем достаточно перехода к определенному типу.

Я не думаю, что изменение значения переменной task - хорошая идея, хотя и допустимая.

```go
switch task {
case tasks.SearchAdd:
    // task is tasks.SearchAdd in this scope
case tasks.SearchUpdate:
case tasks.SearchDelete:
}

был бы гораздо более гоишским по духу, чем любой другой подход, допускаемый Go в его нынешнем состоянии. Нет необходимости в сопоставлении с образцом на языке Haskellish, более чем достаточно перехода к определенному типу.

Я не думаю, что манипулирование значением переменной задачи - хорошая идея, хотя и допустимая.
`` ''

Тогда удачи с вашими посетителями.

@sirkon Что вы имеете в виду под посетителями? Мне, кстати, понравился этот синтаксис, однако следует записать переключатель следующим образом:

switch task {
case Task.SearchAdd:
    // task is Task.SearchAdd in this scope
case Task.SearchUpdate:
case Task.SearchDelete:
}

Каким будет отсутствие значения для Task ? Например:

var task Task

Будет ли это nil ? Если да, то должны ли switch иметь лишние case nil ?
Или он будет инициализирован первым типом? Это было бы неудобно, потому что тогда порядок объявления типов имеет значение, которого не было раньше, однако это, вероятно, будет нормально для числовых перечислений.

Я предполагаю, что это эквивалентно switch task.(type) но для переключения требуются все случаи, верно? как в .. если вы пропустите один случай, ошибка компиляции. И никакие default допускаются. Это правильно?

Что вы имеете в виду под посетителями?

Я имел в виду, что это единственный типобезопасный вариант в Go для такой функциональности. Гораздо хуже для определенного набора случаев (ограниченное количество предопределенных альтернатив).

И какова была бы бесполезность для Задачи? Например:

var task Task

Боюсь, что в Go это должен быть нулевой тип, так как это

Или он будет инициализирован первым типом?

было бы слишком странно, особенно для определенной цели.

Я предполагаю, что это эквивалентно переключению задачи. (Тип), но переключение потребует наличия всех случаев, верно? как в .. если вы пропустите один случай, ошибка компиляции.

Да, верно.

И по умолчанию не допускается. Это правильно?

Нет, значения по умолчанию разрешены. Хотя обескуражен.

PS Кажется, у меня есть представление о типах сумм у Go @ianlancetaylor и других людей Go. Похоже, что nilness делает их весьма уязвимыми для NPD, поскольку Go не имеет никакого контроля над нулевыми значениями.

Если ноль, значит, все в порядке. Я бы предпочел, чтобы case nil было требованием для оператора switch. Выполнить if task != nil до этого тоже нормально, просто мне это не очень нравится: |

Было бы это тоже разрешено?

type Foo oneof {
  A = 3
  B = "3"
  C = 3.0
  D = struct { E bool }{ true }
}

Было бы это тоже разрешено?

type Foo oneof {
  A = 3
  B = "3"
  C = 3.0
  D = struct { E bool }{ true }
}

Ну, тогда никаких констант, только

type Foo oneof {
    A <type reference>
}

или

type Foo oneof {
    A = iota
    B
    C
}

или

type Foo oneof {
    A = 1
    B = 2
    C = 3
}

Никакого сочетания йот и ценностей. Или сочетание с контролем по значениям, они не должны повторяться.

FWIW, одна вещь, которая мне показалась интересной в новейшем дизайне дженериков, - это то, что он показал другое место для решения по крайней мере некоторых вариантов использования типов суммы, избегая ловушки с нулевыми значениями. Он определяет дизъюнктивные контракты, которые в некотором смысле суммируются, но поскольку они описывают ограничения, а не типы, не обязательно иметь нулевое значение (поскольку вы не можете объявлять переменные этого типа). То есть, по крайней мере, можно написать функцию, которая принимает ограниченный набор возможных типов, с проверкой типов этого набора во время компиляции.

Теперь, конечно, как есть дизайн не работает для предполагаемых здесь вариантов использования: дизъюнкции перечисляют только базовые типы или методы и, таким образом, все еще широко открыты. И, конечно же, даже как общая идея, она довольно ограничена, поскольку вы не можете создать экземпляр общей (или суммирующей) функции или значения. Но ИМО показывает, что пространство дизайна для решения некоторых вариантов использования сумм намного больше, чем идея самих типов сумм. И это размышление о суммах, таким образом, больше зацикливается на конкретном решении, чем на конкретных проблемах.

В любом случае. Просто подумал, это интересно.

@Merovius отлично

func addOne(x int|float64) int|float64 {
    switch x := x.(type) {
    case int:
        return x + 1
    case float64:
         return x + 1
    }
}

станет:

contract intOrFloat64(T) {
    T int, float64
}

func addOne(type T intOrFloat64) (x T) T {
    return x + 1
}

Что касается самих типов сумм, то, если обобщенные типы в конечном итоге появятся, я был бы еще более сомнительным, чем сейчас, относительно того, перевесит ли выгода от их введения затраты на простой язык, такой как Go.

Однако, если бы что-то нужно было сделать, то самым простым и наименее разрушительным решением IMO была бы идея @ianlancetaylor об «ограниченных интерфейсах», которая была бы реализована точно так же, как «неограниченные» интерфейсы сегодня, но могла бы быть только удовлетворена. по указанным типам. Фактически, если вы взяли лист из книги общего дизайна и сделали ограничение типа первой строкой интерфейсного блока:

type intOrFloat64 interface{ type int, float64 }    

тогда это будет полностью обратно совместимо, так как вам вообще не понадобится новое ключевое слово (например, restrict ). Вы по-прежнему можете добавлять методы в интерфейс, и это было бы ошибкой времени компиляции, если бы методы не поддерживались всеми указанными типами.

Я не вижу никаких проблем с присвоением значений переменной ограниченного типа интерфейса. Если тип значения в RHS (или тип по умолчанию нетипизированного литерала) не был точным соответствием одному из указанных типов, то он просто не компилировался. Итак, у нас было бы:

var v1 intOrFloat64 = 1        // compiles, dynamic type int
var v2 intOrFloat64 = 1.0      // compiles, dynamic type float64
var v3 intOrFloat64 = 1 + 2i   // doesn't compile, complex128 is not a specified type

Было бы ошибкой времени компиляции, если бы переключение типа не соответствовало указанному типу, и могла бы быть реализована проверка полноты. Однако утверждение типа все равно потребуется для преобразования значения ограниченного интерфейса в значение его динамического типа, как сегодня.

Нулевые значения не являются проблемой при таком подходе (или, во всяком случае, не более серьезной проблемой, чем сегодня с интерфейсами в целом). Нулевое значение ограниченного интерфейса будет nil (подразумевая, что он в настоящее время ничего не содержит), и указанные типы, конечно, будут иметь свои собственные нулевые значения внутри, которые будут nil для типов с нулевым значением.

Все это кажется мне вполне работоспособным, хотя, как я уже сказал ранее, безопасность времени компиляции действительно стоит дополнительных сложностей - у меня есть сомнения, поскольку я никогда не чувствовал необходимости в типах сумм в моем собственном программировании.

IIUC, дженерики не будут динамическими типами, так что все это не выдерживает критики. Однако, если интерфейсам разрешено работать как контракты (в чем я сомневаюсь), это не решит исчерпывающих проверок и перечислений, о чем (я думаю, может быть, нет?) Sumtypes.

@alanfo , @Merovius Спасибо за реплику; Интересно, что это обсуждение разворачивается в этом направлении:

Мне нравится на долю секунды перевернуть точку зрения: я пытаюсь понять, почему контракты нельзя полностью заменить параметризованными интерфейсами, которые допускают указанное выше ограничение типа. На данный момент я не вижу какой-либо серьезной технической причины, за исключением того, что такие типы интерфейса «сумма», когда используются как типы «суммы», хотели бы ограничить возможные динамические значения точно типами, перечисленными в интерфейсе, в то время как - если тот же интерфейс использовался в позиции контракта - перечисляемые типы в интерфейсе должны были бы служить базовыми типами, чтобы быть разумно полезным общим ограничением.

@Goodwine
Я не предлагал, чтобы общий дизайн учитывал все, что можно было бы делать с типами суммы - как

Однако дизайн дженериков позволил бы написать функцию, которая работает с ограниченным набором типов, которые компилятор будет применять, а это то, что мы вообще не можем сделать в настоящее время.

Что касается ограниченных интерфейсов, компилятор будет знать точные типы, которые могут быть использованы, и поэтому для него станет возможным выполнить исчерпывающую проверку в операторе переключения типа.

@Griesemer

Я озадачен тем, что вы говорите, поскольку я думал, что черновик проектного документа универсальных шаблонов довольно четко объясняет (в разделе «Почему бы не использовать интерфейсы вместо контрактов»), почему последние считались лучшим средством выражения общих ограничений, чем первые.

В частности, контракт может выражать взаимосвязь между параметрами типа, и поэтому необходим только один контракт. Любой из его параметров типа может использоваться в качестве типа получателя метода, указанного в контракте.

Чего нельзя сказать об интерфейсе, параметризованном или нет. Если бы у них вообще были какие-либо ограничения, каждому параметру типа потребовался бы отдельный интерфейс.

Это затрудняет выражение взаимосвязи между параметрами типа с помощью интерфейсов, хотя это не невозможно, как показано в примере с графом.

Однако, если вы думаете, что мы можем «убить двух зайцев одним выстрелом», добавив ограничения типа к интерфейсам, а затем используя их как для общих, так и для общих целей, тогда (помимо упомянутой вами проблемы) я думаю, что вы наверное прав, что это было бы технически осуществимо.

Я предполагаю, что не имеет особого значения, могут ли ограничения типа интерфейса включать `` не встроенные '' типы в том, что касается дженериков, хотя необходимо найти какой-то способ, чтобы ограничить их точными типами (а также не производными типами) поэтому они подходят для типов сумм. Возможно, мы могли бы использовать const type для последнего (или даже просто const ), если мы хотим придерживаться текущих ключевых слов.

@griesemer Есть несколько причин, по которым параметризованные типы интерфейсов не являются прямой заменой контрактов.

  1. Параметры типа такие же, как и для других параметризованных типов.
    В таком виде, как

    type C2(type T C1) interface { ... }
    

    параметр типа T существует вне самого интерфейса. Любой аргумент типа, переданный как T должен быть уже известен для выполнения контракта C1 , а тело интерфейса не может дополнительно ограничивать T . Это отличается от параметров контракта, которые ограничиваются телом контракта в результате передачи в него. Это означало бы, что каждый параметр типа функции должен быть независимо ограничен перед передачей в качестве параметра ограничения для любого другого параметра типа.

  2. В теле интерфейса невозможно указать тип получателя.
    Интерфейсы должны позволять вам писать что-то вроде:

    type C3(type U C1) interface(T) {
        Add(T) T
    }
    

    где T обозначает тип получателя.

  3. Некоторые типы интерфейсов не могут служить общими ограничениями.
    Любые операции, основанные на нескольких значениях типа получателя, несовместимы с динамической отправкой. Следовательно, эти операции нельзя использовать со значениями интерфейса. Это означало бы, что интерфейс не удовлетворял бы себя (например, в качестве аргумента типа для параметра типа, ограниченного тем же интерфейсом). Это было бы удивительно. Одно из решений состоит в том, чтобы вообще не разрешать создание значений интерфейсов для таких интерфейсов, но это в любом случае запретило бы предполагаемый здесь вариант использования.

Что касается различения между ограничениями базового типа и ограничениями идентичности типа, есть один метод, который может работать. Представьте, что мы можем определить собственные ограничения, например

contract (T) indenticalTo(U) {
    *T *U
}

(Здесь я использую изобретенную нотацию, чтобы указать единственный тип в качестве «получателя». Я объявляю контракт с явным типом получателя как «ограничение», так же как функция с получателем произносится как «метод». Параметры после имени контракта являются параметрами обычного типа и не могут появляться в левой части предложения ограничения в теле ограничения.)

Поскольку базовым типом литерального указателя является он сам, это ограничение подразумевает, что T идентично U . Поскольку это объявлено как ограничение, вы можете написать (identicalTo(int)), (identicalTo(uint)), ... как дизъюнкцию ограничений.

Хотя контракты могут быть полезны для выражения каких-то типов сумм, я не думаю, что с ними можно выразить общие типы сумм. Из того, что я видел в черновике, нужно перечислить конкретные типы, поэтому вы не можете написать что-то вроде этого:

contract Foo(T, U) {
    T U, int64
}

Что нужно для выражения общего типа суммы неизвестного типа и одного / нескольких известных типов. Даже если бы дизайн допускал такие конструкции, они бы выглядели странно при использовании, поскольку оба параметра фактически были бы одним и тем же.

Я еще немного подумал о том, как проект дженериков может измениться, если интерфейсы будут расширены для включения ограничений типа, а затем использованы для замены контрактов в дизайне.

Пожалуй, проще всего проанализировать ситуацию, если мы рассмотрим разное количество параметров типа:

Без параметров

Без изменений :)

Один параметр

Здесь нет реальных проблем. Параметризованный интерфейс (в отличие от неуниверсального) потребуется только в том случае, если параметр типа ссылается на себя и / или какой-либо другой независимый фиксированный тип (ы) необходим для создания экземпляра интерфейса.

Два или более параметра

Как упоминалось ранее, каждый параметр типа должен быть ограничен индивидуально, если ему вообще необходимо ограничение.

Параметризованный интерфейс потребуется только в том случае, если:

  1. Параметр типа относится к самому себе.

  2. Интерфейс ссылался на другой параметр типа или параметры, которые _ уже были объявлены_ в разделе параметров типа (по-видимому, мы не хотели бы здесь возвращаться).

  3. Некоторые другие независимые фиксированные типы были необходимы для создания экземпляра интерфейса.

Из них (2) на самом деле является единственным проблемным случаем, поскольку он исключил бы параметры типа, ссылающиеся друг на друга, как в примере с графом. Независимо от того, объявил ли один «узел» или «край» первым, его ограничивающий интерфейс все равно будет нуждаться в передаче другого в качестве параметра типа.

Однако, как указано в проектном документе, вы можете обойти это, объявив непараметрические (поскольку они не ссылаются на себя) NodeInterface и EdgeInterface на верхнем уровне, поскольку тогда не будет проблем с их обращением друг к другу независимо от Порядок декларирования. Затем вы можете использовать эти интерфейсы для ограничения параметров типа структуры Graph и связанных с ней методов New.

Так что не похоже, что здесь есть какие-то непреодолимые проблемы, даже если идея контракта лучше.

Предположительно, comparable теперь может стать просто встроенным интерфейсом, а не контрактом.

Конечно, интерфейсы можно было бы встроить друг в друга, как это уже возможно.

Я не уверен, как можно справиться с проблемой метода указателя (в тех случаях, когда это необходимо указать в контракте), поскольку вы не можете указать получателя для метода интерфейса. Возможно, потребуется специальный синтаксис (например, звездочка перед именем метода), чтобы указать метод указателя.

Обращаясь теперь к наблюдениям @stevenblenkinsop , я задаюсь вопросом, упростило бы жизнь, если бы параметризованные интерфейсы вообще не позволяли каким-либо образом ограничивать свои собственные параметры типа? Я не уверен, что это действительно полезная функция, если только кто-то не придумает разумного варианта использования.

Лично я не считаю удивительным то, что некоторые типы интерфейсов не могут удовлетворить себя как общие ограничения. Тип интерфейса в любом случае не является допустимым типом получателя и поэтому не может иметь методов.

Хотя идея Стивена о встроенной функции identifyTo () могла бы работать, мне кажется, что она потенциально слишком длинна для определения типов сумм. Я бы предпочел синтаксис, который позволяет указать целую строку типов как точную.

@urandom , конечно, прав в том, что, поскольку в настоящее время существует черновик

interface Foo(T) {
    const type T, int64  // 'const' indicates types are exact i.e. no derived types
}

почему мы не можем просто добавить к языку Дискриминационные союзы вместо того, чтобы изобретать очередную прогулку по их отсутствию?

@griesemer Вы могли знать, а могли и не знать, но я с самого начала выступал за использование интерфейсов для определения ограничений :) Я больше не думаю, что точные идеи, которые я привожу в этом посте, являются правильными (особенно, Предлагаю обратиться к операторам). И мне действительно нравится последняя итерация дизайна контрактов намного больше, чем предыдущая. Но в целом я полностью согласен с тем, что (возможно, расширенные) интерфейсы как ограничения жизнеспособны и заслуживают рассмотрения.

@urandom

Я не думаю, что с их помощью можно выразить общие типы сумм

Я хочу повторить, что моя точка зрения заключалась не в том, что «с их помощью можно строить типы сумм», а в том, что «вы можете решить некоторые проблемы, которые решают с их помощью типы сумм». Если ваша проблема формулируется как «Мне нужны типы сумм», то неудивительно, что типы сумм - единственное решение. Я просто хотел сказать, что можно было бы обойтись без них, если бы мы сосредоточились на проблемах, которые вы хотите решить с их помощью.

@alanfo

Это затрудняет выражение взаимосвязи между параметрами типа с помощью интерфейсов, хотя это не невозможно, как показано в примере с графом.

Я считаю, что «неудобно» - это субъективно. Лично я считаю использование параметризованных интерфейсов более естественным, а пример графика - очень хорошей иллюстрацией. Для меня Graph - это сущность, а не связь между разновидностью Edge и разновидностью узла.

Но, честно говоря, я не думаю, что любой из них действительно более или менее неудобен - вы пишете практически один и тот же код, чтобы выразить практически одни и те же вещи. И FWIW, для этого есть предшествующий уровень техники . Классы типов Haskell во многом похожи на интерфейсы, и, как указывается в этой вики-статье, использование классов типов с несколькими параметрами для выражения отношений между типами - вполне нормальное занятие.

@stevenblenkinsop

В теле интерфейса невозможно указать тип получателя.

То, как вы бы это сделали, - это аргументы типа на сайте использования. т.е.

type Adder(type T) interface {
    Add(t T) T
}

func Sum(type T Adder(T)) (vs []T) T {
    var t T
    for _, v := range vs {
        t = t.Add(v)
    }
    return t
}

Это требует некоторой осторожности в отношении того, как работает унификация, чтобы вы могли разрешить самореферентные параметры типа, но я думаю, что это можно заставить работать.

Ваши 1. и 3. Я не очень понимаю, должен признать. Мне было бы полезно привести несколько конкретных примеров.


В любом случае, было бы немного лукавством отказаться от этого в конце продолжения этого обсуждения, но, вероятно, это не тот вопрос, чтобы обсуждать мелкие детали дизайна дженериков. Я поднял его только для того, чтобы немного расширить пространство дизайна для этого вопроса :) Потому что казалось, что прошло много времени с тех пор, как новые идеи были включены в обсуждение типов сумм.

```go
switch task {
case tasks.SearchAdd:
    // task is tasks.SearchAdd in this scope
case tasks.SearchUpdate:
case tasks.SearchDelete:
}

был бы гораздо более гоишским по духу, чем любой другой подход, допускаемый Go в его нынешнем состоянии. Нет необходимости в сопоставлении с образцом на языке Haskellish, более чем достаточно перехода к определенному типу.
Я не думаю, что манипулирование значением переменной задачи - хорошая идея, хотя и допустимая.

Тогда удачи с вашими посетителями.

Почему вы думаете, что сопоставление с образцом невозможно в Go? Если вам не хватает примеров сопоставления шаблонов, см., Например, Rust.

@Merovius re: "Для меня график - это сущность"

Является ли это сущностью времени компиляции или у нее есть представление во время выполнения? Одно из основных различий между контрактами и интерфейсами заключается в том, что интерфейс - это объект времени выполнения. Он участвует в сборке мусора, имеет указатели на другие объекты среды выполнения и так далее. Преобразование контракта в интерфейс означало бы введение нового временного объекта среды выполнения, который имеет указатели на узлы / вершины, которые он содержит (сколько?), Что кажется неудобным, когда у вас есть набор функций графа, каждая из которых может более естественно принимать параметры, указывающие на различные части графиков по-своему, в зависимости от потребностей функции.

Ваша интуиция может быть введена в заблуждение, используя «График» для контракта, поскольку «График» кажется объектным, а контракт на самом деле не определяет какой-либо конкретный подграф; это больше похоже на определение набора терминов, которые будут использоваться позже, как в математике или праве. В некоторых случаях вам может понадобиться и контракт графа, и интерфейс графа, что приведет к досадному конфликту имен. Однако я не могу придумать лучшего имени в голове.

Напротив, размеченное объединение - это объект времени выполнения. Не ограничивая реализацию, вам нужно подумать о том, на что может быть похож их массив. Для массива из N элементов требуется N дискриминаторов и N значений, и это можно сделать разными способами. (У Джулии есть интересные представления, иногда она помещает дискриминаторы и значения в отдельные массивы.)

Чтобы предложить уменьшение количества ошибок, которые в настоящее время возникают повсеместно со схемами interface{} , но чтобы убрать непрерывный ввод оператора | , я бы предложил следующее:

type foobar union {
    int
    float64
}

Один только случай использования замены многих interface{} таким типом безопасности был бы огромным преимуществом для библиотеки. Просто взглянув на половину того, что есть в криптобиблиотеке, можно было бы использовать это.

Такие проблемы, как: ах вы указали ecdsa.PrivateKey вместо *ecdsa.PrivateKey - вот общая ошибка, которая поддерживается только ecdsa.PrivateKey. Тот простой факт, что это должны быть четкие типы объединения, несколько повысит безопасность типов.

Хотя это предложение занимает больше _пространства_ по сравнению с int|float64 оно заставляет пользователя задуматься об этом. Сохранение кодовой базы намного чище.

Чтобы предложить уменьшение количества ошибок, которые в настоящее время возникают повсеместно со схемами interface{} , но чтобы убрать непрерывный ввод оператора | , я бы предложил следующее:

type foobar union {
    int
    float64
}

Один только случай использования замены многих interface{} таким типом безопасности был бы огромным преимуществом для библиотеки. Просто взглянув на половину того, что есть в криптобиблиотеке, можно было бы использовать это.

Такие проблемы, как: ах вы указали ecdsa.PrivateKey вместо *ecdsa.PrivateKey - вот общая ошибка, которая поддерживается только ecdsa.PrivateKey. Тот простой факт, что это должны быть четкие типы объединения, несколько повысит безопасность типов.

Хотя это предложение занимает больше _пространства_ по сравнению с int|float64 оно заставляет пользователя задуматься об этом. Сохранение кодовой базы намного чище.

Смотрите этот (комментарий) , это мое предложение.

Фактически мы можем представить обе наши идеи на языке. Это приведет к существованию двух собственных способов выполнения ADT, но с разными синтаксисами.

Мое предложение по функциям, особенно по сопоставлению с образцом, для вашей совместимости и возможности извлекать выгоду из этой функции для старых баз кода.

Но похоже на перебор, не так ли?

Кроме того, тип суммы может иметь значение по умолчанию nil . Конечно, для каждого переключателя потребуется nil case.
Сопоставление с шаблоном можно выполнить следующим образом:
- декларация

type U enum{
    A(int64),
    B(string),
}

- соответствие

...
var a U
...
switch a {
    case A{b}:
         //process b here
    case B{b}:
         //...
    case nil:
         //...
}
...

Если кто-то не любит сопоставление с образцом - см. Предложение sirkon выше.

Кроме того, тип суммы может иметь значение по умолчанию nil . Конечно, для каждого переключателя потребуется nil case.

Не было бы проще запретить неинициализированное значение во время компиляции? В случаях, когда нам нужно инициализированное значение, мы можем добавить его к типу суммы: т.е.

type U enum {
  None
  A(string)
  B(uint64)
}
...
var a U.None
...
switch a {
  case U.None: ...
  case U.A(str): ...
  case U.B(i): ...
}

Кроме того, тип суммы может иметь значение по умолчанию nil . Конечно, для каждого переключателя потребуется nil case.

Не было бы проще запретить неинициализированное значение во время компиляции? В случаях, когда нам нужно инициализированное значение, мы можем добавить его к типу суммы: т.е.

Нарушает существующий код.

Кроме того, тип суммы может иметь значение по умолчанию nil . Конечно, для каждого переключателя потребуется nil case.

Не было бы проще запретить неинициализированное значение во время компиляции? В случаях, когда нам нужно инициализированное значение, мы можем добавить его к типу суммы: т.е.

Нарушает существующий код.

Нет никакого существующего кода с типами сумм. Хотя я думаю, что значение по умолчанию должно быть тем, что определено в самом типе. Либо первая запись, либо первая по алфавиту, либо что-то в этом роде.

Нет никакого существующего кода с типами сумм. Хотя я думаю, что значение по умолчанию должно быть тем, что определено в самом типе. Либо первая запись, либо первая по алфавиту, либо что-то в этом роде.

Я согласился с вами в первой мысли, но после некоторого размышления новое зарезервированное имя для объединения могло быть использовано ранее в некоторой кодовой базе (объединение, перечисление и т. Д.)

Я думаю, что обязательство проверять на ноль было бы довольно болезненно использовать.

Похоже, критическое изменение для обратной совместимости, которое может быть решено только с помощью Go2.0.

Нет никакого существующего кода с типами сумм. Хотя я думаю, что значение по умолчанию должно быть тем, что определено в самом типе. Либо первая запись, либо первая по алфавиту, либо что-то в этом роде.

Но есть много существующего кода Go, в котором все сводится к нулю. Это определенно будет переломным моментом. Хуже того, gofix и подобные инструменты могут изменять типы переменных только на Опции (того же типа), создавая, по крайней мере, уродливый код, во всех остальных случаях он просто сломает все в мире.

Если ничего другого, подумайте: Zero нужно что-то вернуть. Но все это технические препятствия, которые можно решить - например, это препятствие довольно очевидно, если нулевое значение типа суммы четко определено и, вероятно, будет «паникой», если нет. Еще более серьезный вопрос заключается в том, почему тот или иной выбор является правильным и вписывается ли этот выбор в язык в целом, и как. ИМО, лучший способ решить эту проблему - по-прежнему говорить о конкретных случаях, когда типы сумм решают конкретные проблемы или их отсутствие создает. Для этого применяются три критерия для отчета об опыте .

Обратите внимание, в частности, что и «не должно быть нулевого значения, и следует запретить создание неинициализированных значений», и «по умолчанию должна быть первая запись» были упомянуты выше несколько раз. Итак, считаете ли вы, что так должно быть, или это не добавляет новой информации. Но это делает и без того огромную ветку еще длиннее и в будущем труднее найти в ней нужную информацию.

Давайте поразмышляем. Существует Invalid Kind, для которого значение int по умолчанию равно 0. Если бы у вас была функция, которая принимала Reflect.Kind, и вы передали неинициализированную переменную этого типа, она оказалась бы Invalid. Если, поразмыслить.

Теперь давайте рассмотрим html / template.contentType. Тип Plain является его значением по умолчанию и действительно рассматривается как таковой функцией stringify, поскольку это резерв. В будущем с гипотетической суммой такое поведение не только будет необходимо, но также невозможно использовать для него значение nil, поскольку nil ничего не будет значить для пользователя этого типа. Здесь будет практически обязательно всегда возвращать именованное значение, и у вас есть четкое значение по умолчанию того, каким должно быть это значение.

Это я снова с другим примером, где хорошо работают алгебраические / вариативные / суммы / любые типы данных.

Итак, мы используем базу данных noSQL без транзакций (распределенная система, транзакции для нас не работают), но мы любим целостность и согласованность данных по очевидной причине и должны обходить проблемы одновременного доступа, обычно с помощью немного сложных запросов условного обновления за один запись (запись одной записи является атомарной).

У меня новая задача - написать набор сущностей, которые можно вставлять, добавлять или удалять (только одна из этих операций).

Если бы у нас было что-то вроде

type EntityOp oneof {
    Insert   Reference
    NewState string
    Delete   struct{}
}

Метод мог быть просто

type DB interface {
    …
    Capture(ctx context.Context, processID string, ops map[string]EntityOp) (bool, error)
}

Одним из фантастических способов использования суммы времен является представление узлов в AST. Другой - заменить nil на option который проверяется во время компиляции.

@DemiMarie, но в сегодняшнем Go эта сумма также может быть нулевой, как я предлагал выше, мы можем просто сделать nil вариантом каждого перечисления, в каждом переключателе будет регистр nil, но это обязательство не так уж плохо, особенно если мы хотите эту функцию, не нарушая весь существующий код go (в настоящее время у нас есть все, что нужно)

Не знаю, принадлежит ли он сюда, но все это осталось от меня в Typescript, где существует очень классная функция под названием «Строковые литералы», и мы можем это сделать:

var name: "Peter" | "Consuela"; // string type with compile-time constraint

Это похоже на строковое перечисление, которое, на мой взгляд, намного лучше, чем традиционные числовые перечисления.

@Merovius
конкретный пример - работа с произвольным JSON.
В Rust это можно представить как
enum Value {
Нулевой,
Булево (булево),
Число (Число),
Строка (Строка),
Массив (Vec),
Объект (Карта),
}

Тип союза как два преимущества:

  1. Самостоятельное документирование кода
  2. Разрешение компилятору или go vet проверять неправильное использование типа объединения
    (например, переключатель, на котором проверяются не все типы)

Для синтаксиса следующее должно быть совместимо с Go1 , как и с псевдонимом типа :

type Token = int | float64 | string

Тип объединения может быть реализован внутри как интерфейс; что важно, так это то, что использование типа union позволяет сделать код более читаемым и выявлять такие ошибки, как

var tok Token

switch t := tok.(type) {
case int:
    // do something
}

Компилятор должен выдать ошибку, поскольку в переключателе используются не все типы Token .

Проблема заключается в том, что (насколько мне известно) нет способа хранить типы указателей (или типы, содержащие указатели, такие как string ) и типы, не являющиеся указателями, вместе. Даже типы с разной раскладкой не подойдут. Не стесняйтесь поправлять меня, но проблема в том, что точный сборщик мусора плохо работает с переменными, которые могут быть указателями и простыми переменными одновременно.

Мы можем пойти по пути неявного бокса - как сейчас делает interface{} . Но я не думаю, что это дает достаточные преимущества - это все еще выглядит как прославленный тип интерфейса. Может, взамен можно разработать какую-нибудь vet проверку?

Сборщик мусора должен будет прочитать биты тега из объединения, чтобы определить макет. Это не невозможно, но было бы большим изменением в среде выполнения, которое могло бы замедлить работу gc.

Может, вместо этого можно разработать какую-нибудь ветеринарную проверку?

https://github.com/BurntSushi/go-sumtype

Сборщик мусора должен будет прочитать биты тега из объединения, чтобы определить макет.

Это та же самая гонка, которая существовала с интерфейсами, когда они могли содержать не указатели. От этого дизайна явно отказались.

go-sumtype интересно, спасибо. Но что произойдет, если один и тот же пакет определяет два типа объединения?

Компилятор мог бы реализовать тип объединения внутри как интерфейс, но добавив унифицированный синтаксис и стандартную проверку типов.

Если есть N проектов, использующих типы объединения, каждый по-разному и с достаточно большим N, возможно, введение одного способа сделать это может быть лучшим решением.

Но что произойдет, если один и тот же пакет определяет два типа объединения?

Ничего особенного? Логика зависит от типа и использует фиктивный метод для распознавания разработчиков. Просто используйте разные имена для фиктивных методов.

@skybrian IIRC текущее растровое изображение, которое определяет макет типа, в настоящее время хранится в одном месте. Добавление такой вещи для каждого объекта добавит много переходов и сделает каждый необязательный объект корнем сборщика мусора.

Проблема с этим заключается в том, что (насколько мне известно) нет способа хранить типы указателей (или типы, содержащие указатели, такие как строка) и типы без указателей вместе

Я не считаю это необходимым. Компилятор может перекрывать макет для типов, когда совпадают карты указателей, а не иначе. Если они не совпадают, их можно будет разместить последовательно или использовать указатель, который используется в настоящее время для интерфейсов. Он может даже использовать несмежные макеты для членов структуры.

Но я не думаю, что это дает достаточные преимущества - это все еще выглядит как прославленный тип интерфейса.

В моем предложении типы объединения - это _ точно_ прославленный тип интерфейса - тип объединения - это просто подмножество интерфейса, которому разрешено хранить только перечислимый набор типов. Это потенциально дает компилятору свободу выбора более эффективного метода хранения для определенных наборов типов, но это деталь реализации, а не основная мотивация.

@rogpeppe - Из любопытства могу ли я использовать тип суммы напрямую или мне нужно явно привести его к известному типу, чтобы что-то с ним сделать? Потому что, если мне нужно постоянно приводить его к известному типу, я действительно не знаю, какие преимущества это дает, чем то, что уже дано нам с интерфейсами. Основное преимущество, которое я вижу, - это проверка ошибок во время компиляции, вроде того, что немаршалинг все равно будет происходить во время выполнения, что более вероятно, когда вы увидите проблему с переданным недопустимым типом. Другим преимуществом является более ограниченный интерфейс, который, как мне кажется, не требует смены языка.

Я могу сделать

type FooType int | float64

func AddOne(foo FooType) FooType {
    return foo + 1
}

// if this can be done, what happens here?
type FooType nil | int
func AddOne(foo FooType) FooType {
    return foo + 1
}

Если это невозможно, я не вижу большой разницы с

type FooType interface{}

func AddOne(foo FooType) (FooType, error) {
    switch v := foo.(type) {
        case int:
              return v + 1, nil
        case float64:
              return v + 1.0, nil
    }

    return nil, fmt.Errorf("invalid type %T", foo)
}

// versus
type FooType int | float64

func AddOne(foo FooType) FooType {
    switch v := foo.(type) {
        case int:
              return v + 1
        case float64:
              return v + 1.0
    }

    // assumes the compiler knows that there is no other type is 
    // valid and thus this should always returns a value
    // Would the compiler error out on incomplete switch types?
}

@xibz

Могу ли я из любопытства использовать тип суммы напрямую или мне нужно явно привести его к известному типу, чтобы что-то с ним сделать? Потому что, если мне нужно постоянно приводить его к известному типу, я действительно не знаю, какие преимущества это дает, чем то, что уже дано нам с интерфейсами.

@rogpeppe , поправьте меня, если я ошибаюсь 🙏
Необходимость всегда выполнять сопоставление с образцом (так называется "приведение" при работе с типами сумм в функциональных языках программирования) на самом деле является одним из самых больших преимуществ использования типов сумм. Принуждение разработчика явно обрабатывать все возможные формы типа суммы - это способ предотвратить использование разработчиком переменной, считая, что она принадлежит к данному типу, тогда как на самом деле это другой тип. Преувеличенный пример в JavaScript:

const a = "1" // string "1"
const b = a + 5 // string "15" and not number 6

Если это невозможно, я не вижу большой разницы с

Я думаю, вы сами заявляете о некоторых преимуществах, не так ли?

Основное преимущество, которое я вижу, - это проверка ошибок во время компиляции, вроде того, что немаршалинг все равно будет происходить во время выполнения, что более вероятно, когда вы увидите проблему с переданным недопустимым типом. Другим преимуществом является более ограниченный интерфейс, который, как мне кажется, не требует смены языка.

// Would the compiler error out on incomplete switch types?

Исходя из того, что делают языки функционального программирования, я думаю, что это должно быть возможно и настраиваемое 👍

@xibz также

@xibz

Могу ли я из любопытства использовать тип суммы напрямую или мне нужно явно привести его к известному типу, чтобы что-то с ним сделать?

Вы можете вызывать для него методы, если все члены типа используют этот метод.

Если взять ваш int | float64 в качестве примера, что будет в результате:

var x int|float64 = int(2)
var y int|float64 = float64(0.5)
fmt.Println(x * y)

Произойдет ли неявное преобразование из int в float64 ? Или от float64 до int . Или паника?

Итак, вы почти правы - в большинстве случаев вам нужно будет проверить тип перед использованием. Я считаю, что это преимущество, а не недостаток.

Кстати, преимущество времени выполнения может быть значительным. Чтобы продолжить ваш пример типа, срез типа [](int|float64) не должен содержать никаких указателей, потому что можно представить все экземпляры типа в нескольких байтах (вероятно, 16 байтов из-за ограничений выравнивания), что может в некоторых случаях приводят к значительному повышению производительности.

@rogpeppe

в большинстве случаев вам нужно будет проверить тип перед его использованием. Я считаю, что это преимущество, а не недостаток.

Я согласен, что это не недостаток. Я просто пытаюсь понять, какие преимущества это дает нам по сравнению с интерфейсами.

срез типа не обязательно должен содержать какие-либо указатели, потому что можно представить все экземпляры типа в нескольких байтах (вероятно, в 16 байтах из-за ограничений выравнивания), что в некоторых случаях может привести к значительному повышению производительности.

Хм, я не уверен, что покупаю значительную часть этого. Я уверен, что в очень редких случаях это уменьшит размер памяти наполовину. С учетом сказанного, я не думаю, что сохраненная память недостаточно значительна для изменения языка.

@stouf

Необходимость всегда выполнять сопоставление с образцом (так называется "приведение" при работе с типами сумм в функциональных языках программирования) на самом деле является одним из самых больших преимуществ использования типов сумм.

Но какие преимущества это дает языку, который еще не поддерживает интерфейсы? Изначально я был полностью сторонником сумм, но когда я начал думать об этом, я как бы потерял, какие преимущества это принесет.


С учетом всего сказанного, если использование типа суммы может обеспечить более чистый и читаемый код, я был бы за это на 100%. Однако на первый взгляд кажется, что он будет почти идентичен коду интерфейса.

@xibz сопоставление с образцом было бы полезно в коде

Это немного надумано, но, например, если у вас есть синтаксическое дерево выражений, чтобы сопоставить квадратное уравнение, вы можете сделать что-то вроде:

match Add(Add(Mult(Const(a), Power(Var(x), 2)), Mult(Const(b), Var(x))), Const(c)) {
  // here a, b, c are bound to the constants and x is bound to the variable name.
  // x must have been the same in both var expressions or it wouldn't match.
}

Простые примеры, которые идут только на один уровень в глубину, не покажут большой разницы, но здесь мы поднимаемся до пяти уровней, что было бы довольно сложно сделать с переключателями вложенного типа. Язык с сопоставлением с образцом может быть многоуровневым, при этом вы не пропустите ни одного случая.

Однако я не уверен, насколько это возможно вне компиляторов.

@xibz
Преимущество типов суммы в том, что вы и компилятор точно знаете, какие типы могут существовать в сумме. В этом вся разница. С пустыми интерфейсами вам всегда придется беспокоиться и защищаться от злоупотреблений в api, всегда имея ветку, единственной целью которой является восстановление, когда пользователь дает вам тип, которого вы не ожидаете.

Поскольку кажется мало надежды на то, что типы сумм будут реализованы в компиляторе, я надеюсь, что по крайней мере стандартная директива комментариев, такая как //go:union A | B | C , предложена и поддерживается go vet .

При стандартном способе объявления типа суммы через N лет можно будет узнать, сколько пакетов его используют.

С недавними проектами дизайна для дженериков, возможно, к ним можно было бы привязать типы сумм.

В одном из проектов высказывалась идея использовать интерфейсы вместо контрактов, и интерфейсы должны были поддерживать списки типов:

type Foo interface { 
     int64, int32, int, uint, uint32, uint64
}

Хотя это само по себе не приведет к объединению, упакованному в память, но, возможно, при использовании в универсальной функции или структуре оно не будет упаковано в коробку и, по крайней мере, обеспечит безопасность типов при работе с конечным списком типов.

И, возможно, использование этих конкретных интерфейсов в переключателях типов потребует, чтобы такой переключатель был исчерпывающим.

Это не идеальный короткий синтаксис (например: Foo | int32 | []Bar ), но это что-то.

С недавними проектами дизайна для дженериков, возможно, к ним можно было бы привязать типы сумм.

В одном из проектов высказывалась идея использовать интерфейсы вместо контрактов, и интерфейсы должны были поддерживать списки типов:

type Foo interface { 
     int64, int32, int, uint, uint32, uint64
}

Хотя это само по себе не приведет к объединению, упакованному в память, но, возможно, при использовании в универсальной функции или структуре оно не будет упаковано в коробку и, по крайней мере, обеспечит безопасность типов при работе с конечным списком типов.

И, возможно, использование этих конкретных интерфейсов в переключателях типов потребует, чтобы такой переключатель был исчерпывающим.

Это не идеальный короткий синтаксис (например: Foo | int32 | []Bar ), но это что-то.

Очень похоже на мое предложение: https://github.com/golang/go/issues/19412#issuecomment -520306000

type foobar union {
  int
  float
}

@mathieudevos вау, мне это на самом деле очень нравится.

Для меня самая большая странность (на самом деле единственная оставшаяся странность) с последним предложением дженериков - это списки типов в интерфейсах. Они просто не подходят . Затем вы получаете несколько интерфейсов, которые можно использовать только как ограничения параметра типа, и так далее ...

На мой взгляд, концепция union отлично работает, потому что вы могли затем встроить union в interface чтобы выполнить «ограничение, включающее методы и необработанные типы». Интерфейсы продолжают функционировать как есть, а с семантикой, определенной для объединения, их можно использовать в обычном коде, и ощущение странности уходит.

// Ordinary interface
type Stringer interface {
    String() string
}

// Type union
type Foobar union {
    int
    float
}

// Equivalent to an interface with a type list
type FoobarStringer interface {
    Stringer
    Foobar
}

// Unions can intersect
type SuperFoo union {
    Foobar
    int
}

// Doesn't compile since these unions can't intersect
type Strange interface {
    Foobar
    union {
        int
        string
    }
}

РЕДАКТИРОВАТЬ - На самом деле, только что видел этот CL: https://github.com/golang/go/commit/af48c2e84b52f99d30e4787b1b8d527b5cd2ab64

Основное преимущество этого изменения заключается в том, что оно открывает двери для общих
(без ограничений) использование интерфейсов со списками типов

...Большой! Интерфейсы становятся полностью пригодными для использования как типы сумм, что унифицирует семантику для обычного и ограниченного использования. (Очевидно, еще не включен, но я думаю, что это отличное место, куда стоит направиться.)

Я открыл # 41716, чтобы обсудить способ, которым версия типов суммы появляется в текущем проекте проекта универсальных шаблонов.

Я просто хотел поделиться старым предложением @henryas об алгебраических типах данных. Он очень хорошо написан с предоставленными вариантами использования.
https://github.com/golang/go/issues/21154
К сожалению, @mvdan закрыл его в тот же день без какой-либо оценки проделанной работы. Я почти уверен, что этот человек действительно так себя чувствовал, и поэтому в аккаунте gh больше нет действий. Мне жаль того парня.

Мне очень нравится # 21154. Кажется, это совсем другое дело (и, следовательно, комментарий @mvdan ), закрывающий его как обман, не совсем удачный. Открыть там снова или включить в обсуждение здесь?

Да, мне бы очень хотелось иметь возможность моделировать бизнес-логику более высокого уровня таким же образом, как описано в этом выпуске. Типы сумм для перечислимых, ограниченных параметров и предлагаемые принятые типы, как в другой проблеме, были бы замечательными в наборе инструментов. Бизнес / код домена в Go в настоящий момент иногда кажется немного неуклюжим.

Моя единственная обратная связь заключается в том, что type foo,bar внутри интерфейса выглядит немного неудобным и второсортным, и я согласен с тем, что должен быть выбор между допускающим значение NULL и не допускающим NULL (если возможно).

@ProximaB Я не понимаю, почему вы говорите: «Больше никаких действий в аккаунте gh нет». С тех пор они создали и прокомментировали множество других проблем, многие из которых относятся к проекту Go. Я не вижу никаких доказательств того, что на их деятельность вообще повлияла эта проблема.

Более того, я полностью согласен с тем, что Даниэль закрыл этот вопрос как обман. Я не понимаю, почему @andig говорит, что они предлагают что-то другое. Насколько я могу понять текст # 21154, он предлагает в точности то же самое, что мы обсуждаем здесь, и я бы совсем не удивился, если даже точный синтаксис уже был предложен где-то в этом мегапотоке (семантика, насколько описывались, наверняка были. несколько раз). Фактически, я бы даже сказал, что закрытие Дэниэлса подтверждается объемом этого выпуска, потому что он уже содержит довольно подробное и нюансированное обсуждение # 21154, поэтому повторение всего этого было бы трудным и излишним.

Я согласен и понимаю, что, вероятно, разочарование в том, что предложение закрыто как обман. Но я не знаю практического способа избежать этого. Обсуждение в одном месте кажется полезным для всех участников, а оставлять открытыми несколько вопросов для одного и того же, без какого-либо обсуждения их, явно бессмысленно.

Более того, я полностью согласен с тем, что Даниэль закрыл этот вопрос как обман. Я не понимаю, почему @andig говорит, что они предлагают что-то другое. Насколько я понимаю текст № 21154, он предлагает то же самое, что мы обсуждаем здесь.

Перечитывая этот выпуск, я согласен. Кажется, я запутался в этой проблеме с контрактами на дженерики. Я категорически поддерживаю типы сумм. Я не хотел показаться резким, примите мои извинения, если это натолкнулось на это.

Я человек, и время от времени решение проблем может быть непростым, поэтому обязательно указывайте, когда я делаю ошибку :) Но в этом случае я действительно думаю, что любое предложение по конкретным типам сумм должно развиваться из этой ветки точно так же, как https: / /github.com/golang/go/issues/19412#issuecomment -701625548

Я человек, и время от времени решение проблем может быть непростым, поэтому обязательно указывайте, когда я делаю ошибку :) Но в этом случае я действительно думаю, что любое конкретное предложение типов сумм должно быть ответвлением из этой темы, как и # 19412 ( комментарий)

@mvdan не человек. Поверьте мне. Я его сосед. Просто шучу.

Спасибо за внимание. Я не так привязан к своим предложениям. Не стесняйтесь искажать, модифицировать и сбивать любую их часть. Я был занят в реальной жизни, поэтому у меня нет возможности активно участвовать в обсуждениях. Приятно знать, что люди читают мои предложения, и некоторым они действительно нравятся.

Первоначальное намерение состоит в том, чтобы разрешить группировку типов по их релевантности предметной области, где они не обязательно разделяют общее поведение, и чтобы компилятор обеспечил это. На мой взгляд, это просто проблема статической проверки, которая выполняется во время компиляции. Компилятору нет необходимости генерировать код, сохраняющий сложные отношения между типами. Сгенерированный код может обрабатывать эти типы домена нормально, как если бы они были типом обычного интерфейса {}. Разница в том, что теперь компилятор выполняет дополнительную проверку статического типа при компиляции. В этом суть моего предложения № 21154.

@henryas Рад тебя видеть! 😊
Мне интересно, если бы Голанг не использовал утиную типизацию, это сделало бы отношения между типами намного более строгими и позволило бы группировать объекты по их релевантности предметной области, как вы описали в своем предложении.

@henryas Рад тебя видеть! 😊
Мне интересно, если бы Голанг не использовал утиную типизацию, это сделало бы отношения между типами намного более строгими и позволило бы группировать объекты по их релевантности предметной области, как вы описали в своем предложении.

Было бы, но это нарушило бы обещание совместимости с Go 1. Нам, вероятно, не понадобились бы типы сумм, если бы у нас был явный интерфейс. Тем не менее, утиная печать - не обязательно плохо. Это делает некоторые вещи более легкими и удобными. Мне нравится утиная печать. Это вопрос использования правильного инструмента для работы.

@henryas Согласен. Это был гипотетический вопрос. Создатели Go определенно глубоко учли все взлеты и падения.
С другой стороны, руководство по кодированию, такое как проверка соответствия интерфейса, никогда не появится.
https://github.com/uber-go/guide/blob/master/style.md#verify -interface-compliance

Не могли бы вы провести это обсуждение не по теме где-нибудь еще? На этот выпуск подписано много людей.
Удовлетворенность открытым интерфейсом была частью Go с момента его создания, и она не изменится.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги