Go: предложение: спецификация: общие средства программирования

Созданный на 14 апр. 2016  ·  816Комментарии  ·  Источник: golang/go

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

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

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

Go2 LanguageChange NeedsInvestigation Proposal generics

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

Позвольте мне предварительно напомнить всем о нашей https://golang.org/wiki/NoMeToo политике. Вечеринка смайликов выше.

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

CL https://golang.org/cl/22057 упоминает эту проблему.

Позвольте мне предварительно напомнить всем о нашей https://golang.org/wiki/NoMeToo политике. Вечеринка смайликов выше.

Существует Summary of Go Generics Discussions , в котором делается попытка дать обзор обсуждений из разных мест. Он также предоставляет несколько примеров решения проблем, в которых вы хотели бы использовать дженерики.

В связанном предложении есть два «требования», которые могут усложнить реализацию и снизить безопасность типов:

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

Эти требования, кажется, исключают, например, систему, подобную системе трейтов Rust, где универсальные типы ограничены границами трейтов. Зачем они нужны?

Возникает соблазн встроить дженерики в стандартную библиотеку на очень низком уровне, как в C++ std::basic_string., стд::распределитель>. В этом есть свои преимущества — иначе никто бы этого не делал, — но он имеет широкомасштабные и иногда неожиданные последствия, такие как непонятные сообщения об ошибках C++.

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

Универсальный код не должен быть спецификацией универсального типа.

@tamird Важной особенностью типов интерфейса Go является то, что вы можете определить неинтерфейсный тип T, а затем определить тип интерфейса I, чтобы T реализовал I. См. https://golang.org/doc/faq#implements_interface . Было бы непоследовательно, если бы Go реализовал форму дженериков, для которой универсальный тип G можно было бы использовать только с типом T, в котором явно сказано: «Я могу быть использован для реализации G».

Я не знаком с Rust, но я не знаю ни одного языка, который требует, чтобы T явно заявлял, что его можно использовать для реализации G. Два упомянутых вами требования не означают, что G не может предъявлять требования к T, просто поскольку I налагает требования на T. Требования просто означают, что G и T могут быть написаны независимо. Это очень желательная функция для дженериков, и я не могу отказаться от нее.

@ianlancetaylor https://doc.rust-lang.org/book/traits.html объясняет особенности Rust. Хотя я думаю, что в целом это хорошая модель, она плохо подходит для современного Go.

@sbunce Я также думал, что концепции были ответом, и вы можете видеть, что идея разбросана по различным предложениям перед последним. Но обескураживает тот факт, что концепции изначально планировались для того, что стало C++11, а сейчас уже 2016 год, и они все еще вызывают споры и не очень близки к тому, чтобы быть включенными в язык C++.

Будет ли полезна научная литература для каких-либо указаний по оценке подходов?

Единственная статья, которую я читал на эту тему, — « Полезны ли разработчикам универсальные типы? » (извините, платный доступ, вы можете найти в Google путь к загрузке в формате pdf), в котором было следующее, чтобы сказать

Следовательно, консервативная интерпретация эксперимента
в том, что универсальные типы можно рассматривать как компромисс
между положительными характеристиками документации и
отрицательные характеристики растяжимости. Захватывающая часть
исследование заключается в том, что оно показало ситуацию, когда использование
(более сильная) система статического типа оказала негативное влияние на
время разработки и в то же время ожидаемая польза
fit – сокращение времени исправления ошибок типа – не появилось.
Мы думаем, что такие задачи могли бы помочь в будущих экспериментах в
выявление влияния систем типов.

Я также вижу, что https://github.com/golang/go/issues/15295 также ссылается на легкие, гибкие объектно-ориентированные дженерики .

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

См.: http://dl.acm.org/citation.cfm?id=2738008 Барбары Лисков:

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

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

Было бы интересно иметь один или несколько экспериментальных транспиляторов — исходный код Go generics для компилятора исходного кода Go 1.xy.
Я имею в виду - слишком много разговоров/аргументов для моего мнения, и никто не пишет исходный код, который _пытается_ реализовать _какие-то_ дженерики для Go.

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

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

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

Пингую @yizhouzhang и @andrewcmyers , чтобы они могли высказать свое мнение о роде, подобном дженерикам в Go. Похоже, это может быть хорошая пара :)

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

Вот ссылка на документ, для которого не требуется доступ к цифровой библиотеке ACM:
http://www.cs.cornell.edu/andru/papers/genus/

Домашняя страница Genus находится здесь: http://www.cs.cornell.edu/projects/genus/

Мы еще не выпустили компилятор публично, но планируем сделать это довольно скоро.

Рады ответить на любые вопросы людей.

С точки зрения матрицы решений @mandolyte , Genus набирает 17 баллов, деля первое место. Я бы добавил еще несколько критериев для оценки. Например, модульная проверка типов важна, как и другие, такие как @sbunce , отмеченные выше, но в схемах на основе шаблонов она отсутствует. Технический отчет для статьи Genus содержит гораздо большую таблицу на странице 34, в которой сравниваются различные дизайны дженериков.

Я только что просмотрел весь документ Summary of Go Generics , который был полезным резюме предыдущих обсуждений. Механизм обобщений в Genus, на мой взгляд, не страдает от проблем, выявленных для C++, Java или C#. Генераторы рода овеществлены, в отличие от Java, поэтому вы можете узнать типы во время выполнения. Вы также можете создавать экземпляры для примитивных типов, и вы не получите неявную упаковку в тех местах, где вам это действительно не нужно: массивы T, где T является примитивом. Система типов наиболее близка к Haskell и Rust — на самом деле она немного мощнее, но я думаю, что она также интуитивно понятна. Примитивная специализация аля C# в настоящее время не поддерживается в Genus, но может быть. В большинстве случаев специализация может быть определена во время компоновки, поэтому генерация истинного кода во время выполнения не требуется.

CL https://golang.org/cl/22163 упоминает эту проблему.

Способ ограничения универсальных типов, не требующий добавления новых языковых концепций: https://docs.google.com/document/d/1rX4huWffJ0y1ZjqEpPrDy-kk-m9zWfatgCluGRBQveQ/edit?usp=sharing.

Genus выглядит действительно круто, и это явно важное достижение искусства, но я не понимаю, как это применимо к Go. Есть ли у кого-нибудь набросок того, как это будет интегрироваться с системой/философией типов Go?

Проблема в том, что команда go блокирует попытки. В названии четко изложены намерения команды go. И если этого было недостаточно, чтобы отпугнуть всех желающих, функции, требуемые от такой широкой области в предложениях ian, ясно показывают, что если вам нужны дженерики, то они не хотят вас. Глупо даже пытаться вести диалог с командой go. Тем, кто ищет дженерики в go, я говорю: ломайте язык. Начните новое путешествие — многие последуют за вами. Я уже видел отличную работу, проделанную в форках. Организуйте себя, сплотитесь вокруг дела

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

@andrewcmyers надеется, что @ianlancetaylor поможет вам в этом. Просто несколько примеров, на которые можно было бы посмотреть, очень помогли бы.

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

Одним из ключевых аспектов Go является то, что когда вы пишете программу на Go, большая часть того, что вы пишете, — это код. Это отличается от C++ и Java, где вы пишете гораздо больше типов. Genus, кажется, в основном связан с типами: вы пишете ограничения и модели, а не код. Система типов Go очень проста. Система типов Genus гораздо сложнее.

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

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

Я не эксперт в Go, но его система типов не кажется проще, чем Java до дженериков. Синтаксис типа немного легче в хорошем смысле, но основная сложность кажется примерно такой же.

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

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

Помимо относительной сложности, они настолько разные, что Genus не может отобразить 1:1. Отсутствие подтипов кажется большим.

Если тебе интересно:

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

В отличие от большинства языков, спецификация Go очень короткая и четкая в отношении соответствующих свойств системы типов, начиная с https://golang.org/ref/spec#Constants и заканчивая разделом под названием «Блоки» (все из которых напечатано менее 11 страниц).

В отличие от универсальных шаблонов Java и C#, механизм универсальных шаблонов Genus не основан на подтипах. С другой стороны, мне кажется, что в Go есть подтипы, но структурные подтипы. Это также хорошо сочетается с подходом рода, который имеет структурный оттенок, а не полагается на заранее объявленные отношения.

Я не верю, что в Go есть структурные подтипы.

Хотя два типа, базовый тип которых идентичен, поэтому идентичны
можно заменять друг другом, https://play.golang.org/p/cT15aQ-PFr

Это не распространяется на два типа, которые имеют общее подмножество полей,
https://play.golang.org/p/KrC9_BDXuh.

В четверг, 28 апреля 2016 г., в 13:09, Эндрю Майерс, [email protected]
написал:

В отличие от дженериков Java и C#, механизм дженериков Genus не основан на
подтип. С другой стороны, мне кажется, что в Go есть подтипы,
но структурное подтипирование. Это также хорошо подходит для подхода Genus,
который имеет структурный вкус, а не полагается на заранее объявленные
отношения.


Вы получаете это, потому что подписаны на эту тему.
Ответьте на это письмо напрямую или просмотрите его на GitHub
https://github.com/golang/go/issues/15292#issuecomment-215298127

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

Именно поэтому я пропинговал вас, genus кажется гораздо лучшим подходом, чем Java/C #, такие как дженерики.

Были некоторые идеи относительно специализации на типах интерфейсов; например, подход _package templates_ "предложения" 1 2 являются его примерами.

тл;др; общий пакет со специализацией интерфейса будет выглядеть так:

package set
type E interface { Equal(other E) bool }
type Set struct { items []E }
func (s *Set) Add(item E) { ... }

Версия 1. со специализацией на уровне пакета:

package main
import items set[[E: *Item]]

type Item struct { ... }
func (a *Item) Equal(b *Item) bool { ... }

var xs items.Set
xs.Add(&Item{})

Версия 2. специализация области объявления:

package main
import set

type Item struct { ... }
func (a *Item) Equal(b *Item) bool { ... }

var xs set.Set[[E: *Item]]
xs.Add(&Item{})

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

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

type E interface{}
func Reduce(zero E, items []E, fn func(a, b E) E) E { ... }

Reduce[[E: int]](0, []int{1,2,3,4}, func(a, b int)int { return a + b } )
// there are probably ways to have some aliases (or similar) to make it less verbose
alias ReduceInt Reduce[[E: int]]
func ReduceInt Reduce[[E: int]]

Подход к специализации интерфейса имеет интересные свойства:

  • Уже существующие пакеты, использующие интерфейсы, можно было бы специализировать. например, я мог бы вызвать sort.Sort[[Interface:MyItems]](...) и выполнить сортировку по конкретному типу, а не по интерфейсу (с потенциальным выигрышем от встраивания).
  • Тестирование упрощено, мне нужно только убедиться, что общий код работает с интерфейсами.
  • Легко сказать, как это работает. т.е. представьте, что [[E: int]] заменяет все объявления E на int .

Но при работе с пакетами возникают проблемы с многословием:

type op
import "set"

type E interface{}
func Union(a, b set.Set[[set.E: E]]) set.Set[[set.E: E]] {
    result := set.New[[set.E: E]]()
    ...
}

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

_PS, тем, кто ворчит о медленном прогрессе дженериков, я аплодирую команде Go за то, что они тратят больше времени на проблемы, которые приносят большую пользу сообществу, например, ошибки компилятора/времени выполнения, SSA, GC, http2._

@egonelbre ваше замечание о том, что дженерики на уровне пакетов предотвратят «злоупотребление», действительно важно, и я думаю, что большинство людей упускают из виду. Это, а также их относительная семантическая и синтаксическая простота (затрагиваются только конструкции package и import) делают их очень привлекательными для Go.

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

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

type [V, E] Graph [V interface { Edges() E }, E interface { Endpoints() (V, V) }] ...

Я думаю, что большая проблема с интерфейсами в качестве ограничений заключается в том, что методы не так распространены в Go, как в Java. Встроенные типы не имеют методов. Нет набора универсальных методов, подобных тем, что используются в java.lang.Object. Пользователи обычно не определяют такие методы, как Equals или HashCode, для своих типов, если в этом нет особой необходимости, потому что эти методы не определяют тип для использования в качестве ключей сопоставления или в любом алгоритме, требующем равенства.

(Равенство в Go — интересная история. Язык дает вашему типу «==», если он соответствует определенным требованиям (см. https://golang.org/ref/spec#Logical_operators, ищите «comparable»). Любой тип с « ==" может служить в качестве ключа карты. Но если ваш тип не заслуживает "==", то вы ничего не можете написать, чтобы заставить его работать в качестве ключа карты.)

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

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

введите целое число
func (i Int) Less(j Int) bool { return i < j }

Тип Int «наследует» все операторы и другие свойства типа int. Хотя вам нужно использовать между ними два, чтобы использовать Int и int вместе, что может быть проблемой.

Здесь могут помочь модели рода. Но они должны быть очень простыми. Я думаю, что @ianlancetaylor был слишком узок в своей характеристике Go как написания большего количества кода, меньшего количества типов. Общий принцип заключается в том, что Go не терпит сложности. Мы смотрим на Java и C++ и полны решимости никогда туда не заходить. (Не обижайся.)

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

@jba Мы, безусловно, согласны с принципом избегания сложности. «Как можно проще, но не проще». По этой причине я бы, вероятно, исключил некоторые функции Genus из Go, по крайней мере, на первых порах.

Одна из приятных особенностей подхода Genus заключается в том, что он гладко обрабатывает встроенные типы. Напомним, что примитивные типы в Java не имеют методов, и Genus наследует это поведение. Вместо этого Genus обращается с примитивными типами так, как если бы у них был довольно большой набор методов для удовлетворения ограничений. Хеш-таблица требует, чтобы ее ключи можно было хешировать и сравнивать, но все примитивные типы удовлетворяют этому ограничению. Таким образом, экземпляры типов, такие как Map[int, boolean] , вполне законны и не требуют дополнительной суеты. Для этого нет необходимости различать два типа целых чисел (int и Int). Однако, если бы int не был снабжен достаточным количеством операций для некоторых применений, мы бы использовали модель, почти точно такую ​​же, как использование Int выше.

Еще одна вещь, о которой стоит упомянуть, — это идея «натуральных моделей» в роде. Обычно вам не нужно объявлять модель для использования универсального типа: если аргумент типа удовлетворяет ограничению, естественная модель генерируется автоматически. Наш опыт показывает, что это обычный случай; объявление явных именованных моделей обычно не требуется. Но если нужна модель — например, если вы хотите хешировать целые числа нестандартным способом — тогда синтаксис аналогичен тому, что вы предложили: Map[int with fancyHash, boolean] . Я бы сказал, что Genus синтаксически легок в обычных случаях использования, но с резервом мощности, когда это необходимо.

@egonelbre То, что вы здесь предлагаете, похоже на виртуальные типы, поддерживаемые Scala. Существует документ ECOOP'97, написанный Крестеном Крабом Торупом "Универсальность в Java с виртуальными типами", в котором исследуется это направление. Мы также разработали механизмы для виртуальных типов и виртуальных классов в нашей работе («J&: вложенное пересечение для масштабируемой композиции программного обеспечения», OOPSLA'06).

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

// (общее) определение типа функции
тип Sum64 func (X, Y) float64 {
вернуть float64 (X) + float64 (Y)
}

// создать экземпляр, позиционно
я := 42
вар j уинт = 86
сумма := &Sum64{i, j}

// создать экземпляр по именованным типам параметров
сумма := &Sum64{ X: целое, Y: uint}

// теперь используем его...
результат := sum(i, j) // результат 128

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

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

Так что вопрос в том, как это спланировать.

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

Возможный синтаксис?

// Module defining generic type
module list(type t)

type node struct {
    next *node
    data t
}
// Module using generic type:
import (
    intlist "module/path/to/list" (int)
    funclist "module/path/to/list" (func (int) int)
)

l := intlist.New()
l.Insert(5)

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

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

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

@thwd Так что автор библиотеки продолжит использовать интерфейсы, но без переключения типов и утверждений типов, необходимых сегодня. И будет ли пользователь библиотеки просто передавать конкретные типы, как если бы библиотека использовала типы как есть... и затем компилятор согласовал бы их? А если нельзя было указать, почему? (например, в библиотеке использовался оператор по модулю, но пользователь предоставил часть чего-то.

Я близко? :-)

@mandolyte да! давайте обменяемся письмами, чтобы не засорять эту тему. Вы можете связаться со мной по адресу «me at thwd dot me». Любой, кто читает это, может быть заинтересован; напишите мне письмо и я добавлю вас в тему.

Это отличная функция для type system и collection library .
Потенциальный синтаксис:

type Element<T> struct {
    prev, next *Element<T>
    list *List<T>
    value T
}
type List<E> struct {
    root Element<E>
    len int
}

За interface

type Collection<E> interface {
    Size() int
    Add(e E) bool
}

super type или type implement :

func contain(l List<parent E>, e E) bool
<V> func (c Collection<child E>)Map(fn func(e E) V) Collection

Вышеприведенный ака в java:

boolean contain(List<? super E>, E e)
<V> Collection Map(Function<? extend E, V> mapFunc);

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

Просто имейте в виду, что стоимость интерфейса невероятно велика.

Уточните, пожалуйста, почему вы считаете стоимость интерфейса "невероятной"?
большой.
Это не должно быть хуже, чем неспециализированные виртуальные вызовы C++.

@minux Я не могу сказать о затратах на производительность, но в отношении качества кода. interface{} нельзя проверить во время компиляции, но дженерики могут. На мой взгляд, в большинстве случаев это важнее, чем проблемы с производительностью при использовании interface{} .

@xoviat

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

Есть (как минимум) два недостатка.

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

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

Является ли гипертип хорошей идеей?

func getAddFunc (aType type) func(aType, aType) aType {
    return func(a, b aType) aType {
        return a+b
    }
}

Является ли гипертип хорошей идеей?

То, что вы описываете здесь, - это просто параметризация типов в стиле C++ (т.е. шаблоны). Он не выполняет модульную проверку типов, потому что невозможно узнать, что тип aType имеет операцию + из данной информации. Ограниченная параметризация типов, как в CLU, Haskell, Java, Genus, является решением.

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

CL https://golang.org/cl/38731 упоминает эту проблему.

@andrewcmyers

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

Конечно есть. Это ограничение подразумевается в определении функции, и ограничения этой формы могут распространяться на все (транзитивные) вызывающие во время компиляции функции getAddFunc .

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

Добавил мое предложение как 2016-09-compile-time-functions.md .

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

@bcmills Я чувствую, что функции времени компиляции - это мощная идея, помимо любого рассмотрения дженериков. Например, я написал решатель судоку, которому нужен счетчик всплывающих окон. Чтобы ускорить это, я предварительно вычислил количество всплывающих окон для различных возможных значений и сохранил их как источник Go . Это то, что можно сделать с go:generate . Но если бы существовала функция времени компиляции, эта таблица поиска могла бы точно так же вычисляться во время компиляции, что избавляло бы сгенерированный машиной код от необходимости фиксации в репозитории. В общем, любая запоминаемая математическая функция хорошо подходит для предварительно созданных интерполяционных таблиц с функциями времени компиляции.

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

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

Что касается второй части, я не вижу способа сделать что-то вроде «нарезки функций, которые обрабатывают слайсы определенного типа и возвращают один элемент» или в специальном синтаксисе []func<T>([]T) T , который это очень легко сделать практически на любом функциональном языке со статической типизацией. Что действительно необходимо, так это значения , способные принимать параметрические типы, а не некоторая генерация кода на уровне исходного кода.

@бунсим

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

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

const func SliceOfSelectors(T gotype) gotype { return []func([]T)T (type) }

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

Да, но, на мой взгляд, такие вещи, которые требуют упаковки, должны быть разрешены при сохранении безопасности типов, возможно, со специальным синтаксисом, указывающим на «упаковку». Большая часть добавления «дженериков» на самом деле состоит в том, чтобы избежать ненадежности типов interface{} , даже если накладных расходов interface{} избежать невозможно. (Возможно, разрешать только определенные конструкции параметрического типа с типами указателя и интерфейса, которые «уже» упакованы? Коробочные объекты Java Integer и т. д. не совсем плохая идея, хотя срезы типов значений сложны)

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

@bcmills
То, что вы предлагаете, не будет модульным. Если модуль A использует модуль B, который использует модуль C, который использует модуль D, изменение того, как параметр типа используется в D, может потребовать распространения обратно в A, даже если разработчик A не знает, что D находится в системе. Слабая связь, обеспечиваемая модульными системами, будет ослаблена, а программное обеспечение станет более хрупким. Это одна из проблем с шаблонами C++.

С другой стороны, если сигнатуры типов охватывают требования к параметрам типа, как в таких языках, как CLU, ML, Haskell или Genus, модуль может быть скомпилирован без какого-либо доступа к внутренностям модулей, от которых он зависит.

@бунсим

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

"не избежать" относительно. Обратите внимание, что накладные расходы на бокс — это пункт №3 в посте Расса от 2009 года (https://research.swtch.com/generic).

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

Хорошая «теория звуковых типов» носит описательный, а не предписывающий характер. Мое предложение, в частности, основано на лямбда-исчислении второго порядка (в соответствии с системой F), где gotype означает вид type , а вся система типов первого порядка поднимается во второй -order («время компиляции»).

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

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

@andrewcmyers

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

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

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

Я слышал, что тот же аргумент используется для поддержки экспорта типов interface , а не конкретных типов в API Go, и обратное оказывается более распространенным: преждевременная абстракция чрезмерно ограничивает типы и препятствует расширению API. (Один такой пример см. в #19584.) Если вы хотите опираться на эту аргументацию, я думаю, вам нужно привести несколько конкретных примеров.

Это одна из проблем с шаблонами C++.

На мой взгляд, основные проблемы с шаблонами C++ (в произвольном порядке):

  • Чрезмерная синтаксическая двусмысленность.
    а. Неоднозначность между именами типов и именами значений.
    б. Чрезмерно широкая поддержка перегрузки операторов, что приводит к ослаблению способности выводить ограничения из использования оператора.
  • Чрезмерная зависимость от разрешения перегрузок для метапрограммирования (или, что то же самое, специальная эволюция поддержки метапрограммирования).
    а. Особенно в отношении правил сворачивания ссылок.
  • Чрезмерно широкое применение принципа SFINAE, приводящее к очень сложным для распространения ограничениям и слишком большому количеству неявных условий в определениях типов, что приводит к очень сложным отчетам об ошибках.
  • Чрезмерное использование вставки токенов и включения текста (препроцессор C) вместо подстановки AST и артефактов компиляции более высокого порядка (которые, к счастью, по крайней мере частично решаются с помощью модулей).
  • Отсутствие хороших языков начальной загрузки для компиляторов C++, что приводит к плохим сообщениям об ошибках в долгоживущих линиях компиляторов (например, инструментальной цепочке GCC).
  • Удвоение (а иногда и умножение) имен в результате отображения наборов операторов на «понятия» с разными именами (вместо того, чтобы рассматривать сами операторы как фундаментальные ограничения).

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

С другой стороны, нужно обновить цепочку зависимостей O(N) только для того, чтобы добавить один метод к типу в модуле A и иметь возможность использовать его в модуле D? Это та проблема, которая замедляет меня на регулярной основе. Там, где конфликтуют параметричность и слабая связь, я в любой момент выберу параметричность.

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

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

func <T io.Reader> ReadAll(in T)

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

Лучшим примером может быть пакет sort , где вы могли бы иметь что-то вроде

func <T Comparable> Sort(slice []T)

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

@bcmills Транзитивные зависимости, не ограниченные системой типов, на мой взгляд, лежат в основе некоторых ваших жалоб на C++. Транзитивные зависимости не представляют такой большой проблемы, если вы управляете модулями A, B, C и D. В общем, вы разрабатываете модуль A и можете лишь слабо осознавать, что модуль D находится внизу, и, наоборот, разработчик D может не знать об A. Если модуль D сейчас, без внесения каких-либо изменений в объявления, видимые в D, начнет использовать какой-либо новый оператор для параметра типа или просто использует этот параметр типа в качестве аргумента типа для нового модуля E со своим собственным неявные ограничения — эти ограничения будут распространяться на всех клиентов, которые могут не использовать аргументы типа, удовлетворяющие ограничениям. Ничто не говорит разработчику D, что они проваливаются. По сути, у вас есть что-то вроде глобального вывода типов со всеми вытекающими отсюда трудностями отладки.

Я считаю, что подход, который мы использовали в Genus [ PLDI'15 ], намного лучше. Параметры типа имеют явные, но легкие ограничения (я принимаю ваше замечание о поддержке ограничений операций; CLU показал, как сделать это правильно, еще в 1977 году). Проверка типов рода является полностью модульной. Общий код может быть либо скомпилирован только один раз для оптимизации пространства кода, либо специализирован для аргументов определенного типа для повышения производительности.

@andrewcmyers

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

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

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

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

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

Или функция, проверяющая определенный код ошибки, может прерваться, если функция, вызвавшая ошибку, изменит способ сообщения об этом условии. (Например, см. https://github.com/golang/go/issues/19647.)

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

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

...и так далее.

В этом отношении Go не является чем-то необычным: неявные ограничения широко распространены в реальных программах.


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

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

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


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

@bcmills Разве ты не делаешь лучшее врагом хорошего? Да, существуют неявные ограничения, которые трудно зафиксировать в системе типов. (Кроме того, хороший модульный дизайн позволяет избежать введения таких неявных ограничений, когда это возможно.) Было бы здорово иметь всеобъемлющий анализ, который мог бы статически проверять все свойства, которые вы хотите проверить, и давать четкие, не вводящие в заблуждение объяснения программистам о том, где они находятся. совершают ошибки. Даже с недавним прогрессом в области автоматической диагностики и локализации ошибок я не затаил дыхание. Во-первых, инструменты анализа могут анализировать только тот код, который вы им даете. Разработчики не всегда имеют доступ ко всему коду, который может быть связан с их собственным.

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

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

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

@andrewcmyers

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

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

Но я бы пошел дальше в этом аргументе: верно и обратное! Если вам _не_ нужно писать комментарий к ограничению (поскольку это очевидно в контексте для людей, работающих с кодом), зачем вам писать этот комментарий для компилятора? Независимо от моих личных предпочтений, использование в Go сборки мусора и нулевых значений явно указывает на предвзятость в сторону «не требовать от программистов указания очевидных инвариантов». Может случиться так, что моделирование в стиле Genus может выразить многие ограничения, которые были бы выражены в комментариях, но как оно работает с точки зрения устранения ограничений, которые также были бы исключены в комментариях?

Мне кажется, что модели в стиле Genus в любом случае больше, чем просто комментарии: в некоторых случаях они действительно меняют семантику кода, а не просто ограничивают его. Теперь у нас будет два разных механизма — интерфейсы и модели типов — для параметризации поведения. Это означало бы серьезный сдвиг в языке Go: со временем мы обнаружили несколько лучших практик для интерфейсов (например, «определение интерфейсов на стороне потребителя»), и не очевидно, что этот опыт можно было бы применить к такой радикально отличающейся системе, даже пренебрегая совместимостью с Go 1.

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

Вот интересная точка данных для «метапрограммирования». Было бы неплохо, чтобы определенные типы в пакетах sync и atomic — а именно, atomic.Value и sync.Map — поддерживали методы CompareAndSwap , но они работают только для типов, которые оказались сопоставимыми. Остальные API-интерфейсы atomic.Value и sync.Map остаются полезными и без этих методов, поэтому для этого варианта использования нам нужно либо что-то вроде SFINAE (или другие виды условно определенных API), либо вернуться к более сложной иерархии типов.

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

@bcmills Можете ли вы подробнее рассказать об этих трех пунктах?

  1. Неоднозначность между именами типов и именами значений.
  2. Чрезмерно широкая поддержка перегрузки операторов
    3. Чрезмерное использование разрешения перегрузок для метапрограммирования

@махдикс Конечно.

  1. Неоднозначность между именами типов и именами значений.

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

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

const a = someValue
x := T{a: b}

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

  1. Чрезмерно широкая поддержка перегрузки операторов

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

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

  1. Чрезмерная зависимость от разрешения перегрузки для метапрограммирования

Библиотека <type_traits> — хорошее место для начала. Ознакомьтесь с реализацией в вашем дружественном соседстве libc++ , чтобы увидеть, как разрешение перегрузки вступает в игру.

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

@bcmills
Поскольку я никогда не использовал C++, не могли бы вы пролить свет на то, где перегрузка операторов с помощью реализации предопределенных «интерфейсов» стоит с точки зрения сложности. Python и Kotlin тому пример.

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

Это предложение основано на шаблонах, системы для расширения макросов будет недостаточно? Я не говорю о go generate или таких проектах, как gotemplate. Я говорю больше о таком:

macro MacroFoo(stmt ast.Statement) {
    ....
}

Макрос может уменьшить шаблон и использование отражения.

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

@samadadi , вы можете донести свою точку зрения, не говоря «что с вами не так, люди». Сказав это, аргумент сложности поднимался уже несколько раз.

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

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

Как отмечено на https://blog.golang.org/toward-go2 , нам необходимо предоставить «отчеты об опыте», чтобы можно было определить потребности и цели дизайна. Не могли бы вы уделить несколько минут и задокументировать макрослучаи, которые вы наблюдали?

Пожалуйста, держите эту ошибку в теме и гражданском. И снова https://golang.org/wiki/NoMeToo. Пожалуйста, комментируйте, только если у вас есть уникальная и конструктивная информация для добавления.

@mandolyte В Интернете очень легко найти подробные объяснения, пропагандирующие генерацию кода в качестве (частичной) замены дженериков:
https://appliedgo.net/generics/
https://www.calhoun.io/using-code-generation-to-survive-without-generics-in-go/
http://blog.ralch.com/tutorial/golang-code-generation-and-generics/

Ясно, что есть много людей, использующих этот подход.

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

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

Минус — нет прецедента (насколько я знаю) такого подхода внутри цепочки инструментов go.

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

ИМХО: это легкодостижимый компромисс с низкой ценой.

Чтобы было ясно, я не считаю генерацию кода в стиле макросов, независимо от того, выполняется ли она с помощью gen, cpp, gofmt -r или других инструментов макросов/шаблонов, хорошим решением проблемы дженериков, даже если они стандартизированы. У него те же проблемы, что и у шаблонов C++: раздувание кода, отсутствие модульной проверки типов и сложность отладки. Ситуация усугубляется, когда вы начинаете, как это и естественно, строить универсальный код в терминах другого универсального кода. На мой взгляд, преимущества ограничены: это сделало бы жизнь разработчиков компилятора Go относительно простой, и он действительно производит эффективный код — если только нет давления на кэш инструкций, частая ситуация в современном программном обеспечении!

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

В среду, 26 июля 2017 г., 22:41 Эндрю Майерс, уведомления@github.com написал:

Чтобы было ясно, я не рассматриваю генерацию кода в стиле макросов, независимо от того,
с gen, cpp, gofmt -r или другими инструментами макросов/шаблонов, чтобы быть хорошим
решение проблемы дженериков, даже если оно стандартизировано. Он имеет то же самое
проблемы с шаблонами C++: раздувание кода, отсутствие модульной проверки типов и
сложность отладки. Становится хуже, когда вы начинаете, как это и естественно, строить
универсальный код в терминах другого универсального кода. На мой взгляд плюсы
ограничено: это сделало бы жизнь разработчиков компилятора Go относительно простой.
и он действительно производит эффективный код — если нет кеша инструкций
давление, частая ситуация в современном софте!


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

Без сомнения, генерация кода не является НАСТОЯЩИМ решением, даже если оно обернуто некоторой языковой поддержкой, чтобы сделать внешний вид «частью языка».

Моя точка зрения заключалась в том, что это было ОЧЕНЬ рентабельно.

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

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

Прочитав предложения @bcmills и @ianlancetaylor по дженерикам, я сделал следующие выводы:

Функции времени компиляции и типы первого класса

Мне нравится идея оценки во время компиляции, но я не вижу смысла ограничивать ее чистыми функциями. В этом предложении представлен встроенный gotype , но его использование ограничено константными функциями и любыми типами данных, определенными в области действия функции. С точки зрения пользователя библиотеки создание экземпляров ограничено функциями конструктора, такими как "New", и приводит к сигнатурам функций, подобным этой:

const func New(K, V gotype, hashfn Hashfn(K), eqfn Eqfn(K)) func()*Hashmap(K, V, hashfn, eqfn)

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

Введите параметры в Go

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

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

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

Однако если бы к идентификаторам было привязано больше ограничений, разве это не помогло бы компилятору? Учитывать:

HashMap[Anything,Anything] // Compiler must always compare the implementation and usages to make sure this is valid.

против

HashMap[Comparable,Anything] // Compiler can first filter out instantiations for incomparable types before running an exhaustive check.

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

@smasher164

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

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

@smasher164

Чего я не понимаю, так это почему бы не разрешить пользователю указывать свои собственные ограничения типа?

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

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

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

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

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

Это может быть так просто и легко указать необходимые операции. См. пример CLU (1977).

@andrewcmyers

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

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

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

а сообщения об ошибках многословны и непонятны.

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

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

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

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

Список компиляторов, в которых вывод нелокального типа, который вы предлагаете, приводит к плохим сообщениям об ошибках, немного длиннее. Он включает в себя SML, OCaml и GHC, где уже было приложено много усилий для улучшения их сообщений об ошибках и где есть хотя бы какая-то явная модульная структура. Возможно, у вас получится лучше, и если вы придумаете алгоритм для хороших сообщений об ошибках с предложенной вами схемой, у вас будет хорошая публикация. В качестве отправной точки для этого алгоритма вам могут пригодиться наши статьи POPL 2014 и PLDI 2015 по локализации ошибок. Они более или менее современные.

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

Разве это не ограничивает широту универсальных программ с возможностью проверки типов? Например, обратите внимание, что в предложении type-params не указано ограничение «Итерируемый». В текущем языке это соответствовало бы либо срезу, либо каналу, но составной тип (скажем, связанный список) не обязательно удовлетворял бы этим требованиям. Определение интерфейса, подобного

type Iterable[T] interface {
    Next() T
}

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

Ограничение, в котором говорится: «Я принимаю набор всех типов, которые являются объектами Iterable, фрагментами или каналами», кажется беспроигрышной ситуацией для пользователя, автора пакета и разработчика компилятора. Я пытаюсь подчеркнуть, что ограничения — это надмножество синтаксически допустимых программ, и некоторые из них могут не иметь смысла с точки зрения языка, а только с точки зрения API.

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

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

Что, если вместо

type Iterable[T] interface {
    Next() T
}

мы отделили идею «интерфейсов» от «ограничений». Тогда у нас может быть

type T generic

type Iterable class {
    Next() T
}

где "класс" означает класс типов в стиле Haskell, а не класс в стиле Java.

Отделение «классов типов» от «интерфейсов» может помочь прояснить часть неортогональности этих двух идей. Тогда Sortable (игнорируя sort.Interface) может выглядеть примерно так:

type T generic

type Comparable class {
    Less(a, b T) bool
}

type Sortable class {
    Next() Comparable
}

Вот некоторые отзывы о разделе «Классы и концепции типов» в Genus от @andrewcmyers и их применимости к Go.

В этом разделе рассматриваются ограничения классов и концепций типов, заявляя

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

Я не уверен, что понимаю это ограничение. Разве привязка ограничения к отдельным идентификаторам не помешает ему быть уникальным для данного типа? Мне кажется, что предложение «где» в Genus по существу создает тип/ограничение из заданного ограничения, но это похоже на создание экземпляра переменной из заданного типа. Ограничение в этом смысле напоминает своего рода .

Вот кардинальное упрощение определений ограничений, адаптированное к Go:

kind Any interface{} // accepts any type that satisfies interface{}.
type T Any // Declare a type of Any kind. Also binds it to an identifier.
kind Eq T == T // accepts any type for which equality is defined.

Таким образом, объявление карты будет выглядеть так:

type Map[K Eq, V Any] struct {
}

где в Роде это может выглядеть так:

type Map[K, V] where Eq[K], Any[V] struct {
}

и в существующем предложении Type-Params это будет выглядеть так:

type Map[K,V] struct {
}

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

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

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

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

type handleFunc func(http.ResponseWriter, *http.Request)
func (f handlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) { f(w,r) }

По сути, это то, что делает стандартная библиотека .

@smasher164

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

Идея состоит в том, что в Genus вы можете удовлетворить одно и то же ограничение с одним и тем же типом более чем одним способом, в отличие от Haskell. Например, если у вас есть HashSet[T] , вы можете написать HashSet[String] для хеширования строк обычным способом, а HashSet[String with CaseInsens] для хеширования и сравнения строк с CaseInsens модель, которая предположительно обрабатывает строки без учета регистра. Род фактически различает эти два типа; это может быть излишним для Go. Даже если система типов не отслеживает это, по-прежнему важно иметь возможность переопределять операции по умолчанию, предоставляемые типом.

kind Any interface{} // принимает любой тип, который удовлетворяет interface{}.
type T Any // Объявить тип Any. Также привязывает его к идентификатору.
kind Eq T == T // принимает любой тип, для которого определено равенство.
type Map[K Eq, V Any] struct { ...
}

Моральным эквивалентом этого в роде будет:

constraint Any[T] {}
// Just use Any as if it were a type
constraint Eq[K] {
   boolean equals(K);
}
class Map[K, V] where Eq[K] { ... }

В Familia мы бы просто написали:

interface Eq {
    boolean equals(This);
}
class Map[K where Eq, V] { ... }

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

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

Сегодня в коде Go распространены две вещи.

  • обертывание значения интерфейса для предоставления дополнительной функциональности (обертка http.ResponseWriter для фреймворка)
  • наличие необязательных методов, которые иногда имеют значения интерфейса (например, Temporary() bool на net.Error )

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

type MyError struct {
  error
  extraContext extraContextType
}
func (m MyError) Error() string {
  return fmt.Sprintf("%s: %s", m.extraContext, m.error)
}

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

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

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

type MyError generic_over[E which_is_a_type_satisfying error] struct {
  E
  extraContext extraContextType
}
func (m MyError) Error() string {
  return fmt.Sprintf("%s: %s", m.extraContext, m.E)
}

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

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

Это сразу вызывает два опасения

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

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

Без этого вы просто создаете экземпляр MyError для самого типа ошибки, а не для динамического типа интерфейса.

Допустим, у нас есть волшебная функция unbox для извлечения и (каким-то образом) применения информации:

func wrap(ec extraContext, err error) error {
  if err == nil {
    return nil
  }
  return MyError{
    E: unbox(err),
    extraContext: ec,
  }
}

Теперь предположим, что у нас есть ненулевая ошибка, err , динамический тип которой *net.DNSError . Тогда это

wrapped := wrap(getExtraContext(), err)
//wrapped 's dynamic type is a MyStruct embedding E=*net.DNSError
_, ok := wrapped.(net.Error)
fmt.Println(ok)

напечатает true . Но если бы динамический тип err был бы *os.PathError , он напечатал бы ложь.

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

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

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

@andrewcmyers нет.

Структура в Go допускает встраивание. Если вы добавите в структуру поле без имени, но с типом, оно сделает две вещи: создаст поле с тем же именем, что и у типа, и позволит прозрачно отправлять любые методы этого типа. Звучит ужасно похоже на наследование, но это не так. Если бы у вас был тип T, у которого был метод Foo(), то следующее эквивалентно

type S struct {
  T
}

и

type S struct {
  T T
}
func (s S) Foo() {
  s.T.Foo()
}

(когда Foo вызывается, его «это» всегда имеет тип T).

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

В Go есть интерфейсы, которые определяют контракт в терминах методов типа. Значение любого типа, удовлетворяющего условиям контракта, может быть заключено в значение этого интерфейса. Значение интерфейса — это указатель на манифест внутреннего типа (динамический тип) и указатель на значение этого динамического типа (динамическое значение). Вы можете выполнять утверждения типа для значения интерфейса, чтобы (а) получить динамическое значение, если вы утверждаете его неинтерфейсный тип, или (б) получить новое значение интерфейса, если вы утверждаете для другого интерфейса, что динамическое значение также удовлетворяет. Обычно последний используется для «тестирования функций» объекта, чтобы узнать, поддерживает ли он необязательные методы. Чтобы повторно использовать более ранний пример, некоторые ошибки имеют метод «Temporary() bool», чтобы вы могли видеть, является ли какая-либо ошибка временной, с помощью:

func isTemp(err error) bool {
  if t, ok := err.(interface{ Temporary() bool}); ok {
    return t.Temporary()
  }
  return false
}

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

type A struct {}
func (A) Foo()
func (A) Bar()

type I interface {
  Foo()
}

type B struct {
  I
}

var i I = B{A{}}

Вы не можете вызвать Bar для i или даже знать, что он существует, если вы не знаете, что динамический тип i — это B, поэтому вы можете развернуть его и получить в поле I тип assert для этого .

Это вызывает настоящие проблемы, особенно при работе с распространенными интерфейсами, такими как error или Reader.

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

@jimmyfrasche Действительно. Что Genus позволяет вам сделать, так это использовать один тип для удовлетворения контракта «интерфейса», не упаковывая его. Значение по-прежнему имеет исходный тип и исходные операции. Кроме того, программа может указать, какие операции тип должен использовать для выполнения контракта — по умолчанию это операции, предоставляемые типом, но программа может предоставить новые, если тип не имеет необходимых операций. Он также может заменить операции, которые будет использовать тип.

@jimmyfrasche @andrewcmyers Для этого варианта использования см. Также https://github.com/golang/go/issues/4146#issuecomment -318200547.

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

type MyError generic_over[E which_is_a_type_satisfying error] struct {
  e E
  extraContext extraContextType
}
func (m MyError) Error() string {
  return fmt.Sprintf("%s: %s", m.extraContext, m.e)
}

Значение, присвоенное e , должно иметь динамический (или конкретный) тип, например *net.DNSError , который реализует error . Вот несколько возможных способов решения этой проблемы при изменении языка в будущем:

  1. Создайте волшебную unbox -подобную функцию, которая раскрывает динамическое значение переменной. Это относится к любому типу, который не является конкретным, например к объединениям.
  2. Если изменение языка поддерживает переменные типа, предоставьте средства для получения динамического типа переменной. Имея информацию о типе, мы можем сами написать функцию unbox . Например,
func unbox(v T1) T2 {
    t := dynTypeOf(v)
    return v.(t)
}

wrap можно записать так же, как и раньше, или как

func wrap(ec extraContext, err error) error {
  if err == nil {
    return nil
  }
  t := dynTypeOf(err)
  return MyError{
    e: v.(t),
    extraContext: ec,
  }
}
  1. Если изменение языка поддерживает ограничения типов, вот альтернативная идея:
type E1 which_is_a_type_satisfying error
type E2 which_is_a_type_satisfying error

func wrap(ec extraContext, err E1) E2 {
  if err == nil {
    return nil
  }
  return MyError{
    e: err,
    extraContext: ec,
  }
}

В этом примере мы принимаем значение любого типа, реализующего ошибку. Любой пользователь wrap , ожидающий error , получит его. Однако тип e внутри MyError совпадает с типом передаваемого err , который не ограничивается типом интерфейса. Если бы кто-то хотел того же поведения, что и 2,

var iface error = ...
wrap(getExtraContext(), unbox(iface))

Поскольку, похоже, никто другой этого не сделал, я хотел бы указать на очень очевидные «отчеты об опыте» для дженериков, как того требует https://blog.golang.org/toward-go2.

Первый — это встроенный тип map :

m := make(map[string]string)

Следующим является встроенный тип chan :

c := make(chan bool)

Наконец, стандартная библиотека изобилует альтернативами interface{} , в которых дженерики работали бы более безопасно:

  • heap.Interface (https://golang.org/pkg/container/heap/#Interface)
  • list.Element (https://golang.org/pkg/container/list/#Element)
  • ring.Ring (https://golang.org/pkg/container/ring/#Ring)
  • sync.Pool (https://golang.org/pkg/sync/#Pool)
  • предстоящие sync.Map (https://tip.golang.org/pkg/sync/#Map)
  • atomic.Value (https://golang.org/pkg/sync/atomic/#Value)

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

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

http://www.yinwang.org/blog-cn/2014/04/18/голанг
Я думаю, что универсальный интерфейс важен. В противном случае он не может обрабатывать аналогичные типы. Иногда интерфейс не может решить проблему.

Простой синтаксис и система типов — важные плюсы Go. Если вы добавите дженерики, язык превратится в уродливую кашу, как Scala или Haskell. Также эта функция привлечет псевдоакадемических фанатов, которые в конечном итоге трансформируют ценности сообщества с «Давайте сделаем это» на «Давайте поговорим о теории CS и математике». Избегайте дженериков, это путь в пропасть.

@bxqgit, пожалуйста, соблюдайте вежливость. Не надо никого оскорблять.

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

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

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

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

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

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

Лично я не вижу много теории CS и математики в разработке языка Go. Это довольно примитивный язык, что, на мой взгляд, хорошо. Также те люди, о которых вы говорите, решили избегать дженериков и добились своего. Если все работает нормально, зачем что-то менять? Вообще, я считаю, что постоянное развитие и расширение синтаксиса языка — плохая практика. Это только добавляет сложности, что приводит к хаосу Haskell и Scala.

Шаблон сложен, но дженерики просты

Посмотрите на функции SortInts, SortFloats, SortStrings в пакете sort. Или SearchInts, SearchFloats, SearchStrings. Или методы Len, Less и Swap для byName в пакете io/ioutil. Чистое шаблонное копирование.

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

Я голосую не за обобщенные универсальные приложения, а за более встроенные универсальные функции, такие как append и copy , которые работают с несколькими базовыми типами. Возможно, для типов коллекций можно добавить sort и search ?

Для моих приложений отсутствует единственный тип — это неупорядоченный набор (https://github.com/golang/go/issues/7088), я бы хотел, чтобы это был встроенный тип, чтобы он получал общий тип, например slice и map . Поместите работу в компилятор (сравнительный анализ для каждого базового типа и выбранного набора типов struct с последующей настройкой для достижения наилучшей производительности) и исключите дополнительные аннотации из кода приложения.

smap встроенный вместо sync.Map тоже пожалуйста. Из моего опыта использование interface{} для обеспечения безопасности типов во время выполнения является недостатком дизайна. Проверка типов во время компиляции — главная причина вообще использовать Go.

@pciet

Из моего опыта использование interface{} для обеспечения безопасности типов во время выполнения является недостатком дизайна.

Можете ли вы просто написать небольшую (безопасную для типов) оболочку?
https://play.golang.org/p/tG6hd-j5yx

@pierrre Эта оболочка лучше, чем чек на reflect.TypeOf(item).AssignableTo(type) . Но написать свой собственный тип с помощью map + sync.Mutex или sync.RWMutex — такая же сложность без утверждения типа, которое требуется для sync.Map .

Мое использование синхронизированной карты было для глобальных карт мьютексов с var myMapLock = sync.RWMutex{} рядом с ним вместо создания типа. Это может быть чище. Общий встроенный тип кажется мне правильным, но требует работы, которую я не могу выполнить, и я предпочитаю свой подход утверждению типа.

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

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

@andrewcmyers «Обобщения для Go могли бы быть намного проще и менее подвержены ошибкам». --- как дженерики в C#.

Разочаровывает то, что Go становится все более и более сложным, добавляя встроенные параметризованные типы.

Несмотря на спекуляции в этом вопросе, я думаю, что это крайне маловероятно.

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

Механическая реализация дженериков типа «копипастер» с помощью компилятора.
решит 99% проблемы способом, верным основе Go
принципы поверхностности и отсутствия неожиданностей.

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

28 ноября 2017 г., 23:54, «Эндрю Майерс» [email protected] написал:

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

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


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

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

@ianlancetaylor Rust позволяет программе реализовать трейт T в существующем типе Q при условии, что их крейт определяет T или Q .

Просто мысль: интересно, смогут ли помочь Саймон Пейтон Джонс (да, известный Haskell) и/или разработчики Rust. Rust и Haskell, вероятно, имеют две самые продвинутые системы типов из всех рабочих языков, и Go должен учиться у них.

Есть также Филипп Уодлер , который работал над Generic Java , что в конечном итоге привело к реализации дженериков, которая есть в Java сегодня.

@tarcieri Я не думаю, что дженерики Java очень хороши, но они проверены в бою.

@DemiMarie , к счастью, здесь участвовал Эндрю Майерс.

Основываясь на своем личном опыте, я думаю, что люди, хорошо знающие разные языки и разные системы типов, могут очень помочь в изучении идей. Но для создания идей в первую очередь нам нужны люди, хорошо знакомые с Go, с тем, как он работает сегодня и как он может работать в будущем. Go разработан, чтобы быть, среди прочего, простым языком. Импорт идей из таких языков, как Haskell или Rust, которые значительно сложнее Go, вряд ли подойдет. И в целом идеи людей, которые еще не написали достаточного количества кода на Go, вряд ли подойдут; не то чтобы идеи были плохими как таковые, просто они не будут хорошо сочетаться с остальной частью языка.

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

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

  1. Интерфейсы требуют, чтобы все операции были выражены в виде методов. Это затрудняет написание интерфейса для встроенных типов, таких как типы каналов. Все типы каналов поддерживают оператор <- для операций отправки и получения, и достаточно просто написать интерфейс с методами Send и Receive , но для присвоения значения канала для этого типа интерфейса вы должны написать стандартные методы Send и Receive . Эти шаблонные методы будут выглядеть одинаково для каждого типа канала, что утомительно.

  2. Интерфейсы имеют динамический тип, поэтому ошибки, связанные с объединением различных статически типизированных значений, обнаруживаются только во время выполнения, а не во время компиляции. Например, функция Merge , которая объединяет два канала в один, используя их методы Send и Receive , потребует, чтобы два канала имели элементы одного типа, но это проверка может быть выполнена только во время выполнения.

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

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

http://www.cs.cornell.edu/andru/papers/familia/ (Чжан и Майерс, OOPSLA'17)
http://io.livecode.ch/learn/namin/unsound (Амин и Тейт, OOPSLA'16)
http://www.cs.cornell.edu/projects/genus/ (Чжан и др., PLDI '15)
https://www.cs.cornell.edu/~ross/publications/shapes/shapes-pldi14.pdf (Greenman, Muehlboeck & Tate, PLDI '14)

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

type Item interface {
    Equal(Item) bool
}

type Set interface {
    Add(Item) Set
    Remove(Item) Set
    Combine(...Set) Set
    Reduce() Set
    Has(Item) bool
    Equal(Set) bool
    Diff(Set) Set
}

Проверка удаления элемента:

type RemoveCase struct {
    Set
    Item
    Out Set
}

func TestRemove(t *testing.T) {
    for i, c := range RemoveCases {
        if c.Out.Equal(c.Set.Remove(c.Item)) == false {
            t.Fatalf("%v failed", i)
        }
    }
}

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

var RemoveCases = []RemoveCase{
    {
        Set: MapPathSet{
            &Path{{0, 0}}:         {},
            &Path{{0, 1}, {1, 1}}: {},
        },
        Item: Path{{0, 0}},
        Out: MapPathSet{
            &Path{{0, 1}, {1, 1}}: {},
        },
    },
    {
        Set: SlicePathSet{
            {{0, 0}},
            {{0, 1}, {1, 1}},
        },
        Item: Path{{0, 0}},
        Out: SlicePathSet{
            {{0, 1}, {1, 1}},
        },
    },
}

Для каждого конкретного типа мне пришлось определить методы интерфейса. Например:

func (the MapPathSet) Remove(an Item) Set {
    return MapDelete(the, an.(Path))
}
func (the SlicePathSet) Remove(an Item) Set {
    return SliceDelete(the, an.(Path))
}

Эти общие тесты могут использовать предложенную проверку типа во время компиляции:

type Item generic {
    Equal(Item) bool
}
func (the SlicePathSet) Remove(an Item) Set {
    return SliceDelete(the, an)
}

Источник: https://github.com/pciet/pathsetbenchmark

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

Так как насчет «универсального» типа, который является интерфейсом и имеет невидимое утверждение типа, добавляемое компилятором при конкретном использовании?

@andrewcmyers Статья "Familia" была интересной (и выше моего понимания). Ключевым понятием было наследование. Как изменились бы концепции для такого языка, как Go, который полагается на композицию вместо наследования?

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

@andrewcmyers Можете ли вы привести пример того, как это будет выглядеть в Go?

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

Насколько я понимаю, интерфейс Go определяет ограничение на тип (например, «этот тип можно сравнивать на предмет равенства, используя интерфейс Comparable type, потому что он удовлетворяет требованиям метода Eq»). Я не уверен, что понимаю, что вы подразумеваете под ограничением типа.

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

Было бы интересно провести конкретное сравнение между Familia и Go. Спасибо, что поделились своим документом.

Интерфейсы Go можно рассматривать как описание ограничений на типы посредством структурного подтипа. Однако это ограничение типа как таковое недостаточно выразительно, чтобы зафиксировать ограничения, необходимые для обобщенного программирования. Например, вы не можете выразить ограничение типа с именем Eq в документе Familia.

Некоторые мысли о мотивации более общих средств программирования в Go:

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

Но sync.Map — это другой вариант использования. В стандартной библиотеке существует потребность в зрелой универсальной реализации синхронизированной карты, помимо простой структуры с картой и мьютексом. Для обработки типов мы можем обернуть его другим типом, который устанавливает тип, не являющийся интерфейсом {}, и выполняет утверждение типа, или мы можем добавить внутреннюю проверку отражения, чтобы элементы, следующие за первым, соответствовали одному и тому же типу. У обоих есть проверки во время выполнения, оболочка требует переписывания каждого метода для каждого типа использования, но добавляет проверку типа во время компиляции для ввода и скрывает утверждение типа вывода, а с внутренней проверкой мы все равно должны делать утверждение типа вывода. В любом случае мы делаем преобразования интерфейсов без фактического использования интерфейсов; interface{} является хаком языка и не будет понятен начинающим программистам Go. Хотя json.Marshal, на мой взгляд, хороший дизайн (включая уродливые, но разумные теги структуры).

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

Обычная карта имеет только проверку типа во время компиляции и не требует каких-либо этих каркасов. Я утверждаю, что sync.Map должен быть таким же или не должен быть в стандартной библиотеке для Go 2.

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

Если вы добавите sync.Map как встроенный, то как далеко вы продвинетесь? У вас есть особый случай для каждого контейнера?
sync.Map — не единственный контейнер, и некоторые из них лучше подходят для одних случаев, чем для других.

@Azareal : @chowey перечислил их в августе:

Наконец, стандартная библиотека изобилует альтернативами interface{}, в которых дженерики работали бы более безопасно:

• heap.Interface (https://golang.org/pkg/container/heap/#Interface)
• list.Element (https://golang.org/pkg/container/list/#Element)
• кольцо.Кольцо (https://golang.org/pkg/container/ring/#Ring)
• sync.Pool (https://golang.org/pkg/sync/#Pool)
• предстоящая sync.Map (https://tip.golang.org/pkg/sync/#Map)
• atomic.Value (https://golang.org/pkg/sync/atomic/#Value)

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

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

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

Мне интересно, есть ли разумные альтернативные реализации с Go 1, которые достигают той же цели для этих стандартных типов библиотек без интерфейса {} и без дженериков.

интерфейсы golang и классы типа haskell преодолевают две вещи (и это очень здорово!):

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

Но,

1.) Иногда вам нужны только анонимные группы, такие как группа int, float64 и string. Как назвать такой интерфейс NumericandString?

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

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

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

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

Вот библиотека, которую я начал сегодня для общих неупорядоченных типов наборов: https://github.com/pciet/unordered

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

Какие потребности существуют в дженериках? Мое негативное отношение к универсальным типам стандартной библиотеки ранее было связано с использованием interface{}; моя жалоба может быть решена с помощью специфичного для пакета типа для интерфейса {} (например, type Item interface{} в pciet/unordered), который документирует предполагаемые невыразимые ограничения.

Я не вижу необходимости в добавлении языковой функции, когда сейчас нам может помочь только документация. В стандартной библиотеке уже есть большое количество проверенного в боевых условиях кода, который предоставляет общие возможности (см. https://github.com/golang/go/issues/23077).

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

Проверки @zerkms во время выполнения можно отключить, установив asserting = false (это не вошло бы в стандартную библиотеку), существует шаблон использования для проверок во время компиляции, и в любом случае проверка типа просто смотрит на структуру интерфейса (используя интерфейс добавляет больше расходов, чем проверка типа). Если интерфейс не работает, вам придется написать свой собственный тип.

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

проверки во время выполнения можно отключить, установив asserting = false

тогда ничто не гарантирует правильность

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

Я этого не говорил. Безопасность типов была бы очень кстати. Ваше решение по-прежнему заражено interface{} .

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

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

@pciet

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

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

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

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

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

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

Я говорю, что существующие функции приводят нас к хорошей сбалансированной точке, которая имеет общие программные решения, и должны быть веские реальные причины для перехода от системы типов Go 1. Не то, как изменение улучшит язык, а то, с какими проблемами люди сталкиваются сейчас, например, поддержка большого количества переключений типов во время выполнения для interface{} в пакетах стандартной библиотеки fmt и базы данных, которые будут исправлены.

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

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

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

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

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

Если бы интерфейсы Go могли фиксировать тип исполнителя, могла бы существовать форма дженериков, полностью совместимая с текущим синтаксисом Go — форма дженериков с одним параметром ( демонстрация ).

@dc0d для общих типов контейнеров. Я считаю, что эта функция добавляет проверку типов во время компиляции, не требуя типа оболочки: https://gist.github.com/pciet/36a9dcbe99f6fb71f5fc2d3c455971e5

@pciet Вы правы. В предоставленном коде № 4 в примере указано, что тип фиксируется для срезов и каналов (и массивов). Но не для карт, потому что есть только один и только один параметр типа: средство реализации. А поскольку для карты требуется параметр двух типов, необходимы интерфейсы-оболочки.

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

  • Совместимость с текущим Go
  • Простой (один общий параметр типа, который _похож_ на _this_ в другом OO, ссылаясь на текущий реализатор)

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

@adg написал:

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

Afaics, связанный раздел, приведенный ниже, не указывает на случай отсутствия универсальности с текущими интерфейсами: «Нет способа написать метод, который принимает интерфейс для типа T, предоставленного вызывающей стороной, для любого T и возвращает значение того же типа Т.”_.

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

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

Интерфейсы — это не просто типы; они также являются ценностями. Невозможно использовать типы интерфейса без использования значений интерфейса, а значения интерфейса не всегда эффективны.

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

Вот что делают границы класса типов на сайте определения функции, принимая в качестве входных данных тип T и имея предложение where или requires , указывающее интерфейс(ы), которые требуются для типа T. Во многих случаях эти интерфейсные словари могут быть автоматически мономорфизированы во время компиляции, так что никакие указатели словаря (для интерфейсов) не передаются в функцию во время выполнения (мономорфизация, которую, как я полагаю, компилятор Go применяет к интерфейсам в настоящее время?). Под «значениями» в приведенной выше цитате я предполагаю, что он имеет в виду тип ввода T, а не словарь методов для типа интерфейса, реализованного типом T.

Если мы затем разрешим параметры типа для типов данных (например, struct ), то указанный выше тип T может быть сам параметризован, поэтому у нас действительно будет тип T<U> . Фабрики для таких типов, которые должны сохранять знания о U , называются типами высшего порядка (HKT) .

Обобщения разрешают безопасные полиморфные контейнеры.

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


@tamird написал:

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

Границы трейтов в Rust по сути являются границами классов типов.

@alex написал:

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

Почему вы думаете, что они плохо подходят? Возможно, вы думаете, что трейт-объекты, которые используют диспетчеризацию во время выполнения, менее производительны, чем мономорфизм? Но их можно рассматривать отдельно от принципа универсальности границ класса типов (см. мое обсуждение гетерогенных контейнеров/коллекций ниже). Afaics, интерфейсы Go уже являются трейт-подобными границами и достигают цели классов типов, которая заключается в поздней привязке словарей к типам данных в месте вызова, а не в анти-шаблоне ООП, который связывает раннее (даже если все еще находится на этапе компиляции). time) словари к типам данных (при создании/построении). Классы типов могут (по крайней мере, частичное улучшение степеней свободы) решить проблему выражения , которую ООП не может.

@jimmyfrasche написал:

  • https://golang.org/doc/faq#covariant_types

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

Однако я также хочу указать, что иерархии наследования (также известные как подтипы) неизбежны 1 при назначении (входные данные функции) и from (выходные данные функции), если язык поддерживает объединения и пересечения, потому что, например, int ν string может принять задание от int или string , но ни один из них не может принять задание от int ν string . На самом деле, без объединений единственными альтернативными способами предоставления статически типизированных гетерогенных контейнеров/коллекций являются подклассы или экзистенциально ограниченный полиморфизм (т. е. трейт-объекты в Rust и экзистенциальная квантификация в Haskell). Ссылки выше содержат обсуждение компромиссов между экзистенциалами и союзами. Afaik, единственный способ сделать разнородные контейнеры/коллекции в Go сейчас — это отнести все типы к пустому interface{} , что отбрасывает информацию о типизации, и я предполагаю, что требуются приведения типов и проверка типов во время выполнения, какие 2 побеждает точку статической типизации.

«Анти-шаблон», которого следует избегать, — это создание подклассов, также известное как виртуальное наследование (см. также «EDIT # 2» о проблемах с неявным подчинением и равенством и т. д.).

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

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

@andrewcmyers написал:

В отличие от универсальных шаблонов Java и C#, механизм универсальных шаблонов Genus не основан на подтипах.

Именно наследование и создание подклассов ( а не структурное подтипирование ) — худший анти-шаблон, который вы не хотите копировать из Java, Scala, Ceylon и C++ (не имеющий отношения к проблемам с шаблонами C++ ).

@thwd писал:

Показатель степени сложности параметризованных типов — это дисперсия. Типы Go (кроме интерфейсов) инвариантны, и это можно и нужно сохранить за правило.

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

@bxqgit написал:

Простой синтаксис и система типов — важные плюсы Go. Если вы добавите дженерики, язык превратится в уродливую кашу, как Scala или Haskell.

Обратите внимание, что Scala пытается объединить ООП, подклассы, FP, универсальные модули, HKT и классы типов (через implicit ) в один PL. Возможно, одних классов типов может быть достаточно.

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

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

Afaics, в разделе « Семантика » _Type Parameters в Go_ проблема, с которой столкнулся @ianlancetaylor , является проблемой концептуализации, потому что он (afaics), по-видимому, невольно заново изобрел классы типов :

Можем ли мы объединить SortableSlice и PSortableSlice , чтобы получить лучшее из обоих миров? Не совсем; невозможно написать параметризованную функцию, которая поддерживает либо тип с методом Less , либо встроенный тип. Проблема в том, что SortableSlice.Less не может быть создан для типа без метода Less , и нет способа создать экземпляр метода только для одних типов, но не для других.

Предложение requires Less[T] для связанного класса типов (даже если оно неявно выводится компилятором) в методе Less для []T находится в T , а не []T . Реализация класса типов Less[T] (который содержит метод метода Less ) для каждого T будет либо предоставлять реализацию в теле функции метода, либо назначать < Встроенная функция U[T] , если для методов Sortable[U] требуется параметр типа U , представляющий тип реализации, например, []T . У Afair @keean есть еще один способ структурирования сортировки, использующий отдельный класс типов для типа значения T , для которого не требуется HKT.

Обратите внимание, что эти методы для []T могут реализовывать класс типов Sortable[U] , где U равно []T .

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

Выбор класса типов, привязанного к месту вызова, разрешается во время компиляции для статически известного T . Если вам нужна гетерогенная динамическая диспетчеризация, посмотрите варианты, которые я объяснил в своем предыдущем посте.

Я надеюсь, что @keean найдет время, чтобы прийти сюда и помочь объяснить классы типов, поскольку он более опытен и помог мне изучить эти концепции. У меня могут быть некоторые ошибки в моем объяснении.

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


Раздел « Циклы » кажется неверным. Создание во время выполнения экземпляра S[T]{e} экземпляра struct не имеет ничего общего с выбором реализации вызываемой универсальной функции. Вероятно, он думает, что компилятор не знает, специализируется ли он на реализации универсальной функции для типа аргументов, но все эти типы известны во время компиляции.

Возможно, спецификацию раздела « Проверка типов » можно упростить, изучив концепцию @keean о связном графе различных типов в качестве узлов для алгоритма унификации. Любые отдельные типы, соединенные ребром, должны иметь конгруэнтные типы, при этом ребра должны создаваться для любых типов, которые соединяются посредством присваивания или иным образом в исходном коде. Если есть объединение и пересечение (из моего предыдущего поста), то нужно учитывать направление присваивания ( каким-то образом? ). Каждый отдельный неизвестный тип начинается с наименьшей верхней границы (LUB) Top и наибольшей нижней границы (GLB) Bottom , а затем ограничения могут изменить эти границы. Связанные типы должны иметь совместимые границы. Все ограничения должны быть ограничениями класса типов.

В реализации :

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

Я считаю, что правильный технический термин — мономорфизация .

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

Профилирование подскажет программисту, какие функции больше всего выиграют от мономорфизации. Возможно, оптимизатор Java Hotspot выполняет оптимизацию мономорфизации во время выполнения?

@egonelbre написал:

Существует Summary of Go Generics Discussions , в котором делается попытка дать обзор обсуждений из разных мест.

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

В разделе «Проблемы: общие структуры данных »:

Минусы

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

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

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

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

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

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

Это не действительный минус. Правило 80/20 гласит: не добавляйте неограниченную сложность (например, преждевременную оптимизацию) к коду, который при профилировании не требует этого. Программист может оптимизировать в 20% случаев, в то время как остальные 80% обрабатываются ограниченной сложностью и когнитивной нагрузкой общих API.

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

Альтернативные решения:

  • использовать более простые структуры вместо сложных структур

    • например, используйте map[int]struct{} вместо Set

Роб Пайк (и я также видел, как он говорил об этом в видео), кажется, упускает из виду тот факт, что универсальных контейнеров недостаточно для создания универсальных функций. Нам нужен этот T в map[T] , чтобы мы могли передавать общий тип данных в функциях для ввода, вывода и для наших собственных struct . Обобщения только для параметров типа контейнера совершенно недостаточно для выражения общих API, а общие API необходимы для ограниченной сложности и когнитивной нагрузки и получения регулярности в языковой экосистеме. Кроме того, я не видел повышенного уровня рефакторинга (таким образом, снижения возможности компоновки модулей, которые не могут быть легко реорганизованы), который требуется для неуниверсального кода, о чем и заключается Проблема выражения, о которой я упоминал в своем первом посте.

В разделе « Общие подходы »:

Шаблоны пакетов
Это подход, используемый Modula-3, OCaml, SML (так называемые «функторы») и Ada. Вместо указания отдельного типа для специализации весь пакет является универсальным. Вы специализируете пакет, фиксируя параметры типа при импорте.

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

Скорее, я понимаю , что эта параметризация типа пакета (также известного как модуль) позволяет применять параметр(ы) типа к группе struct , interface и func .

Более сложная система типов
Это подход, который используют Haskell и Rust.
[…]
Минусы:

  • трудно вписаться в более простой язык (https://groups.google.com/d/msg/golang-nuts/smT_0BhHfBs/MWwGlB-n40kJ)

Цитирование @ianlancetaylor в связанном документе:

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

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

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

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

Но если бы транспилятор из синтаксиса класса типов эмулировал существующий ручной способ, которым Go может моделировать дженерики (редактировать: я только что прочитал, что @andrewcmyers утверждает, что это правдоподобно ), это может быть менее обременительным и найти полезную синергию. Например, я понял, что два класса типов параметров можно эмулировать с помощью interface , реализованного в struct , который эмулирует кортеж, или @jba упомянул идею использования встроенного interface в контексте . По-видимому, struct структурно, а не номинально типизированы, если не указано имя с type ? Также я подтвердил, что метод interface может вводить другой interface , так что afaics можно будет транспилировать из HKT в вашем примере сортировки, о котором я писал в своем предыдущем посте здесь. Но мне нужно больше думать об этом, когда я не так хочу спать.

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

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

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


@ianlancetaylor написал:

Разочаровывает то, что Go становится все более и более сложным, добавляя встроенные параметризованные типы.

Несмотря на спекуляции в этом вопросе, я думаю, что это крайне маловероятно.

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

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

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

@pciet написал:

Я голосую не за обобщенные универсальные приложения, а за более встроенные универсальные функции, такие как append и copy , которые работают с несколькими базовыми типами. Возможно, для типов коллекций можно добавить sort и search ?

Расширение этой инерции, возможно, помешает всеобъемлющей функции дженериков когда-либо появиться в Go. Те, кто хотел получить дженерики, скорее всего, уйдут на более зеленое пастбище. @andrewcmyers повторил это:

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

@шелби3

Afaik, единственный способ сделать разнородные контейнеры/коллекции в Go сейчас — это включить все типы в пустой интерфейс {}, который отбрасывает информацию о типизации и, как я предполагаю, требует приведения типов и проверки типов во время выполнения, что как бы 2 побеждает точку статическая типизация.

См. шаблон оболочки в комментариях выше для проверки статического типа коллекций interface{} в Go.

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

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

@pciet этот код буквально делает то же самое, что @shelby3 описывал и рассматривал антипаттерн. Цитирую вас ранее:

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

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

Этот подход имеет ряд недостатков:

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

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

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

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

type Foo interface {
    ...
}

type Bar interface {
    ...
}

Как мы можем выразить, используя чисто статическую проверку типов, что нам нужен тип, который реализует как Foo , так и Bar ? Насколько я знаю, это невозможно в Go (если не прибегать к проверкам во время выполнения, которые могут дать сбой, избегая безопасности статического типа).

С помощью системы обобщений на основе классов типов мы могли бы выразить это так:

func baz<T Foo + Bar>(t T) {
    ...
}

@tarcieri

Как мы можем выразить, используя чисто статическую проверку типов, что нам нужен тип, который реализует как Foo, так и Bar?

просто так:

type T interface {
    Foo
    Bar
}

func baz(t T) { ... }

@sbinet аккуратно, TIL

Лично я считаю отражение во время выполнения неправильной функцией, но это только я... Я могу объяснить, почему, если кому-то интересно.

Я думаю, что любой, кто реализует дженерики любого типа, должен сначала несколько раз прочитать "Элементы программирования" Степанова. Это позволит избежать многих проблем, связанных с Not Invented Here, и повторного изобретения колеса. После прочтения этого должно быть ясно, почему "концепции C++" и "классы типов Haskell" являются правильным способом создания дженериков.

Я вижу, что эта проблема снова активна
Вот игровая площадка для предложений соломенного человека
https://go-li.github.io/test.html
просто вставьте демонстрационные программы отсюда
https://github.com/go-li/demo

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

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

Также мы с нетерпением ждем любых дженериков, которые вы примете, продолжайте в том же духе!

@anlhord , где подробности реализации по этому поводу? Где можно прочитать о синтаксисе? Что реализовано? Что не реализовано? Каковы спецификации для этой реализации? Каковы плюсы и минусы для него?

Ссылка на игровую площадку содержит наихудший пример этого:

package main

import "fmt"

func main() {
    fmt.Println("Hello, playground")
}

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

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

Надеюсь, это поможет вам понять проблемы с вашим комментарием.

@joho написал:

Будет ли полезна научная литература для каких-либо указаний по оценке подходов?

Единственная статья, которую я читал на эту тему, — « Выгодно ли разработчикам использовать универсальные типы ?» (извините, платный доступ, вы можете найти в Google путь к загрузке в формате pdf), в котором было следующее, чтобы сказать

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

Я предполагаю, что ООП и подклассы (например, классы в Java и C++) не будут рассматриваться всерьез, потому что в Go уже есть класс типов, подобный interface (без явного параметра универсального типа T ), Java цитируется как то, что нельзя копировать, и потому, что многие утверждали, что они являются анти-шаблоном. Upthread я связался с некоторыми из этих аргументов. Мы могли бы углубиться в этот анализ, если кому-то интересно.

Я еще не изучал более новые исследования, такие как система Genus, упомянутая выше . Я опасаюсь систем «кухонной раковины», которые пытаются смешивать так много парадигм (например, подклассы, множественное наследование, ООП, линеаризация признаков, implicit , классы типов, абстрактные типы и т. д.) из-за жалоб на Scala. на практике так много крайних случаев, хотя, возможно, это улучшится с Scala 3 (он же Dotty и исчисление DOT). Мне любопытно, сравнивается ли их сравнительная таблица с экспериментальной Scala 3 или с текущей версией Scala?

Так что, на самом деле, остаются функторы ML и классы типов Haskell с точки зрения проверенных систем универсальности, которые значительно улучшают расширяемость и гибкость по сравнению с ООП + создание подклассов.

Я записал некоторые из личных дискуссий @keean, и у меня были модули функторов ML по сравнению с классами типов. Основные моменты кажутся:

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

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

  • Функторы ML более мощные/гибкие, чем классы типов, но это достигается за счет большего количества аннотаций и, возможно, большего количества взаимодействий в крайних случаях. И, согласно @keean , им требуются зависимые типы (для связанных типов ), что является более сложной системой типов. @keean считает, что Степановское _выражение универсальности как алгебра_ плюс классы типов достаточно мощны и гибки, так что это, кажется, лучшее место для современной, хорошо проверенной (в Haskell, а теперь и в Rust) универсальности. Однако аксиомы не применяются классами типов.

  • Я предложил добавить объединения для разнородных контейнеров с классами типов для расширения вдоль другой оси проблемы выражения, хотя это требует неизменности или копирования (только для случаев, когда используется разнородная расширяемость), которое, как известно, имеет O(log n) замедление по сравнению с безудержной изменчивой императивностью.

@larst написал:

Было бы интересно иметь один или несколько экспериментальных транспиляторов — исходный код Go generics для компилятора исходного кода Go 1.xy.

PS Я сомневаюсь, что Go примет такую ​​сложную систему типизации, но я подумываю о транспиляторе для существующего синтаксиса Go, как я упоминал в своем предыдущем посте (см. редактирование внизу). И мне нужна надежная универсальная система вместе с этими очень желательными функциями Go. Генераторы классов типов на Go — это то, что мне нужно.

@bcmills написал о своем предложении о функциях времени компиляции для универсальности:

Я слышал, что тот же аргумент используется для поддержки экспорта типов interface , а не конкретных типов в API Go, и обратное оказывается более распространенным: преждевременная абстракция чрезмерно ограничивает типы и препятствует расширению API. (Один такой пример см. в #19584.) Если вы хотите опираться на эту аргументацию, я думаю, вам нужно привести несколько конкретных примеров.

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

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

Применяется принцип наименьшей мощности. Сила/гибкость инвариантов, «спрятанных в» функциях времени компиляции для универсальности, должна быть сопоставлена ​​с их способностью причинять вред, например, читабельности исходного кода в экосистеме (при этом модульное разделение чрезвычайно важно, поскольку читатель не не нужно читать потенциально неограниченное количество кода из-за неявных транзитивных зависимостей, чтобы понять данный модуль/пакет!). Неявное разрешение экземпляров реализации класса типов имеет эту проблему, если их алгебра не соблюдается .

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

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

Но на самом деле Go не пытался разработать абстракцию для модуляризации этих эффектов. В Rust есть такая абстракция (которая, кстати, я считаю излишним пита/цурисом/ограничением для некоторых/большинства случаев использования, и я выступаю за более простую абстракцию однопоточной модели, но , к сожалению, Go не поддерживает ограничение всех порожденных горутин одним и тем же потоком ) . А Haskell требует монадического контроля над эффектами из-за применения чистых функций для ссылочной прозрачности .


@alercah написал:

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

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


@andrewcmyers написал:

Чтобы было ясно, я не считаю генерацию кода в стиле макросов, независимо от того, выполняется ли она с помощью gen, cpp, gofmt -r или других инструментов макросов/шаблонов, хорошим решением проблемы дженериков, даже если они стандартизированы. У него те же проблемы, что и у шаблонов C++: раздувание кода, отсутствие модульной проверки типов и сложность отладки. Ситуация усугубляется, когда вы начинаете, как это и естественно, строить универсальный код в терминах другого универсального кода. На мой взгляд, преимущества ограничены: это сделало бы жизнь разработчиков компилятора Go относительно простой, и он действительно производит эффективный код — если только нет давления на кэш инструкций, частая ситуация в современном программном обеспечении!

@keean , похоже , согласен с тобой.

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

Раздел «Обзор», по-видимому, подразумевает, что универсальное использование в Java ссылок на бокс для экземпляров...

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

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

Это заявление о том, что происходит с универсальными структурами данных в долгосрочной перспективе. Другими словами, общая структура данных часто заканчивается сбором всех различных вариантов использования, а не несколькими меньшими реализациями для разных целей. В качестве примера посмотрите https://www.scala-lang.org/api/2.12.3/scala/collection/immutable/List.html.

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

Общие структуры и API, которые работают с ними, как правило, более абстрактны, чем специально созданные API...

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

Добавлено примечание о когнитивной нагрузке многих подобных API.

Повторные реализации в особых случаях на практике не безграничны. Вы увидите только фиксированное количество специализаций.

Это не действительный минус.

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

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

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

Конечно, на практике вам может понадобиться этот стиль оптимизации менее чем в 1% случаев.

Альтернативные решения:

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

Шаблоны пакетов

Я могу ошибаться, но это кажется не совсем правильным. Функторы ML (не путать с функторами FP) также могут возвращать выходные данные, тип которых остается параметризованным.

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

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

Можете ли вы в следующий раз сделать комментарии/правки непосредственно в самом документе.

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

В качестве примера посмотрите https://www.scala-lang.org/api/2.12.3/scala/collection/immutable/List.html.

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

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

Я не думаю, что библиотека коллекций Scala будет репрезентативной для библиотек, созданных для PL только с классами типов для полиморфизма. Дело в том, что в коллекциях Scala используется антишаблон наследования , который приводит к сложной иерархии, в сочетании с помощниками implicit , такими как CanBuildFrom , которые резко увеличивают бюджет сложности. И я думаю, что если придерживаться точки зрения @keean о том, что «Элементы программирования» Степанова являются алгеброй , можно было бы создать элегантную библиотеку коллекций. Это была первая альтернатива библиотеке коллекций, основанной на функторах (FP), которую я видел (т.е. не копирующей Haskell ), также основанной на математике. Я хочу увидеть это на практике, и это одна из причин, по которой я сотрудничаю/обсуждаю с ним разработку нового PL. И на данный момент я планирую изначально транспилировать этот язык в Go (хотя я годами пытался найти способ избежать этого). Так что, надеюсь, вскоре мы сможем поэкспериментировать, чтобы увидеть, как это работает.

Я считаю, что сообщество/философия Go скорее подождет, чтобы увидеть, что работает на практике, и примет это позже, как только будет доказано, чем спешить и загрязнять язык неудачными экспериментами. Потому что, как вы повторили, все эти абстрактные утверждения не столь конструктивны (за исключением, может быть, теоретиков проектирования PL). Также, вероятно, неправдоподобно разработать согласованную систему дженериков комитетом.

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

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

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

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

Конечно, на практике вам может понадобиться этот стиль оптимизации менее чем в 1% случаев.

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

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

Вы можете быть правы. У меня также есть скептицизм относительно того, насколько часто я буду использовать какую-либо конкретную языковую функцию. Я думаю, что эксперименты, чтобы выяснить, как я продолжу. Разработка PL — это итеративный процесс, поэтому проблема заключается в борьбе с развивающейся инерцией, которая затрудняет итерацию процесса. Так что я думаю, что Роб Пайк прав в видео, где он предлагает писать программы, которые пишут код для программ (имеется в виду, писать инструменты генерации и транспиляторы), чтобы экспериментировать и тестировать идеи.

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

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

Я присоединяюсь к тем, кто хотел бы воспрепятствовать попытке добавить в Go какую-то слишком упрощенную функцию шаблонов и заявить, что это дженерики. IOW, я думаю, что правильно функционирующая система дженериков, которая не станет плохой инерцией, в корне несовместима с желанием иметь чрезмерно упрощенный дизайн дженериков. Afaik, система дженериков нуждается в хорошо продуманном и проверенном целостном дизайне. Вторя тому, что написал @larsth , я призываю тех, у кого есть серьезные предложения, сначала создать транспайлер (или реализовать его в ответвлении интерфейса gccgo), а затем поэкспериментировать с предложением, чтобы мы все могли лучше понять его ограничения. Меня вдохновило прочитать ветку, что @ianlancetaylor не думает, что в Go будет добавлено загрязнение плохой инерцией . Что касается моей конкретной жалобы на предложение параметризации на уровне пакета, мое предложение для тех, кто когда-либо его предлагает, подумайте, не сделать ли компилятор, с которым мы все могли бы играть, а затем мы все могли бы обсудить примеры того, что нам нравится и не нравится. мне это нравится. В противном случае мы говорим мимо друг друга, потому что, возможно, я даже не правильно понимаю предложение, как оно описано абстрактно. Я не должен понимать это предложение, потому что я не понимаю, как параметризованный пакет можно повторно использовать в другом пакете, который также параметризован. IOW, если пакет принимает параметры, то он также должен создавать экземпляры других пакетов с параметрами. Но, похоже, в предложении говорилось, что единственный способ создать экземпляр параметризованного пакета — использовать конкретный тип, а не параметры типа.

Извинения такие затянутые. Хочу убедиться, что меня правильно поняли.

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

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

  1. универсальные пакеты на основе «интерфейса»,
  2. явно общие пакеты.

Для 1. не требуется, чтобы общий пакет делал что-то особенное - например, текущий container/ring можно было бы использовать для специализации. Представьте здесь «специализацию» как замену всех экземпляров интерфейса в пакете конкретным типом (и игнорирование циклического импорта). Когда этот пакет сам специализируется на другом пакете, он может использовать "интерфейс" в качестве специализации - из этого следует, что тогда это использование также будет специализированным.

Для 2. вы можете посмотреть на них двумя способами. Одним из них является рекурсивная конкретная специализация при каждом импорте - аналогично шаблонам/макросам, ни в коем случае не будет "частично примененного пакета". Конечно, с функциональной стороны также можно увидеть, что общий пакет является частичным с параметрами, а затем вы его специализировали.

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

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

Я знаю, что это не было явно направлено на этот подход, но у него есть 4 разных прототипа для проверки идеи. Конечно, они не являются полноценными транспиляторами, но их достаточно для проверки некоторых идей. т.е. я не уверен, реализовал ли кто-нибудь случай «использования параметризованного пакета из другого».

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

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

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

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

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

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

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

Насколько я понимаю, это создаст меньше шаблонов, чем шаблоны C++, но больше, чем классы типов. У вас есть хорошая реальная программа для Ады, которая демонстрирует проблему? _(Под реальным я подразумеваю код, который кто-то использует/использовал в производстве.)_

Конечно, взгляните на мою доску Ada go: https://github.com/keean/Go-Board-Ada/blob/master/go.adb .

Хотя это довольно расплывчатое определение производства, код оптимизирован, работает так же, как версия C++ и ее открытый исходный код, а алгоритм совершенствовался в течение нескольких лет. Вы также можете посмотреть версию C++: https://github.com/keean/Go-Board/blob/master/go.cpp

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

Если вы хотите взглянуть на систему обобщений пакетов для императивного языка, я думаю, что Ada — одна из лучших для изучения. Жаль, что они решили пойти на мультипарадигму и добавить в Аду все ООП. Ада — это расширенный Паскаль, а Паскаль был небольшим и элегантным языком. Паскаль плюс дженерики Ады все еще были бы довольно маленьким языком, но, на мой взгляд, были бы намного лучше. Поскольку фокус Ады сместился на объектно-ориентированный подход, найти хорошую документацию и примеры того, как делать то же самое с дженериками, кажется трудно.

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

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

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

@kean писал :

Лично я считаю отражение во время выполнения неправильной функцией, но это только я... Я могу объяснить, почему, если кому-то интересно.

Стирание типов позволяет использовать «Теоремы бесплатно», что имеет практическое значение . Доступное для записи (и, возможно, даже читаемое из-за транзитивных отношений с императивным кодом?) отражение во время выполнения делает невозможным гарантировать ссылочную прозрачность в любом коде, и, следовательно, определенные оптимизации компилятора невозможны, а монады, безопасные для типов, невозможны. Я понимаю, что в Rust еще нет функции неизменности. OTOH, отражение позволяет проводить другие оптимизации , которые в противном случае были бы невозможны, если бы они не могли быть статически типизированы.

Я также заявил в аптреде:

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


@keean написал:

[…] для того, чтобы пакет был аппликативным, он должен быть чистым (то есть без изменяемых переменных на уровне пакета)

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

@egonelbre написал:

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

Очевидно, я имел в виду «первоклассные параметризованные пакеты» и соразмерный полиморфизм времени выполнения (также известный как динамический), о котором впоследствии упомянул @keean , потому что я предположил, что параметризованные пакеты были предложены вместо классов типов или ООП.

РЕДАКТИРОВАТЬ: но есть два возможных значения для модулей «первого класса»: модули как значения первого класса, такие как в Successor ML и MixML , отличающиеся от модулей как значения первого класса с типами первого класса, как в 1ML, и необходимый компромисс в модульной рекурсии (т.е. смешивании ) между ними.

@keean написал:

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

Что вы подразумеваете под зависимыми типами? (РЕДАКТИРОВАТЬ: теперь я предполагаю , что он имел в виду «независимую от значения» типизацию, то есть « функции, тип результата которых зависит от аргумента [время выполнения?] [типа]») Конечно, не зависит от значений, например, int данных, например, в Idris. Я думаю, вы имеете в виду зависимую типизацию (т.е. отслеживание) типа значений, представляющих экземпляры экземпляров модуля, вниз по иерархии вызовов, чтобы такие полиморфные функции можно было мономорфизировать во время компиляции? Возникает ли полиморфизм времени выполнения из-за того, что такие мономорфные типы являются экзистенциальным типом, привязанным к динамическим типам? Модули F-ing продемонстрировали, что «зависимые» типы не являются абсолютно необходимыми для моделирования модулей ML в системе F ω . Я слишком упростил, если предполагаю, что @rossberg переформулировал модель типизации, чтобы удалить все требования мономорфизации?

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

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

Классы типов могут реализовывать каждый тип только одним способом, в противном случае для преодоления ограничения требуется шаблонная оболочка newtype . Вот еще один пример нескольких способов реализации алгоритма. Afaics, @keean обошел это ограничение в своем примере сортировки класса типов , переопределив неявный выбор явно выбранным Relation , используя обертку типов data для общего обозначения различных отношений в типе значения, но я сомневаюсь является ли такая тактика общей для всех вариантов модульности. Однако более обобщенное решение (которое может помочь в улучшении проблемы модульности глобальной уникальности , возможно, в сочетании с ограничением потерянных файлов в качестве улучшения предлагаемого управления версиями для разрешения потерянных файлов за счет использования не по умолчанию для реализаций, которые могут быть потерянными) может заключаться в том, чтобы иметь дополнительный параметр типа неявно для всех классов типов interface , который, если он не указан, по умолчанию соответствует обычному неявному сопоставлению, но когда он указан (или когда он не указан, не соответствует любому другому 2 ), затем выбирает реализацию, которая имеет то же значение в своем списке пользовательских значений, разделенных запятыми (так что это более обобщенное, модульное сопоставление, чем имя конкретного экземпляра implement ). Список, разделенный запятыми, предназначен для того, чтобы реализация могла различаться более чем по одной степени свободы, например, если она имеет две ортогональные специализации. Желаемый специализированный не по умолчанию может быть указан либо в объявлении функции, либо на месте вызова. В месте вызова, например, f<non-default>(…) .

Так зачем нам параметризованные модули, если у нас есть классы типов? Afaics только для замены ( ← важная ссылка для щелчка), потому что повторное использование классов типов для этих целей не подходит, например, мы хотим, чтобы модуль пакета мог охватывать несколько файлов, и мы хотим иметь возможность неявно открывать содержимое модуль в область без дополнительного шаблона . Так что, возможно, продвижение вперед с параметризацией пакета _syntactical-only_ только подстановки (не первого класса) является разумным первым шагом, который может решить универсальность на уровне модуля, оставаясь при этом открытым для совместимости и неперекрывающейся функциональности, если позднее добавляются классы типов для функционального уровня. универсальность. макросы , например , типизированными или просто синтаксической (также известной как «препроцессор») подстановкой. Если они типизированы, то модули дублируют функциональность классов типов, что нежелательно как с точки зрения сведения к минимуму перекрывающихся парадигм/концепций ЯП, так и с точки зрения потенциальных крайних случаев из-за взаимодействий перекрытия (например , при попытке предложить как функторы ML, так и классы типов). ). Типизированные модули более модульны, потому что модификации любой инкапсулированной реализации внутри модуля, которые не изменяют экспортированные подписи, не могут привести к тому, что потребители модуля станут несовместимыми (кроме вышеупомянутой антимодульной проблемы перекрывающихся экземпляров реализации классов типов). Мне интересно прочитать мысли @keean по этому поводу.

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

В помощь другим читателям. Под «полиморфной рекурсией», я думаю, подразумеваются типы с более высоким рангом, например, параметризованные обратные вызовы, устанавливаемые во время выполнения, когда компилятор не может мономорфизировать тело функции обратного вызова, потому что оно неизвестно во время компиляции. Экзистенциальные типы, как я упоминал ранее, эквивалентны типаж-объектам Rust, которые являются одним из способов получения гетерогенных контейнеров с более поздней привязкой в ​​проблеме выражения, чем виртуальное наследование подклассов class , но не так открыто для расширения в выражении. Проблема в виде объединений с неизменяемыми структурами данных или копирования 3 , которые имеют стоимость производительности O (log n) .

1 В приведенном выше примере HKT не требуется, поскольку SET не требует, чтобы тип elem был параметром универсального типа set , т. е. это не set<elem> .

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

3 Обратите внимание, что изменение неизменяемых структур данных не обязательно требует копирования всей структуры данных, если структура данных достаточно умна, чтобы изолировать историю, такую ​​как односвязный список.

Реализация func pick(a CollectionOfT, count uint) []T была бы хорошим примером применения дженериков (из https://github.com/golang/go/issues/23717):

// pick returns a slice (len = n) of pseudorandomly chosen elements 
// in unspecified order from c which is an array, slice, or map.
for i, e := range pick(c, n) {

Подход к интерфейсу {} здесь сложен.

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

Кажется, Херб Саттер пришел к тому же выводу: теперь есть интересное предложение по программированию во время компиляции на C++ .

У него есть некоторые общие элементы как с пакетом Go reflect , так и с моим более ранним предложением для функций времени компиляции в Go .

Привет.
Я написал предложение для дженериков с ограничениями для Go. Вы можете прочитать это здесь . Возможно, его можно добавить как документ 15292. В основном он касается ограничений и читается как поправка к Taylors Type Parameters в Go .
Он задуман как пример работоспособного (я полагаю) способа создания «типобезопасных» дженериков в Go — надеюсь, он сможет что-то добавить к этому обсуждению.
Обратите внимание, что, хотя я прочитал (большую часть) эту очень длинную ветку, я не перешел по всем ссылкам в ней, поэтому другие могли сделать аналогичные предложения. Если это так, то прошу прощения.

бр. Хр.

Синтаксис байкшеддинг:

constraint[T] Array {
    :[#]T
}

может быть

type [T] Array constraint {
    _ [...]T
}

который больше похож на Иди ко мне. :-)

Здесь несколько элементов.

Одна вещь — заменить : на _ и заменить # на ... .
Я полагаю, вы могли бы сделать это, если это предпочтительнее.

Другое дело — заменить constraint[T] Array на type[T] Array constraint .
Казалось бы, это указывает на то, что ограничения являются типами, что я не думаю, что это правильно. Формально ограничение представляет собой _предикат_ для множества всех типов, т.е. отображение набора типов в набор { true , false }.
Или, если хотите, вы можете думать об ограничении просто как о наборе типов.
Это не тип _a_.

бр. Хр.

Почему это constraint не может быть просто interface ?

type [T io.Writer] List struct { 
    element T; 
    next *List[T];
}

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

Кроме того, если предложение по типам сумм принимается в той или иной форме (#19412), то их следует использовать для ограничения типа.

Хотя я считаю, что ключевое слово ограничения, что-то вроде этого следует добавить, чтобы не повторять большие ограничения и предотвратить ошибки по рассеянности.

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

// similar to the map[T]... syntax
// also no constraint
type List[T] struct {
    element T
    next *List[T]
}

// with constraint
type List[T] struct {
    element T
    next *List[T]
} where T is io.Writer | encoding.BinaryMarshaler

type BigConstraint constraint {
     io.Writer
     SomeFunc() int
     AnotherFunc()
     AField int64
     StringField string
}


// with predefined constraint
type List[T, U] struct {
    element T
    val U
    next *List[T, U]
} where T is BigConstraint | encoding.BinaryMarshaler,
    U is io.Reader

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

@surlykke Прошу прощения, если в предложении есть ответ на любой из них.

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

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

Коллекции - это другое использование:

// An unordered collection of comparable items.
type [T Comparable] Set []T

func (a Set) Diff(from Set) Set {
    // the implementation is the same as one with
    //     type Comparable interface { Equal(Comparable) bool }
    //     type Set []Comparable
}

// compile error
d := Set[int]{1, 2}.Diff(Set[string]{“abc”, “def”})

// Go 1, easier to read but runtime error
d := Set{1, 2}.Diff(Set{“abc”, “def”})

Для случая типа коллекции я не уверен, что это большая победа над дженериками Go 1, поскольку есть компромиссы в отношении читабельности.

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

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

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

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

@jba
Это немного многословно. Может быть, если бы в Go был лямбда-синтаксис, это было бы немного приятнее. С другой стороны, кажется, что самая большая проблема, которую он пытается решить, — это проверка того, поддерживает ли тип какой-либо оператор. Было бы проще, если бы в Go были предопределенные интерфейсы для различных операторов:

func equal[T](x, y T) bool
    where T is runtime.Equitable {
    return x == y
}

func copyable[T](x, y []T) int {
    return copy(x, y)
}

или что-то в этом роде.

Если проблема связана с расширением встроенных функций, то, возможно, проблема заключается в том, как язык создает типы адаптеров. Например, разве раздувание, связанное с sort.Interface, не является причиной https://github.com/golang/go/issues/16721 и sort.Slice?
Глядя на https://github.com/golang/go/issues/21670#issuecomment -325739411, идея @Sajmani о наличии интерфейсных литералов может быть ингредиентом, необходимым для того, чтобы параметры типа могли легко работать со встроенными функциями.
Посмотрите на следующее определение Iterator:

type [T] Iterator interface {
    Next() (elem T, done bool)
}

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

func SliceIterator(slice []T) Iterator {
    i := 0
    return Iterator{
        Next: func() (elem int, done bool) {
            v := slice[i]
            if i+1 == len(slice) {
                return v, true
            }
            i++
            return v, false
        },
    }
}

func main() {
    arr := []int{1,2,3,4,5}
    // SliceIterator works for an arbitrary slice
    print(SliceIterator(arr))
}

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

type ListNode struct {
    v string
    next *ListNode
}
func (l *ListNode) Iterator() Iterator {
    ptr := l
    return Iterator{
        Next: func() (elem int, done bool) {
            v := ptr.v
            if ptr.next == nil {
                return v, true
            }
            ptr = ptr.next
            return v, false
        },
    }
}

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

@pciet Я не совсем уверен, что вы подразумеваете под «уровнем приложения». В Go есть встроенная функция len , которая может применяться к массиву, указателю на массив, срезу, строке и каналу, поэтому в моем предложении, если параметр типа ограничен одним из них в качестве базового типа , к нему можно применить len .

@pciet О вашем примере с ограничением/интерфейсом Comparable . Обратите внимание, что если вы определяете (вариант интерфейса):

type Comparable interface { Equal(Comparable) bool }
type Set []Comparable

Затем вы можете поместить все, что реализует Comparable в Set . Сравните это с:

constraint [T] Comparable { Equal(t T) bool }
type [T Comparable[T]] Set []T
...
type FooSet Set[Foo] // Where Foo satisfies constraint Comparable

где вы можете поместить только значения типа Foo в FooSet . Это более сильная безопасность типов.

@urandom Опять же, я не фанат:

type MyConstraint constraint {....}

поскольку я не считаю константу типом. Также я бы точно не позволил:

var myVar MyConstraint

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

@urandom О байкшединге: я считаю, что ограничения должны быть объявлены сразу после параметров типа. Рассмотрим обычную функцию, определенную следующим образом:

func MyFunc(i) {
     if (i>0) fmt.Println("It's positive")
} with i being an integer

Вы не могли прочитать это слева направо. Вместо этого вы должны сначала прочитать func MyFunc(i) , чтобы определить, что это определение функции. Затем вам придется перейти к концу, чтобы выяснить, что такое i , а затем вернуться к телу функции. Не идеально, имхо. И я не вижу, чем должны отличаться общие определения.
Но очевидно, что эта дискуссия ортогональна обсуждению вопроса о том, должны ли в Go быть ограничения или дженерики.

@surlykke
Меня устраивает, что это не тип. Самое главное, что у них есть имя, чтобы на них можно было ссылаться несколькими типами.

Для функций, если мы будем следовать синтаксису rust, это будет:

func MyFunc[I](i I) int64
     where I is being an integer {
   return 42
}

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

@surlykke для потомков, не могли бы вы найти, куда можно добавить ваше предложение:
https://docs.google.com/document/d/1vrAy9gMpMoS3uaVphB32uVXX4pi-HnNjkMEgyAHX4N4

Это отличное место, чтобы "собрать" все предложения.

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

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

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

Практический сценарий: если мы рассмотрим случай пакета math/bits , выполнение утверждения типа для вызова OnesCount для каждого uintXX будет лучше, чем иметь эффективную библиотеку для манипулирования битами. Если, однако, утверждения типа были преобразованы в следующие

func OnesCount(x T) int {
    switch x.(type) {
    case uint:
        // separate uint functionality...
    case uint8:
        // separate uint8 functionality...
    case uint16:
        // separate uint16 functionality...
    case uint32:
        // separate uint32 functionality...
    case uint64:
        // separate uint64 functionality...
    }
}

Звонок в

var x uint8 = 255
bits.OnesCount(x)

затем вызовет следующую сгенерированную функцию (имя здесь не важно):

func $OnesCount_uint8(x uint8) {
    // separate uint8 functionality...
}

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

Если вы собираетесь использовать «операторы, используемые в функции» в качестве ограничений, то какие преимущества даст вам написание второй функции, содержащей подмножество операторов, используемых в первой?

@bcmills Один из них — спецификация, а другой — реализация. Это то же преимущество, что и статическая типизация: вы можете раньше отлавливать ошибки.

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

@mandolyte Я не совсем уверен, куда его добавить - может быть, абзац в разделе «Общие подходы» под названием «Общие методы с ограничениями»?
Документ, кажется, не содержит много об ограничении параметров типа, поэтому, если вы добавите абзац, где будет упомянуто мое предложение, то там могут быть перечислены и другие подходы к ограничениям.

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

@egonelbre Это очень мило. Спасибо!

@jba
Мне нравится ваше предложение, но я думаю, что оно слишком тяжело для golang. Это напоминает мне много шаблонов в C++. Основная проблема, я думаю, заключается в том, что с его помощью можно писать действительно сложный код.
Решить, перекрываются ли два экземпляра универсального интерфейса из-за перекрытия ограниченного набора типов, было бы сложной задачей, вызывающей замедление времени компиляции. То же самое для генерации кода.

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

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

  • Ограничения не так легко увидеть. Хотя теоретически godoc может перечислить все ограничения в документации, они не видны в исходном коде, кроме как неявно.
  • Из-за этого можно случайно включить дополнительное ограничение, которое будет видно только тогда, когда вы попытаетесь использовать функцию неожиданным образом. Требуя явной спецификации ограничений, программист должен точно знать, какие ограничения они вводят.
  • Это делает решение о том, какие ограничения разрешены, гораздо более случайным. Например, могу ли я определить следующую функцию? Каковы фактические ограничения на T, U и V здесь? Если мы требуем от программиста явного указания ограничений, то мы консервативны в том, какие ограничения мы допускаем (позволяя нам расширять их медленно и преднамеренно). Если мы в любом случае попытаемся быть консервативными, как нам выдать сообщение об ошибке для такой функции? «Ошибка: невозможно назначить uv() для T, потому что это накладывает недопустимое ограничение»?
func[T, U, V] Foo(u U, v V) {
  var t T = u.v(V) + 1;
}
  • Вызов универсальных функций в других универсальных функциях усугубляет описанную выше ситуацию, поскольку теперь вам нужно просмотреть все ограничения вызываемых объектов, чтобы понять ограничения функции, которую вы пишете или читаете.
  • Отладка может быть очень сложной, потому что сообщения об ошибках должны либо не предоставлять достаточно информации для поиска источника ограничения, либо они должны раскрывать внутренние детали функции. Например, если F есть какое-то требование к типу T , а автор F пытается выяснить, откуда взялось это требование, он хотел бы, чтобы компилятор предупредите их, какой именно оператор вызывает ограничение (особенно если он исходит от общего вызываемого объекта). Но пользователю F эта информация не нужна, и, действительно, если она включена в сообщения об ошибках, то мы пропускаем детали реализации F в сообщениях об ошибках от его пользователей, что ужасный пользовательский опыт.

@alercah

Например, могу ли я определить следующую функцию?

func[T, U, V] Foo(u U, v V) {
  var t T = u.v(V) + 1;
}

Нет u.v(V) — это синтаксическая ошибка, поскольку V — это тип, а переменная t не используется.

Однако вы можете определить эту функцию, которая может быть той, которую вы хотели:

func[T, U, V] Foo(u U, v V) {
    var _ T = u.v(v) + 1;
}

Каковы фактические ограничения на T, U и V здесь?

  • Тип V не имеет ограничений.
  • Тип U должен иметь метод v , который принимает один параметр или varargs некоторого типа, назначаемого из V , потому что u.v вызывается с одним аргументом. типа V .

    • U.v может быть полем функционального типа, но, возможно, это должно подразумевать метод; см. № 23796.

  • Тип, возвращаемый U.v , должен быть числовым, поскольку к нему добавляется константа 1 .
  • Тип возвращаемого значения U.v должен назначаться T , потому что u.v(…) + 1 присваивается переменной типа T .
  • Тип T должен быть числовым, поскольку возвращаемый тип U.v является числовым и может быть присвоен T .

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

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

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

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

func[T] Foo() (t T) {
    x := 42;
    t = T{x: "some string"}  // Is x an index, or a field name?
    _ = x
}

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

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

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

Не каждое соответствующее ограничение может быть выражено системой типов (см. также https://github.com/golang/go/issues/22876#issuecomment-347035323). Некоторые ограничения накладываются паниками во время выполнения; некоторые из них проверяются детектором гонки; наиболее опасные ограничения просто документируются и вообще не обнаруживаются.

Все эти «утечки внутренних деталей» в той или иной степени. (См. также https://xkcd.com/1172/.)

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

Может быть? Именно так авторы API используют аннотации типов в языках с выводом типов, таких как Haskell и ML, но это также ведет в кроличью нору глубоко параметрических («высшего порядка») типов в целом.

Например, предположим, что у вас есть эта функция:

func [F, Arg, Result] InvokeAsync(f F, x Arg) (<-chan Result) {
    c := make(chan result, 1)
    go func() { c <- f(x) }()
    return c
}

Как вы выражаете явные ограничения для типа Arg ? Они зависят от конкретного экземпляра F . Похоже, что такая зависимость отсутствует во многих недавних предложениях по ограничениям.

Нет. uv(V) — это синтаксическая ошибка, потому что V — это тип, а переменная t не используется.

Однако вы можете определить эту функцию, которая может быть той, которую вы хотели:

Да, это было намерением, мои извинения.

Тип T должен быть числовым, потому что возвращаемый тип U.v является числовым и может быть присвоен T .

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

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

Я имел в виду «ограничения, которые мы допускаем», как в языке. С явными ограничениями нам гораздо проще решить, какие ограничения мы хотим разрешить пользователям писать, а не просто говорить, что ограничение — это «все, что заставляет вещи компилироваться». Например, мой пример Foo выше на самом деле включает неявный дополнительный тип, отдельный от T , U или V , поскольку мы должны учитывать возвращаемый тип u.v . Этот тип никаким образом не упоминается явно в объявлении f ; свойства, которыми он должен обладать, совершенно неявны. Точно так же, хотим ли мы разрешить типы с более высоким рангом ( forall )? Я не могу придумать пример навскидку, но я также не могу убедить себя, что вы не можете неявно написать привязку типа с более высоким рангом.

Другой пример — следует ли разрешать функции использовать преимущества перегруженного синтаксиса. Если функция с неявными ограничениями выполняет for i := range t для некоторого t универсального типа T , синтаксис работает, если T является любым массивом, срезом, каналом, или карту. Но семантика совсем другая, особенно если T — тип канала. Например, если t == nil (что может случиться, пока T является массивом), то итерация либо ничего не делает, поскольку в нулевом слайсе или карте нет элементов, либо блокируется навсегда так как это то, что получает на каналах nil . Это большой футган ждет, чтобы случиться. Аналогично делается m[i] = ... ; если я намереваюсь, чтобы m был картой, мне нужно будет принять меры против того, чтобы он на самом деле был срезом, поскольку в противном случае код может запаниковать при назначении вне диапазона.

На самом деле, я думаю, это служит еще одним аргументом против неявных ограничений: авторы API могут писать искусственные операторы только для того, чтобы добавить ограничения. Например, for _, _ := range t { break } запрещает канал, но разрешает карты, срезы и массивы; x = append(x) заставляет x иметь тип среза. var _ = make(T, 0) позволяет использовать срезы, карты и каналы, но не массивы. Будет книга рецептов о том, как неявно добавлять ограничения, чтобы кто-то не мог вызвать вашу функцию с типом, для которого вы не написали правильный код. Я даже не могу придумать способ написать код, который компилируется только для типов карт, если я также не знаю тип ключа. И я вовсе не думаю, что это гипотетично; карты и срезы ведут себя совершенно по-разному для большинства приложений.

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

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

Не каждое соответствующее ограничение может быть выражено системой типов (см. также #22876 (комментарий)). Некоторые ограничения накладываются паниками во время выполнения; некоторые из них проверяются детектором гонки; наиболее опасные ограничения просто документируются и вообще не обнаруживаются.

Все эти «утечки внутренних деталей» в той или иной степени. (См. также https://xkcd.com/1172/.)

Я действительно не понимаю, как сюда входит #22876; который пытается использовать систему типов для выражения ограничения другого типа. Всегда будет верно, что мы не можем выразить некоторые ограничения на значения или на программы, даже с системой типов произвольной сложности. Но мы говорим здесь только об ограничениях на типы . Компилятор должен быть в состоянии ответить на вопрос «Могу ли я создать этот универсальный экземпляр с типом T ?» это означает, что он должен понимать ограничения, явные или неявные. (Обратите внимание, что некоторые языки, такие как C++ и Rust, не могут решить этот вопрос в целом, потому что он может зависеть от произвольных вычислений и, таким образом, сводится к проблеме остановки, но они по-прежнему выражают ограничения, которые должны быть удовлетворены.)

То, что я имею в виду, больше похоже на «какое сообщение об ошибке должно даваться в следующем примере?»

func [U] DirectlyConstrained(U t) {
    t.DoSomething();
}
func [T] IndirectlyConstrained(T t) {
    DirectlyConstrainted(t);
}
func Illegal() {
    IndirectlyConstrained(4);
}

Мы можем сказать Error: cannot call IndirectlyConstrained with [T = int]; T must have a method with signature func (T t) DoSomething() . Это сообщение об ошибке полезно для пользователя IndirectlyConstrained , потому что оно четко указывает на то, что они отсутствуют. Но он не предоставляет никакой информации для тех, кто пытается отладить, почему IndirectlyConstrained имеет это ограничение, что является большой проблемой удобства использования, если это большая функция. Мы могли бы добавить Note: this constraint exists on T because IndirectlyConstrained calls DirectlyConstrained with [U = T] on line N , но теперь мы просочились в детали реализации IndirectlyConstrained . Кроме того, мы не объяснили, почему IndirectlyConstrained имеет ограничение, поэтому мы добавляем еще Note: this constraint exists on U because DirectlyConstrained calls t.DoSomething() on line M ? Что, если неявное ограничение исходит от какого-то вызываемого объекта, расположенного на четыре уровня ниже по стеку вызовов?

Кроме того, как нам отформатировать эти сообщения об ошибках для типов, которые явно не указаны в качестве параметров? Например, если в приведенном выше примере IndirectlyConstrained вызывает DirectlyConstrained(t.U()) . Как мы вообще относимся к типу? В этом случае мы могли бы сказать the type of t.U() , но значение не обязательно будет результатом одного выражения; он может быть построен из нескольких утверждений. Затем нам нужно либо синтезировать выражение с правильными типами для сообщения об ошибке, которое никогда не появляется в коде, либо нам нужно найти какой-то другой способ сослаться на него, который будет менее понятен для пользователя. бедный абонент, который нарушил ограничение.

Как вы выражаете явные ограничения на тип Arg? Они зависят от конкретной реализации F. Похоже, что такая зависимость отсутствует во многих недавних предложениях по ограничениям.

Отбросьте F и сделайте f типом func (Arg) Result . Да, он игнорирует функции с переменным числом переменных, но и остальная часть Go тоже. Предложение сделать varargs funcs назначаемыми совместимым подписям можно сделать отдельно.

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

Пожалуйста, оцените мою реализацию дженериков в вашем следующем проекте
http://go-li.github.io/

Мы можем сказать Error: cannot call IndirectlyConstrained with [T = int]; T must have a method with signature func (T t) DoSomething() . Это сообщение об ошибке […] не предоставляет никакой информации для тех, кто пытается отладить, почему IndirectlyConstrained имеет это ограничение, что является большой проблемой с точки зрения удобства использования, если это большая функция.

Я хочу указать на важное предположение, которое вы здесь делаете: сообщение об ошибке от go build является _единственным_ инструментом, доступным программисту для диагностики проблемы.

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

То есть отладка во время выполнения — это интерактивный процесс. Так почему же мы должны предполагать неинтерактивную отладку для ошибок времени компиляции⸮ В качестве одной из альтернатив мы могли бы научить инструмент guru ограничениям типов. Тогда вывод компилятора будет примерно таким:

somefile.go:123: Argument `4` to DirectlyConstrained has type `int`,
    but DirectlyConstrained requires a type `T` with method `DoSomething()`.
    (For more detail, run `guru contraints path/to/somefile.go:#1033`.)

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

Мы могли бы добавить Note: this constraint exists on T because IndirectlyConstrained calls DirectlyConstrained with [U = T] on line N , но теперь мы просочились в детали реализации IndirectlyConstrained .

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

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

Если функция с неявными ограничениями делает для i := range t для некоторого t универсального типа T , синтаксис работает, если T является любым массивом, срезом, каналом , или карту. Но семантика совершенно другая, особенно если T является типом канала.

Я думаю, что это более убедительный аргумент в пользу ограничений типов, но он не требует, чтобы явные ограничения были настолько подробными, как предлагают некоторые люди. Чтобы устранить неоднозначность сайтов вызова, кажется достаточным ограничить параметры типа чем-то близким к reflect.Kind . Нам не нужно описывать операции, которые и так понятны из кода; вместо этого нам нужно только сказать что-то вроде « T — это тип среза». Это приводит к гораздо более простому набору ограничений:

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

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

TypeConstraint = "sliceable" | "map" | "chan" | "struct" | "integer" | "float" | "type"

с такими примерами, как:

func[T:integer, U, V] Foo(u U, v V) {
    var _ T = u.v(v) + 1;
}
func [S:sliceable, T] append(s S, x ...T) S {
    dst := s
    if cap(s) - len(s) < len(x) {
        dst = make(S, len(s), nextSizeClass(cap(s)))
        copy(dst, s)
    }
    copy(dst[len(s):cap(s)], x)
    return dst[:len(s)+len(x)]
}

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

Чтобы упростить объяснения, мы можем добавить новый элемент кода, genre .
Отношение между жанрами и типами подобно отношению между типами и ценностями.
Другими словами, под жанром понимается тип типов.

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

  • Буль
  • Нить
  • Int8, Uint8, Int16, Uint16, Int32, Uint32, Int64, Uint64, Int, Uint, Uintptr
  • Плавающее32, Плавающее64
  • Комплекс64, Комплекс128
  • Массив, срез, карта, канал, указатель, UnsafePointer

Есть и другие заранее объявленные жанры, такие как Comaprable, Numeric, Interger, Float, Complex, Container и т. д. Мы можем использовать Type или * для обозначения жанра всех типов.

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

Каждая структура, интерфейс и тип функции соответствуют жанру.

Мы также можем объявить пользовательские жанры:

genre Addable = Numeric | String
genre Orderable = Interger | Float | String
genre Validator = func(int) bool // each parameter and result type must be a specified type.
genre HaveFieldsAndMethods = {
    width  int // we must use a specific type to define the fields.
    height int // we can't use a genre to define the fields.
    Load(v []byte) error // each parameter and result type must be a specified type.
    DoSomthing()
}
genre GenreFromStruct = aStructType // declare a genre from a struct type
genre GenreFromInterface = anInterfaceType // declare a genre from an interface type
genre GenreFromStructInterface = aStructType + anInterfaceType
genre ComparableStruct = HaveFieldsAndMethods & Comprable
genre UncomparableStruct = HaveFieldsAndMethods &^ Comprable

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

  • Const Integer — это жанр (отличный от Integer ), и его экземпляр должен быть постоянным значением, тип которого должен быть целым числом. Однако постоянное значение можно рассматривать как особый тип.
  • Const func(int) bool — это жанр (отличный от func(int) bool ), и его экземпляр должен быть значением функции decared. Однако объявление функции можно рассматривать как особый тип.

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

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

Объявление ящика (предположим, что следующий код объявлен в пакете lib ):

crate Example [T Float, S {width, height T}, N Const Integer] [*, *, *] {
    type MyArray [N]T

    func Add(a, b T) T {
        return a+b
    }

    type M struct {
        x T
        y S
    }

    func (m *M) Area() T {
        m.DoSomthing()
        return m.y.width * m.y.height
    }

    func (m *M) Perimeter() T {
        return 2 * Add(m.y.width, m.y.height)
    }

    export M, Add, MyArray
}

Используя вышеупомянутый ящик.

import "lib"

// We can use AddFunc as a normal delcared function.
// Its genre is "Const func (a, b T) T"
type Rect, AddFunc, Array = lib.Example[float32, struct{x, y float32}, 100]

func demo() {
    var r Rect
    a, p = r.Area(), r.Perimeter()
    _ = AddFunc(a, p)
}

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

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

В среду, 4 апреля 2018 г., 12:41 dotaheor уведомления@github.com написал:

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

Чтобы упростить объяснения, мы можем добавить новый элемент кода — жанр.
Отношения между жанрами и типами подобны отношениям между типами.
и ценности.
Другими словами, под жанром понимается тип типов.

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

  • Буль
  • Нить
  • Int8, Uint8, Int16, Uint16, Int32, Uint32, Int64, Uint64, Int, Uint,
    Uintptr
    и с плавающей запятой32, с плавающей запятой64
  • Комплекс64, Комплекс128
  • Массив, срез, карта, канал, указатель, UnsafePointer

Есть и другие заранее заявленные жанры, такие как Comaprable, Numeric,
Interger, Float, Complex, Container и т. д. Мы можем использовать Type или * для обозначения
Жанр всех видов.

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

Каждая структура, интерфейс и тип функции соответствуют жанру.

Мы также можем объявить пользовательские жанры:

жанр Добавляемый = Числовой | Нить
жанр Заказываемый = Interger | Поплавок | Нить
жанр Validator = func(int) bool // каждый параметр и тип результата должны быть определенного типа.
жанр HaveFieldsAndMethods = {
width int // мы должны использовать определенный тип для определения полей.
height int // мы не можем использовать жанр для определения полей.
Load(v []byte) error // каждый параметр и тип результата должны быть определенного типа.
Делай что-нибудь()
}
жанр GenreFromStruct = aStructType // объявить жанр из типа структуры
жанр GenreFromInterface = anInterfaceType // объявляем жанр из типа интерфейса
жанр GenreFromStructInterface = aStructType | тип интерфейса

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

  • Const Integer — это жанр, и его экземпляр должен быть постоянным значением.
    тип должен быть целым числом.
    Однако постоянное значение можно рассматривать как особый тип.
  • Const func(int) bool — это жанр, и его экземпляр должен быть объявлен.
    значение функции.
    Однако объявление функции можно рассматривать как особый тип.

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

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

Объявление ящика (предположим, что следующий код объявлен в lib
упаковка):

crate Пример [T Float, S {ширина, высота T}, N Const Integer] [*, *, *] {
введите MyArray [N] T

функция Add(a, b T) T {
вернуть а+б
}

// Жанр ящика. Можно использовать только в ящике.

// М — тип жанра G
тип M структура {
х Т
у С
}

функция (м *М) Площадь() Т {
м.Делай что-нибудь()
вернуть мою ширину * мою высоту
}

функция (m *M) Периметр() T {
вернуть 2 * добавить (моя ширина, моя высота)
}

экспорт M, Добавить, MyArray
}

Используя вышеупомянутый ящик.

импортировать «библиотеку»

// Мы можем использовать AddFunc как обычную функцию с делегированием.
введите Rect, AddFunc, Array = lib.Example (float32, struct {x, y float32})

функция демо () {
вар r прямоугольный
a, p = r.Площадь(), r.Периметр()
_ = AddFunc(a, p)
}

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


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

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

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

package lib

// export a type
crate List [T *] * {
    type List struct {
        ...
    }

    export List
}

используй это:

import "lib"

var l lib.List[int]

Будут некоторые правила «жанровой дедукции», точно так же, как «типовая дедукция» в текущей системе.

@dotaheor , @DemiMarie правильно. Ваша концепция «жанра» звучит точно так же, как «вид» из теории типов. (Ваше предложение требует правила подтипа, но это не редкость.)

Ключевое слово genre в вашем предложении определяет новые виды как надвиды существующих видов. Ключевое слово crate определяет объекты с «подписями ящиков», которые не являются подвидом Type .

Как формальная система, ваше предложение выглядит примерно так:

Ящик ::= χ | ⋯
Тип ::= τ | χ | int | bool | ⋯ | func(τ) | func(τ) τ | []τ | χ[τ₁, …]

CrateSig ::= [κ₁, …] ⇒ [κₙ, …]
Вид ::= κ | exactly τ | kindOf κ | Map | Chan | ⋯ | Const κ | Type | CrateSig

Злоупотреблять некоторыми обозначениями теории типов:

  • Читать «⊢» как «влечет за собой».
  • Прочитайте « k1k2 », поскольку « k1 является подвидом k2 ».
  • Читать «:» как «в своем роде».

Тогда правила выглядят примерно так:

τ : exactly τ
exactly τkindOf exactly τ
kindOf exactly τType

τ : κ₁κ₁κ₂τ : κ₂

τ₁ : Typeτ₂ : TypekindOf exactly map[τ₁]τ₂Map
MapType

κ₁κ₂Const κ₁Const κ₂

[…]
(И так далее для всех встроенных типов)


Определения типов присваивают виды, а базовые виды свертываются до видов встроенных типов:

type τ₁ τ₂τ₂ : κτ₁ : kindOf κ

kindOf kindOf κkindOf κ
kindOf MapMap
[…]


genre определяет новые отношения подтипов:
genre κ = κ₁ | κ₂κ₁κ
genre κ = κ₁ | κ₂κ₂κ

(Вы можете определить Numeric и т.п. в терминах | .)

genre κ = κ₁ & κ₂ ∧ ( κ₃κ₁ ) ∧ ( κ₃κ₂ ) ⊢ κ₃κ


Правило расширения ящика аналогично:
type τₙ, … = χ[τ₁, …] ∧ ( χ : [κ₁, …] ⇒ [κₙ, …] ) ∧ ( τ₁ : κ₁ ) ∧ ⋯ ⊢ τₙ : κₙ

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


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

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

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

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

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

Механизмы на основе классов типов, такие как в Genus и Familia, могут сделать это эффективно. Подробнее см. в нашем документе PLDI 2015.

@ДемиМари
Я думаю, что "жанр" == "набор признаков".

[редактировать]
Возможно, traits — лучший ключевой код.
Мы можем просмотреть каждый вид также набор признаков.

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

[править 2]
Предположим, что есть два набора признаков A и B, мы можем выполнить следующие операции:

A + B: union set
A - B: difference set
A & B: intersection set

Набор признаков типа аргумента должен быть надмножеством соответствующего жанра параметров (набор признаков).
Набор признаков типа результата должен быть подмножеством соответствующего жанра результата (набор признаков).

(ПО МОЕМУ МНЕНИЮ)

Тем не менее, я думаю, что перепривязка псевдонимов типов — это правильный путь для добавления дженериков в Go. Это не требует огромных изменений в языке. Пакеты, обобщенные таким образом, по-прежнему можно использовать в Go 1.x. И нет необходимости добавлять ограничения, потому что это можно сделать, установив тип по умолчанию для псевдонима типа на что-то, что уже соответствует этим ограничениям. И наиболее важным аспектом перепривязки псевдонимов типов является то, что встроенные составные типы (срезы, карты и каналы) не нужно изменять и обобщать.

@dc0d

Как псевдонимы типов должны заменять дженерики?

@sighoya Перепривязка псевдонимов типов может заменить дженерики (а не только псевдонимы типов). Предположим, что пакет вводит некоторые псевдонимы типа уровня пакета, такие как:

package likedlist

type T = interface{}

type LinkedList struct {
    // ...
}

Если предоставляется Type Alias ​​Rebinding (и средства компилятора), то можно использовать этот пакет для создания связанных списков для разных конкретных типов вместо пустого интерфейса:

package main

import (
    "likedlist"
)

type intLL = likedlist.LinkedList(likedlist.T = int)
type stringLL = likedlist.LinkedList(likedlist.T = string)

func main() {}

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

// pkg.go
package pkg

type ListNode struct {
    prev, next *ListNode
    element    ?Element
}

func Add(x, y ?T) ?T {
    return x+y
}



// main.go
package main

import "pkg"

type intList = pkg.ListNode[Element=int]
func stringAdd = pkg.Add[T=string]

func main() {
}

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

@dotaheor Это несовместимо с Go 1.x.

@creker Я реализовал инструмент (с именем goreuse ), который использует эту технику для генерации кода и родился как концепция повторного связывания псевдонимов типов.

Его можно найти здесь . Существует 15-минутное видео, которое объясняет инструмент.

@ dc0d , так что он работает как шаблоны C ++, генерирующие специализированные реализации. Я не думаю, что это будет принято, поскольку команда Go (и, честно говоря, я и многие другие люди здесь), кажется, против всего, что похоже на шаблоны C++. Это увеличивает количество двоичных файлов, замедляет компиляцию и, возможно, не может привести к значимым ошибкам. И, кроме того, он несовместим только с бинарными пакетами, которые поддерживает Go. Вот почему C++ решил писать шаблоны в файлах заголовков.

@крекер

поэтому он работает как шаблоны C++, генерирующие специализированные реализации для каждого используемого типа.

Я не знаю (прошло около 16 лет с тех пор, как я написал C++). Но судя по вашему объяснению, так оно и есть. Тем не менее, я не уверен, являются ли они одинаковыми и каким образом.

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

Конечно, у всех здесь есть веские причины для своих предпочтений, основанных на их приоритетах. Первым в моем списке стоит совместимость с Go 1.x.

Он увеличивает двоичные файлы,

Это может быть.

замедляет компиляцию,

Я очень сомневаюсь в этом (так как это можно испытать с goreuse ).

И, кроме того, он несовместим только с бинарными пакетами, которые поддерживает Go.

Я не уверена. Поддерживают ли это другие способы реализации дженериков?

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

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

@dc0d

Я немного думаю об этом.
Помимо того, что он внутренне установлен на интерфейсах, буква «Т» в вашем примере

type T=interface{}

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

@sighoya Я не уверен, что понял, что ты сказал.

Внутренне устанавливается на интерфейсы

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

type T = int

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

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

но это будет означать введение дженериков.

Это _это_ способ введения/реализации дженериков в самом языке Go.

@dc0d

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

Установка 'type T=Int' не даст многого.

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

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

Почему бы не написать вместо этого?:

fun<type T>(t T)

или

fun[type T](t T)

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

@dc0d написал

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

Можете ли вы рассказать об этом подробнее?

@сигхойя

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

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

Можете ли вы рассказать об этом подробнее?

Например, если псевдоним типа параметра типа уровня пакета определен следующим образом:

package genericadd

type T = int

func Add(a, b T) T { return a + b }

Затем практически все числовые типы могут быть назначены T , например:

package main

import (
    "genericadd"
)

var add = genericadd.Add(
    T = float64
)

func main() {
    var (
        a, b float64
    )

    println(add(a, b))
}

@dc0d

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

Они одинаковы в том смысле, что они работают почти одинаково, насколько я вижу. Для каждого экземпляра шаблона класса компилятор будет генерировать уникальную реализацию, если он впервые увидит использование конкретной комбинации шаблона класса и его списка параметров. Это увеличивает размер двоичного файла, поскольку теперь у вас есть несколько реализаций одного и того же шаблона класса. Замедляет компиляцию, так как компилятору теперь нужно генерировать эти реализации и выполнять всевозможные проверки. В случае C++ увеличение времени компиляции может быть огромным. Ваши игрушечные примеры быстры, как и примеры C++.

Я не уверена. Поддерживают ли это другие способы реализации дженериков?

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

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

Просто чтобы проверить мое понимание... рассмотрите этот репозиторий алгоритмов копирования/вставки [ здесь ]. Если вы не хотите использовать «int», код нельзя использовать напрямую. Его нужно скопировать, вставить и изменить, чтобы он работал. И под модификациями я подразумеваю, что каждый экземпляр «int» должен быть изменен на любой тип, который вам действительно нужен.

Подход псевдонима типа внес бы изменения один раз, скажем, T, и вставил бы строку «type T int». Тогда компилятору нужно будет пересвязать T с чем-то другим, скажем, с float64.

Следовательно:
а) Я бы сказал, что замедления работы компилятора не будет, если вы на самом деле не использовали эту технику. Так что это ваш выбор.
b) Учитывая новый материал vgo, где можно использовать несколько версий одного и того же кода... это означает, что должен быть какой-то метод скрытия используемых источников вне поля зрения, тогда, конечно, компилятор может отслеживать, являются ли два используется одно и то же повторное связывание и избегается дублирование. Поэтому я думаю, что раздувание кода будет таким же, как и современные методы копирования/вставки.

Мне кажется, что между псевдонимами типов и грядущим vgo основа для такого подхода к дженерикам почти завершена...

В предложении [ здесь ] перечислены некоторые «неизвестные». Так что было бы неплохо еще немного раскрыть его.

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

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

В пн, 9 апр. 2018 г., 8:32 Антоненко Артем[email protected]
написал:

@mandolyte https://github.com/mandolyte вы можете добавить еще один уровень
косвенность путем помещения специализированных типов в некоторый общий контейнер. Тот
способ, которым ваша реализация может остаться прежней. Затем компилятор сделает все
магия. Я думаю, что предложение параметров типа Яна работает именно так.


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

Мне кажется, что в этом обсуждении компромисса между модульностью и производительностью присутствует понятная путаница. Техника C++ повторной проверки типов и создания экземпляров универсального кода для каждого типа, для которого он используется, плоха для модульности, плоха для двоичных дистрибутивов и из-за раздувания кода плохо влияет на производительность. Хорошая часть этого подхода заключается в том, что он автоматически сопоставляет сгенерированный код с используемыми типами, что особенно полезно, когда используемые типы являются примитивными типами, такими как int . Java однородно транслирует общий код, но платит за это производительностью, особенно когда код использует тип T[] .

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

  1. Создавайте специализированные экземпляры для примитивных типов. Это может быть сделано либо автоматически, либо по указанию программиста. Некоторая диспетчеризация необходима для доступа к правильному экземпляру, но ее можно объединить с диспетчеризацией, уже необходимой для гомогенного перевода. Это будет работать аналогично C#, но не требует полной генерации кода во время выполнения; в среде выполнения может потребоваться небольшая дополнительная поддержка для настройки таблиц диспетчеризации по мере загрузки кода.
  2. Используйте единую универсальную реализацию, в которой массив T фактически представлен как массив примитивного типа, когда T инстанцируется как примитивный тип. Этот подход, который мы использовали в PolyJ, Genus и Familia, значительно повышает производительность по сравнению с подходом Java, хотя и не так быстро, как полностью специализированная реализация.

@dc0d

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

Псевдонимы типов — это неправильный путь, потому что это должна быть постоянная ссылка.
Лучше написать «T Type» напрямую, и тогда вы увидите, что действительно используете дженерики.

Почему вы хотите использовать глобальную переменную типа «T» для всего пакета/модуля, переменные локального типа в <> или [] более модульны.

@крекер

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

Для ссылочных типов, но не для типов значений.

@ДемиМари

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

«Стирание типа» неоднозначно, я предполагаю, что вы имеете в виду стирание параметра типа, то, что предоставляет Java, что также не совсем верно.
Java имеет мономорфизацию, но она постоянно мономорфизируется (полу) до верхней границы общего ограничения, которое в основном является объектом.
Чтобы предоставить методы и поля других типов, верхняя граница внутренне приводится к вашему соответствующему типу, что довольно уродливо.
Если проект Valhalla будет принят, ситуация изменится для типов значений, но, к сожалению, не для ссылочных типов.

Go не нужно идти по пути Java, потому что:

«Двоичная совместимость для скомпилированных пакетов между выпусками не гарантируется»

тогда как это невозможно в Java.

Мне кажется, что в этом обсуждении компромисса между модульностью и производительностью присутствует понятная путаница. Техника C++ повторной проверки типов и создания экземпляров универсального кода для каждого типа, для которого он используется, плоха для модульности, плоха для двоичных дистрибутивов и из-за раздувания кода плохо влияет на производительность.

О каком исполнении здесь идет речь?

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

Если под «раздуванием кода» и «производительностью» вы подразумеваете «размер входных данных компоновщика» и «время компоновки», то проблема также довольно проста, если вы можете сделать определенные (разумные) предположения о вашей системе сборки. Вместо создания каждой специализации в каждой единице компиляции вы можете вместо этого создать список необходимых специализаций, и система сборки создаст экземпляр каждой уникальной специализации ровно один раз перед компоновкой («модель Cfront»). IIRC, это одна из проблем, которую пытаются решить модули C++ .

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


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

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

@сигхойя

Для ссылочных типов, но не для типов значений.

Из того, что я прочитал, C# JIT выполняет специализацию во время выполнения для каждого типа значения и один раз для всех ссылочных типов. Нет специализации времени компиляции (IL-time). Вот почему подход C# полностью игнорируется — команда Go не хочет зависеть от генерации кода во время выполнения, поскольку это ограничивает платформы, на которых может работать Go. В частности, в iOS вам не разрешено генерировать код во время выполнения. Это работает, и я на самом деле сделал некоторые из них, но Apple не разрешает это в AppStore.

Как ты сделал это?

В пн, 9 апр. 2018 г., 15:41 Антоненко Артем[email protected]
написал:

@sighoya https://github.com/sighoya

Для ссылочных типов, но не для типов значений.

Из того, что я читал, С# JIT выполняет специализацию во время выполнения для каждого значения.
тип и один раз для всех ссылочных типов. Нет времени компиляции
специализация. Вот почему подход C# полностью игнорируется — команда Go
не хочет зависеть от генерации кода во время выполнения, поскольку это ограничивает платформы Go
может продолжаться. В частности, на iOS вам не разрешено генерировать код.
во время выполнения. Это работает, и я на самом деле сделал некоторые из них, но Apple не разрешает
это в AppStore.


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

@DemiMarie запустил мой старый исследовательский код, чтобы убедиться (это исследование было прекращено по другим причинам). В очередной раз отладчик ввел меня в заблуждение. Я выделяю страницу, пишу на нее какие-то инструкции, защищаю ее с помощью PROT_EXEC и перехожу на нее. Под отладчиком работает. Без отладчика приложение получает сигнал SIGKILL с сообщением CODESIGN в журнале сбоев, как и ожидалось. Таким образом, он не работает даже без AppStore. Еще более сильный аргумент против генерации кода во время выполнения, если iOS важна для Go.

Во-первых, было бы полезно еще раз обдумать 5 правил программирования Роба Пайка .

Второе (ИМХО):

Что касается медленной компиляции и размера двоичного файла, сколько универсальных типов используется в распространенных типах приложений, разрабатываемых с использованием Go (_n обычно мало_ из правила 3)? Если проблема не требует высокого уровня кардинальности в конкретных концепциях (большое количество типов), на эти накладные расходы можно не обращать внимания. Даже тогда я бы сказал, что что-то не так с этим подходом. При внедрении системы электронной коммерции никто не определяет отдельный тип для каждого вида продукта, его вариаций и, возможно, возможных настроек.

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

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

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

Ущерб производительности @dc0d обычно означает заполнение кеша инструкций из-за разных реализаций шаблонов классов. Как именно это соотносится с реальной производительностью — вопрос открытый, никаких бенчмарков я не знаю, но теоретически это проблема.

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

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

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

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

Вт, 10 апр. 2018 г., 5:46 Каве Шахбазян[email protected]
написал:

Во-первых, было бы полезно обдумать 5 правил программирования Роба Пайка.
https://users.ece.utexas.edu/%7Eadnan/pike.html еще раз.

Второе (ИМХО):

О медленной компиляции и размере бинарного файла, сколько общих типов используется в
распространенные типы приложений, которые разрабатываются с использованием Go ( n — этообычно маленькие из правила 3)? Если проблема не требует высокого уровня
количество элементов в конкретных понятиях (большое количество типов), которые накладные расходы могут
быть упущен из виду. Даже тогда я бы сказал, что что-то не так с этим
подход. При внедрении системы электронной коммерции никто не определяет отдельный
тип для каждого вида продукта и его вариации и, возможно, возможные
настройки.

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

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

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


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

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

@крекер

Из того, что я прочитал, C# JIT выполняет специализацию во время выполнения для каждого типа значения и один раз для всех ссылочных типов. Нет специализации времени компиляции (IL-time).

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

Я думаю, что общая система С# подойдет для go, если вместо этого мы будем генерировать код во время компиляции.
Генерация кода во время выполнения в смысле С# невозможна с go, потому что go не является виртуальной машиной.

@dc0d

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

Можете ли вы уточнить немного.

@sighoya Моя ошибка; Я имел в виду не бинарный дистрибутив, а бинарные пакеты - лично я понятия не имею, насколько это важно.

@creker Хороший итог! (MO) Если не будет найдена веская причина, следует избегать любой формы перегрузки языковых конструкций Go. Одной из причин использования повторного связывания псевдонимов типов является избежание перегрузки встроенных составных типов, таких как срезы или карты.

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

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

Есть три категории имен, которые мы должны придумывать ежедневно:

  • Для объектов домена/логики
  • Типы/логика рабочего процесса программы
  • Сервисы/Типы данных интерфейса/Логика

Сколько раз программисту удавалось избежать именования чего-либо в своем коде?

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

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

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

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

Вы не возражаете, если я попытаюсь реализовать их в своем gomacro- интерпретаторе Go?

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

  1. ныне заброшенный язык , который я создал, когда был наивен :) Он транспилировался в исходный код C
  2. Common Lisp с моей библиотекой cl-parametric-types — он также поддерживает частичную и полную специализацию универсальных типов и функций.

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

Только начал над этим работать. Вы можете следить за прогрессом на https://github.com/cosmos72/gomacro/tree/generics-v1 .

На данный момент я начинаю с (слегка измененного) сочетания третьего и четвертого предложений Яна, перечисленных на https://github.com/golang/proposal/blob/master/design/15292-generics.md#Proposal .

@cosmos72 По ссылке ниже есть сводка предложений. Ваша смесь одна из них?
https://docs.google.com/document/d/1vrAy9gMpMoS3uaVphB32uVXX4pi-HnNjkMEgyAHX4N4

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

На данный момент я перехожу к технике «специализации типов», используемой в C++, Rust и других, возможно, с небольшим количеством «параметризованных областей шаблонов», потому что наиболее общий синтаксис Go для новых типов — это type ( Foo ...; Bar ...) , и я расширяю это до template[T1,T2...] type ( Foo ...; Bar ...) .
Кроме того, я держу дверь открытой для «Ограниченной специализации».

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

Смесь, о которой я говорил, находится между https://github.com/golang/proposal/blob/master/design/15292/2013-10-gen.md и https://github.com/golang/proposal/blob/ .

Обновление: чтобы избежать рассылки спама по этой официальной проблеме Go после первоначального объявления, вероятно, лучше продолжить обсуждение, относящееся к gomacro, в выпуске gomacro # 24: добавить дженерики.

Обновление 2: первые функции шаблона скомпилированы и успешно выполнены. См. https://github.com/cosmos72/gomacro/tree/generics-v1 .

Просто для записи можно перефразировать мое мнение (о дженериках и перепривязке псевдонимов типов):

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

@dc0d
Но разве шаблоны C++ не являются функцией компилятора и языка?

@sighoya Последний раз, когда я профессионально писал на C ++, это было примерно в 2001 году. Так что я могу ошибаться. Но если предположить, что последствия именования точны - часть «шаблона» - да (или, скорее, нет); это может быть функция компилятора (а не функция языка), сопровождаемая некоторыми языковыми конструкциями, которые, скорее всего, не перегружают какие-либо языковые конструкции, задействованные в системе типов.

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

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

type BinaryTreeOfStrings struct {
    left, right *BinaryTreeOfStrings;
    string content;
}

// Its methods here

type BinaryTreeOfBigInts struct {
    left, right *BinaryTreeOfBigInts;
    uint64 content;
}

// AGAIN the same methods but different type

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

Пожалуйста, обрати внимание:

  • Да, конечный код будет дублироваться. Прямо как если бы мы использовали генератор. И двоичный файл был бы больше.
  • Да, идея не оригинальная, а заимствованная из C++.
  • Да, функции MyTypeне вовлечение чего-либо с типом T (прямо или косвенно) также будет повторяться. Это может быть оптимизировано (например, методы, которые ссылаются на что-то типа T , кроме указателя на объект, принимающий сообщение, будут генерироваться для каждого T ; методы, содержащие вызовы методов, которые генерироваться для каждого T , также будет генерироваться для каждого T , рекурсивно - в то время как методы, в которых единственная ссылка на T - это *T в получателе, и другие методы, вызывающие только эти безопасные методы и удовлетворяющие тем же критериям, могут быть созданы только один раз). В любом случае, ИМО, этот момент большой и менее важный: я был бы вполне счастлив, даже если бы этой оптимизации не существовало.
  • По моему мнению, аргументы типа должны быть явными. Особенно, когда объект удовлетворяет потенциально бесконечным интерфейсам. Опять же: генератор кода.

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

Было бы неудачно, если бы Go пошел по пути C++. Многие люди рассматривают подход C++ как беспорядок, который настроил программистов против самой идеи дженериков: сложность отладки, отсутствие модульности, раздувание кода. Все решения «генератора кода» на самом деле представляют собой просто подстановку макросов — если вы хотите писать код именно так, зачем нам вообще нужна поддержка компилятора?

@andrewcmyers У меня было это предложение Type Alias ​​Rebinding, в котором мы пишем только обычные пакеты и вместо явного использования interface{} мы просто используем его как type T = interface{} в качестве общего параметра на уровне пакета. И это все.

  • Мы отлаживаем его как обычный пакет — это реальный код, а не какое-то промежуточное существо с периодом полураспада.
  • Нет необходимости вмешиваться в систему типов Go на всех уровнях — подумайте только о присваиваемости.
  • Это явно. Никакого скрытого моджо. Конечно, невозможность беспрепятственно связывать общие вызовы может показаться недостатком. Я вижу в этом прорыв! Смена типа в двух последовательных вызовах, в одном операторе не гойиш (ИМО).
  • И самое главное, он обратно совместим с серией Go 1.x (x >= 8).

Хотя идея не нова, способ, которым Go позволяет ее реализовать, прагматичен и ясен.

Дополнительный бонус: в Go нет перегрузки операторов. Но если определить значение псевдонима типа по умолчанию как (например) type T = int , не единственные допустимые типы, которые можно использовать для настройки этого универсального пакета, — это числовые типы, которые имеют внутреннюю реализацию для + оператор.

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

Теперь было бы очень некрасиво использовать любую явную нотацию для универсального типа, у которого есть параметр, реализующий интерфейсы Error и Stringer , а также числовой тип, поддерживающий оператор + !

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

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

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

Я прочитаю все вышеизложенное, обещаю, и все же я добавлю немного - GoLang SDK для Apache Beam кажется довольно ярким примером / демонстрацией проблем, с которыми должен столкнуться разработчик библиотеки, чтобы осуществить что-либо _правильно_ на высоком уровне.

Существует как минимум две экспериментальные реализации дженериков Go. Ранее на этой неделе я провел некоторое время с (1). Я был рад обнаружить, что влияние кода на удобочитаемость было минимальным. И я обнаружил, что использование анонимных функций для проверки равенства работает хорошо; поэтому я убежден, что перегрузка оператора не нужна. Единственная проблема, которую я обнаружил, заключалась в обработке ошибок. Распространенная идиома «вернуть ноль, ошибиться» не будет работать, если типом является, скажем, целое число или строка. Есть несколько способов обойти это, и все они сопряжены со сложностью. Я могу быть немного странным, но мне нравится обработка ошибок в Go. Таким образом, я пришел к выводу, что универсальное решение Go должно иметь универсальное ключевое слово для нулевого значения типа. Компилятор просто заменит его нулем для числовых типов, пустой строкой для строковых типов и nil для структур.

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

Было неплохо использовать тот же код алгоритма для целых чисел и что-то вроде Point:

type Point struct {
    x,y int
}

См. (2) мои тесты и наблюдения.

(1) https://github.com/albrow/fo; другой - вышеупомянутый https://github.com/cosmos72/gomacro#generics
(2) https://github.com/mandolyte/fo-experiments

@mandolyte Вы можете использовать *new(T) , чтобы получить нулевое значение любого типа.

Языковая конструкция, такая как default(T) или zero(T) (первая
в C# IIRC) было бы ясно, но OTOH длиннее, чем *new(T) (хотя и больше
исполнитель).

06.07.2018 9:15 GMT-05:00 Том Торогуд уведомления@github.com :

@mandolyte https://github.com/mandolyte Вы можете использовать *new(T), чтобы получить
нулевое значение любого типа.


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

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

19642 для обсуждения общего нулевого значения

@tmthrgd Почему-то я пропустил этот маленький кусочек. Спасибо!

прелюдия

Обобщения — это специализация настраиваемых конструкций. Три категории специализации:

  • Специализированные типы, Type<T> — массив _;
  • Специализированные вычисления, F<T>(T) или F<T>(Type<T>) — _сортируемый массив_;
  • Специализированная нотация, например _LINQ_ — операторы select или for в Go;

Конечно, есть языки программирования, которые представляют еще более общие конструкции. Но обычные языки программирования, такие как _C++_, _C#_ или _Java_, предоставляют более или менее языковые конструкции, ограниченные этим списком.

мысли

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

Вторая категория универсальных типов/конструкций должна _воздействовать_ на _свойство_ параметра типа. Например, _сортируемый массив_ должен иметь возможность _сравнивать_ _сопоставимые свойства_ своих элементов. Предполагая, что T.(P) является свойством T , а A(T.(P)) является вычислением/действием, которое воздействует на это свойство, (A, .(P)) может применяться либо к каждому отдельному элементу или быть объявленным как специализированное вычисление, переданное исходному настраиваемому вычислению. Примером последнего случая в Go является интерфейс sort.Interface , который также имеет аналогичную отдельную функцию sort.Reverse .

Третья категория универсальных типов/конструкций — это языковые нотации _type-specialized_ — похоже, это не Go вещь _вообще_.

вопросы

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

Приветствуются любые отзывы, более описательные, чем смайлики!

@ dc0d Я бы порекомендовал изучить «Элементы программирования» Сепанова, прежде чем пытаться определить дженерики. TL;DR заключается в том, что мы пишем конкретный код для начала, скажем, алгоритм, который сортирует массив. Позже мы добавим другие типы коллекций, такие как B-дерево и т. д. Мы заметили, что пишем много копий алгоритма сортировки, которые по существу одинаковы, поэтому мы определяем некоторое понятие, скажем, «сортируемый». Теперь мы хотим классифицировать алгоритмы сортировки, возможно, по шаблону доступа, который они требуют, скажем, только вперед, однократный проход (поток), только вперед многократный проход (односвязный список), двунаправленный (двухсвязный список), произвольный доступ (поток). множество). Когда мы добавляем новый тип коллекции, нам нужно только указать, к какой категории «координат» он относится, чтобы получить доступ ко всем соответствующим алгоритмам сортировки. Эти категории алгоритмов очень похожи на интерфейсы Go. Я хотел бы расширить интерфейсы в Go для поддержки нескольких параметров типа и абстрактных/связанных типов. Я не думаю, что функции нуждаются в специальной параметризации типа.

@ dc0d В качестве попытки разбить дженерики на составные части я раньше не рассматривал 3, «специализированное обозначение», как отдельную часть. Возможно, его можно охарактеризовать как определение DSL с использованием ограничений типа.

Я мог бы возразить, что ваши 1 и 2 - это «структуры данных» и «алгоритмы» соответственно. С этой терминологией становится немного яснее, почему может быть трудно четко разделить их, поскольку они часто сильно зависят друг от друга. Но sort.Interface — довольно хороший пример того, как можно провести границу между хранилищем и поведением (с небольшим количеством недавнего сахара, чтобы сделать его лучше), поскольку он кодирует требования Indexable и Comparable в минимальное поведение , необходимое для реализации алгоритма сортировки. с «обменом» и «меньше» (и len). Но это, похоже, не работает на более сложных структурах данных, таких как деревья или кучи, которые в настоящее время требуют некоторых искажений, чтобы отобразить чистое поведение в виде интерфейсов Go.

Я мог бы представить относительно небольшое добавление дженериков к интерфейсам (или что-то еще), которое могло бы позволить реализовать большинство структур данных и алгоритмов из учебников относительно чисто без искажений (как сегодня sort.Interface), но не было бы достаточно мощным для разработки DSL. Хотим ли мы ограничить себя такой ограниченной реализацией дженериков, когда мы вообще собираемся добавить дженерики — это другой вопрос.

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

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

(ПО МОЕМУ МНЕНИЮ)

@infogulch

Возможно, его можно охарактеризовать как определение DSL с использованием ограничений типа.

Ты прав. Но так как они являются частью набора языковых конструкций, ИМО называет их DSL немного неточными.

1 и 2...часто сильно зависят

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

sort.Interface — довольно хороший пример того, как можно провести границу между _хранилищем_ и _поведением_

Хорошо сказано;

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

Это один из моих вопросов: обобщить параметр типа и описать его в терминах ограничений (например, List<T> where T:new, IDisposable ) или предоставить обобщенный _протокол_, применимый ко всем элементам (набора; определенного типа)?

@киан

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

Истинный. Доступ по индексу — это _свойство_ среза (или массива). Таким образом, первое требование к сортируемому контейнеру (или _tree_-able контейнеру, каким бы ни был алгоритм _tree_) — предоставить утилиту _access & mutate (swap)_. Второе требование — элементы должны быть сопоставимы. Это запутанная часть (для меня) того, что вы называете алгоритмами: требования должны выполняться с обеих сторон (в контейнере и в параметре типа). В том-то и дело, что я не могу представить прагматичную реализацию дженериков в Go. Каждая сторона проблемы может быть идеально описана с точки зрения интерфейсов. Но как объединить эти два в эффективной нотации?

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

В примере с сортировкой Ord — это свойство типа, хранящегося в контейнере, а не самого контейнера. Шаблон доступа является свойством контейнера. Апаттерны простого доступа — это «итераторы», но это название происходит от C++, Степанов предпочитал «координаты», поскольку их можно применять к более сложным многомерным контейнерам.

Пытаясь определить сортировку, мы хотим что-то вроде этого:

bubble_sort : forall T U I => T U -> T U requires
   ForwardIterator<T>, Readable<T>, Writable<T>,
   Ord<U>,  ValueType(T) == U, Distance type(T) == I

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

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

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

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

@киан
Я думал, что это то, что вы имели в виду. Но в контексте Go это, вероятно, означало бы необходимость значительного пересмотра того, что могут определять интерфейсы. Я полагаю, что различные встроенные операции, такие как итерации или операторы, должны быть представлены через методы, чтобы такие вещи, как sort.Slice или math.Max , стали универсальными для интерфейсов.

Таким образом, вам нужно будет поддерживать следующий интерфейс (псевдокод):

type [T] OrderedIterator interface {
   Len() int
   ValueAt(i int) *T
}

...
package sort

func [T] Slice(s [T]OrderedIterator, func(i, j int) bool) {
   ...
}

и тогда все слайсы будут иметь эти методы?

@urandom Итератор - это не абстракция коллекции, а абстракция ссылки/указателя на коллекцию. Например, прямой итератор может иметь единственный метод «преемник» (иногда «следующий»). Возможность доступа к данным в месте нахождения итератора не является свойством итератора (в противном случае вы получите варианты чтения/записи/изменения итератора). Лучше всего определять «ссылки» отдельно как интерфейсы Readable, Writable и Mutable:

type T ForwardIterator interface {
   type DistanceType D
   successor(x T) T
}

type T Readable interface {
   type ValueType U 
   source(x T) U
}

Примечание. Тип «T» — это не срез, а тип итератора на срезе. Это может быть просто указатель, если мы примем стиль C++ передачи начального и конечного итераторов таким функциям, как sort.

Для итератора с произвольным доступом мы получим что-то вроде:

type T RandomIterator interface {
   type DistanceType D
   setPosition(x DistanceType)
}

Итак, итератор/координата — это абстракция ссылки на коллекцию, а не сама коллекция. Название «координата» очень хорошо выражает это, если вы думаете об итераторе как о координате, а о коллекции как о карте.

Разве мы не продаем Go, не используя закрытие функций и анонимные функции? Наличие функций/методов в качестве типа первого класса в Go может помочь. Например, используя синтаксис albrow/fo , пузырьковая сортировка может выглядеть так:

type SortableContainer[C,T] struct {
    Less func(C,T,T) bool
    Swap func(C,int,int)
    Next func(C) (T,bool)
}

func (bs *SortableContainer[C,T]) BubbleSort(container C, e1,e2 T) {    
    swapCount := 1
    var item1, item2 T
    item1, ok1 = bs.Next()
    if !ok1 {return}
    item2, ok2 = bs.Next()
    if !ok2 {return}
    for swapCount > 0 {
        swapCount = 0
        for {
            if Less(item2, item1) { 
                bs.Swap(C,item2,item1)
                swapCount += 1
            }
        }
    }
}

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

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

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

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

type I ForwardIterator interface {
   successor(x I) I
}
type R V Readable interface {
   source(x R) V
}
type V Ord interface {
   less(x V, y V) : bool
}

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

Это тесно связано с концепцией «функциональной зависимости». Если тип функционально зависит от другого типа, он должен быть абстрактным/ассоциированным типом. Только если эти два типа независимы, они должны быть параметрами отдельных типов.

Некоторые проблемы:

  1. Невозможно использовать текущий синтаксис f(x I) для многопараметрических интерфейсов. Мне все равно не нравится, что этот синтаксис путает интерфейсы (которые являются ограничениями для типов) с типами.
  2. Потребуется способ объявить параметризованные типы.
  3. Должен быть способ объявить связанные типы для интерфейсов с заданным набором параметров типа.

@keean Не уверен, что понимаю, как и почему количество интерфейсов становится таким большим. Вот полный рабочий пример: https://play.folang.org/p/BZa6BdsfBgZ (на основе фрагментов, а не общего контейнера, поэтому метод Next() не требуется).

Он использует только одну структуру типа, вообще без интерфейсов. Я должен предоставить все анонимные функции и замыкания (в этом, наверное, компромисс?). В примере используется один и тот же алгоритм пузырьковой сортировки для сортировки как среза целых чисел, так и среза точек "(x,y)", где расстояние от начала координат является основой функции Less().

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

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

Сортировка среза — решаемая задача. Вот код фрагмента quicksort.go , реализованный с использованием языка go-li (улучшенный golang) .

func main(){
    var data = []int{5,3,1,8,9}

    Sort(data, func(a *int, b *int) int {
        return *a - *b
    })

    fmt.Println(data)
}

Вы можете поэкспериментировать с этим на детской площадке .

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

@ go-li Я уверен, что вы можете отсортировать кусочек, было бы немного плохо, если бы вы не могли. Суть в том, что обычно вы хотели бы иметь возможность сортировать любой линейный контейнер с одним и тем же кодом, чтобы вам приходилось писать алгоритм сортировки только один раз, независимо от того, какой контейнер (структуру данных) вы сортируете, и независимо от того, какой содержание есть.

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

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

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

@urandom Предполагая, что мы не собираемся делать это «на месте», что было бы небезопасно, вам нужна функция карты, которая имеет «итератор чтения» одного типа и «итератор записи» другого типа, который можно определить примерно так:

map<I, O, U>(first I, last I, out O, fn U) requires
   ForwardIterator<I>, Readable<I>,
   ForwardIterator<O>, Writable<O>,
   UnaryFunction<U>, Domain(U) == ValueType(I), Codomain(U) == ValueType(O)

Для ясности «ValueType» — это связанный тип интерфейсов «Readable» и «Writable», «Domain» и «Codomain» — это связанные типы интерфейса «UnaryFunction». Очевидно, очень помогает, если компилятор может автоматически создавать интерфейсы для типов данных, таких как «UnaryFunction». Хотя этот вид выглядит как отражение, это не так, и все это происходит во время компиляции с использованием статических типов.

@keean Как смоделировать эти ограничения для чтения и записи в контексте текущих интерфейсов Go?

Я имею в виду, когда у нас есть тип A и мы хотим преобразовать его в тип B , сигнатура этой UnaryFunction будет func (input A) B (верно?), но как это может быть быть смоделированы с использованием только интерфейсов и как этот общий map (или filter , reduce и т. д.) будет смоделирован, чтобы сохранить конвейер типов?

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

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

Итак, если мы определим:

ValueType MyIntArrayIterator -> Int

С функциями немного сложнее, но у функции есть тип, например:

fn(x : Int) Float

Мы бы написали такой тип:

Int -> Float

Важно понимать, что -> — это просто конструктор инфиксного типа, например '[]' для массива — это конструктор типа, мы могли бы так же легко написать это;

Fn Int Float
Or
Fn<Int, Float>

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

Domain  Fn<Int, Float> -> Int
Codomain Fn<Int, Float> -> Float

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

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

Спасибо, @kean.

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

Эти «семейства типов» будут переноситься во время выполнения в случае некоторого контекста ошибки?

Как насчет пустых интерфейсов, переключателей типов и отражения?


РЕДАКТИРОВАТЬ: Мне просто любопытно, не жалуюсь.

@giovanisouza92 giovanisouza92 ну, никто не обязал Go использовать дженерики, поэтому я ожидаю скептицизма. Мой подход заключается в том, что если вы собираетесь делать дженерики, вы должны делать это правильно.

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

f(x I, y I) requires ForwardIterator<I>

Против

f(x ForwardIterator, y ForwardIterator)

Обратите внимание, что в последнем случае «x» и «y» могут быть разными типами, которые удовлетворяют интерфейсу ForwardIterator, тогда как в первом синтаксисе «x» и «y» должны быть одного типа (который удовлетворяет прямому итератору). Это важно для того, чтобы функции не были недостаточно ограничены и позволяли распространять конкретные типы гораздо дальше во время компиляции.

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

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

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

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

Двунаправленный итератор (в синтаксисе T func (*T) *[2]*T ) имеет тип func (*) *[2]* в синтаксисе go-li. В словах он принимает указатель на некоторый тип и возвращает указатель на два указателя на следующий и предыдущий элемент того же типа. Это фундаментальный конкретный базовый тип, используемый двусвязным списком .

Теперь вы можете написать то, что вы называете картой, то, что я называю универсальной функцией foreach. Не заблуждайтесь, это работает не только со связанным списком, но и со всем, что предоставляет двунаправленный итератор!

func Foreach(link func(*) *[2]*, list **, direction byte, f func(*)) {

    if nil == *list {
        return
    }

    var end *
    end = *list

    var e *
    e = (*link(*list))[direction]
    f(end)

    for (e != end) && ((*link(e))[direction] != nil) {
        var newe = (*link(e))[direction]
        f(e)
        e = newe
    }
    return
}

Foreach можно использовать двумя способами: вы используете его с лямбдой в итерации, подобной циклу for, над элементами списка или коллекции.

const forward = 1
const backwards = 0
Foreach(iterator, collection, forward, func(element *element_type){
    // do something with every element
})

Или вы можете использовать его для функционального сопоставления функции с каждым элементом коллекции.

Foreach(iterator, collection, backwards, function_to_be_mapped_on_elements)

Двунаправленный итератор, конечно, также может быть смоделирован с использованием интерфейсов в go 1.
interface Iterator { Iter() [2]Iterator } Вам нужно смоделировать его с помощью интерфейсов, чтобы обернуть («упаковать») базовый тип. Затем пользователь-итератор type утверждает известный тип, как только он находит и хочет посетить определенный элемент коллекции. Это потенциально небезопасно во время компиляции.

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

func modern(x func  (*) *[2]*, y func  (*) *[2]*){}

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

func modern_T_syntax<T>(x func  (*T) *[2]*T, y func  (*T) *[2]*T){}

То же, что и выше, но с использованием знакомого синтаксиса заполнителя типа T

func legacy(x Iterator, y Iterator){}

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

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

Итератор и связанный список — две стороны одной медали. Доказательство: любая коллекция, предоставляющая итератор, просто рекламирует себя как связанный список. Допустим, вам нужно отсортировать это. Что?

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

Другое решение состоит в том, чтобы быстро за O(N) выполнить цикл по итератору, собрать указатели на элементы в срез универсальных указателей, обозначенный как []*T , и отсортировать эти универсальные указатели с помощью плохой сортировки срезов.

Пожалуйста, дайте шанс идеям других людей

@go-li Если мы хотим избежать синдрома «не изобретено здесь», мы должны обратиться к Алексу Степанову за определением, поскольку он в значительной степени изобрел универсальное программирование. Вот как я бы определил это, взятое из Степанова «Элементы программирования», стр. 111:

Bidirectional iterator<T> =
    ForwardIterator<T>
/\ predecessor : T -> T
/\ predecessor takes constant time
/\ (forall i in T) successor(i) is defined =>
        predecessor(successor(i)) is defined and equals i
/\ (forall i in T) predecessor(i) is defined =>
        successor(predecessor(i)) is defined and equals i

Это зависит от определения ForwardIterator:

ForwardIterator<T> =
    Iterator<T>
/\ regular_unary_function(successor)

По сути, у нас есть интерфейс, который объявляет функцию successor и функцию predecessor вместе с некоторыми аксиомами, которым они должны соответствовать, чтобы быть действительными.

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

id(x T) T

Возможно, стоит также упомянуть о разнице между параметрическим типом и универсально квантифицированным типом. Параметрический тип будет id<T>(x T) T , тогда как универсальный квантор — id(x T) T (в этом случае мы обычно опускаем самый внешний универсальный квантор forall T ). С параметрическими типами система типов должна иметь тип для T, предоставленный на сайте вызова для id , с универсальной количественной оценкой, которая не требуется, пока T унифицируется с конкретным типом до завершения компиляции. Другой способ понять, что параметрическая функция — это не тип, а шаблон для типа, и это допустимый тип только после того, как T был заменен на конкретный тип. Благодаря универсальной количественной оценке функция id фактически имеет тип forall T . T -> T , который может быть передан компилятором точно так же, как Int .

@го-ли

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

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

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

Я бы оспорил любое утверждение, что универсальное программирование было изобретено C++. Прочитайте Liskov et al. Документ CACM 1977 года, если вы хотите увидеть раннюю модель универсального программирования, которая действительно работает (типобезопасная, модульная, без раздувания кода): https://dl.acm.org/citation.cfm?id=359789 (см. Раздел 4). )

Я думаю, что мы должны прекратить это обсуждение и подождать, пока команда golang (russ) выложит несколько сообщений в блоге, а затем реализует решение 👍 (см. vgo). Они просто сделают это 🎉

https://peter.bourgon.org/blog/2018/07/27/a-response-about-dep-and-vgo.html

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

Эта ветка показывает, что основная команда не заинтересована в активном участии в поиске решения с сообществом.

Но, в конце концов, если они снова смогут найти решение сами, я не против, просто сделайте это 👍

@andrewcmyers Ну, может быть, «изобретенный» был немного натянутым, это, вероятно, больше похоже на Дэвида Массера в 1971 году, который позже работал со Степановым над некоторыми универсальными библиотеками для Ады.

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

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

Было бы неудачно, если бы Go пошел по пути C++.

@andrewcmyers Да, я полностью согласен, пожалуйста, не используйте C ++ для предложений по синтаксису или в качестве эталона для правильного выполнения действий. Вместо этого взгляните на D для вдохновения .

@nomad-программное обеспечение

Мне очень нравится D, но нужны ли go мощные функции метапрограммирования времени компиляции, которые предлагает D?

Мне тоже не нравится синтаксис шаблона в C++, вытекающий из каменного века.

А как насчет обычного ParametricTypeстандарт, найденный в Java или C#, при необходимости его также можно перегрузить с помощью ParametricType

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

@nomad-software Я не предполагал, что синтаксис C++ или механизм шаблонов — правильный способ создания дженериков. Более того, «концепции», определенные Степановым, рассматривают типы как алгебру, что очень правильно для создания дженериков. Посмотрите на классы типов Haskell, чтобы увидеть, как это может выглядеть. Классы типов Haskell семантически очень близки к шаблонам и концепциям C++, если вы понимаете, что происходит.

Итак, +1 за несоблюдение синтаксиса С++ и +1 за нереализованную систему шаблонов, небезопасную для типов :-)

@keean Причина использования синтаксиса D состоит в том, чтобы полностью избежать <,> и придерживаться контекстно-свободной грамматики. Это часть моей идеи использовать D в качестве вдохновения. <,> — действительно плохой выбор для синтаксиса универсальных параметров.

@nomad-software Как я указал выше (в теперь скрытом комментарии), вам нужно указать параметры типа для параметрических типов, но не для универсальных количественных типов (отсюда разница между Rust и Haskell, способ обработки типов на самом деле отличается в системе типов). Также концепции C++ == классы типов Haskell == интерфейсы Go, по крайней мере, на концептуальном уровне.

Является ли синтаксис D действительно предпочтительным:

auto add(T)(T lhs, T rhs) {
    return lhs + rhs;

Почему это лучше, чем стиль C++/Java/Rust:

T add<T>(T lhs, T rhs) {
    return lhs + rhs;
}

Или стиль Scala:

T add[T](T lhs, T rhs) {
    return lhs + rhs;
}

Я немного подумал о синтаксисе параметров типа. Я никогда не был поклонником "угловых скобок" в C++ и Java, потому что они усложняют синтаксический анализ и тем самым мешают разработке инструментов. Квадратные скобки на самом деле являются классическим выбором (из CLU, System F и других ранних языков с параметрическим полиморфизмом).

Однако синтаксис Go довольно обидчив, возможно, потому, что он и так лаконичен. Возможный синтаксис, основанный на квадратных скобках или круглых скобках, создает грамматическую двусмысленность даже хуже, чем та, которую вводят угловые скобки. Поэтому, несмотря на мою предрасположенность, угловые скобки кажутся лучшим выбором для Go. (Конечно, есть и настоящие угловые скобки, которые не создают никакой двусмысленности — ⟨⟩ — но требуют использования символов Unicode).

Конечно, точный синтаксис, используемый для параметров типа, менее важен, чем правильная семантика . С этой точки зрения язык C++ — плохая модель. Работа моей исследовательской группы над дженериками в Genus (PLDI 2015) и Familia (OOPSLA 2017) предлагает другой подход, расширяющий классы типов и объединяющий их с интерфейсами.

@andrewcmyers Я думаю, что обе эти статьи интересны, но я бы сказал, что это не очень хорошее направление для Go, поскольку Genus объектно-ориентирован, а Go — нет, а Familia объединяет подтипы и параметрический полиморфизм, а в Go нет ни того, ни другого. Я думаю, что Go должен просто принять либо параметрический полиморфизм, либо универсальную количественную оценку, ему не нужны подтипы, и, на мой взгляд, это лучший язык, если его нет.

Я думаю, что Go должен искать дженерики, которые не требуют объектной ориентации и не требуют подтипов. В Go уже есть интерфейсы, которые, как мне кажется, являются прекрасным механизмом для дженериков. Если вы видите, что интерфейсы Go == понятия c++ == классы типов Haskell, мне кажется, что способ добавить дженерики, сохраняя при этом вкус «Go», заключается в расширении интерфейсов для приема нескольких параметров типа (я бы как и связанные типы в интерфейсах, но это может быть отдельным расширением, помогающим принять несколько параметров типа). Это было бы ключевым изменением, но для его включения потребуется «альтернативный» синтаксис для интерфейсов в сигнатурах функций, чтобы вы могли получить параметры нескольких типов для интерфейсов, и именно здесь появляется весь синтаксис угловых скобок. .

Интерфейсы Go — это не классы типов — это просто типы — но унификация интерфейсов с классами типов — это то, что Familia показывает способ сделать. Механизмы Genus и Familia не привязаны к полностью объектно-ориентированным языкам. Интерфейсы Go уже делают Go «объектно-ориентированным» в том смысле, в котором это имеет значение, поэтому я думаю, что идеи можно адаптировать в слегка упрощенной форме.

@andrewcmyers

Интерфейсы Go — это не классы типов — это просто типы

Для меня они не ведут себя как типы, так как допускают полиморфизм. Объект в полиморфном массиве, таком как Addable[], по-прежнему имеет свой фактический тип (видимый при отражении во время выполнения), поэтому они ведут себя точно так же, как классы типов с одним параметром. Тот факт, что они помещаются вместо типа в сигнатурах типов, является просто сокращенной нотацией, опускающей переменную типа. Не путайте нотацию с семантикой.

f(x : Addable) == f<T>(x : T) requires Addable<T>

Эта идентичность, конечно, действительна только для интерфейсов с одним параметром.

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

ПО МОЕМУ МНЕНИЮ:

Тем не менее, наличие типа по умолчанию было бы предпочтительнее, чем DSL, предназначенный для выражения ограничений типа. Например, иметь функцию f(s T fmt.Stringer) , которая является общей функцией, которая принимает любой тип, который также/удовлетворяет интерфейсу fmt.Stringer .

Таким образом, можно иметь общую функцию, например:

func add(a, b T int) T int {
    return a + b
}

Теперь функция add() работает с любым типом T , который, например, int поддерживает оператор + .

@ dc0d Я согласен, что это выглядит привлекательно, если посмотреть на текущий синтаксис Go. Однако он не является «полным» в том смысле, что он не может представлять все ограничения, необходимые для дженериков, и все равно будет толчок для его дальнейшего расширения. Это приведет к распространению различных синтаксисов, которые, на мой взгляд, противоречат цели простоты. Я считаю, что простота не проста, она должна быть самой простой, но при этом предлагать необходимую выразительную силу. В настоящее время я вижу, что основным ограничением универсальной выразительной силы Go является отсутствие многопараметрических интерфейсов. Например, интерфейс Collection может быть определен следующим образом:

type T U Collection interface {
   member(c T, v U) Bool
   insert(c T, v U) T
}

Так это имеет смысл, верно? Мы хотели бы писать интерфейсы поверх таких вещей, как коллекции. Итак, вопрос в том, как вы используете этот интерфейс в функции. Мое предложение будет примерно таким:

func[T, U] f(c T, e U) (Bool, T) requires Collection[T, U] {
   a := member(c, e)
   d := insert(c, e)
   return a, d
}

Синтаксис — это всего лишь предложение, однако я не возражаю против синтаксиса, если вы можете выразить эти концепции на языке.

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

type Collection interface (T interface{}, U interface{}) {
   member(c T, v U) Bool
   insert(c T, v U) T
}

Теперь часть (T interface{}, U interface{}) помогает определить ограничения. Например, если члены должны удовлетворять fmt.Stringer , тогда определение будет таким:

type Collection interface (T fmt.Stringer, U fmt.Stringer) {
   member(c T, v U) Bool
   insert(c T, v U) T
}

@ dc0d Это снова будет ограничительным в том смысле, что вы хотите ограничить более чем одним параметром типа, рассмотрите:

type OrderedCollection[T, U] interface
   requires Collection[T, U], Ord[U] {...}

Я думаю, что вижу, откуда вы пришли с размещением параметров, вы могли бы:

type OrderedCollection interface(T, U)
   requires Collection(T, U), Ord(U) {...}

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

@keean Давайте рассмотрим интерфейс heap.Interface . Текущее определение в стандартной библиотеке:

type Interface interface {
    sort.Interface
    Push(x interface{}) // add x as element Len()
    Pop() interface{}   // remove and return element Len() - 1.
}

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

type Interface interface (T interface{}) {
    sort.Interface
    Push(x T) // add x as element Len()
    Pop() T   // remove and return element Len() - 1.
}

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

Наличие типов по умолчанию позволяет нам писать универсальный код, который можно использовать с кодом в стиле Go 1.x. А стандартная библиотека может стать универсальной, ничего не нарушая. Это большая победа, ИМО.

@ dc0d , значит, вы предлагаете постепенное улучшение? То, что вы предлагаете, кажется мне хорошим постепенным улучшением, однако оно все еще имеет ограниченную общую выразительную силу. Как бы вы реализовали интерфейсы «Collection» и «OrderedCollection»?

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

@keean Я не понимаю часть requires Collection[T, U], Ord[U] . Как они ограничивают параметры типа T и U ?

@ dc0d Они работают так же, как и функции, но применяются ко всему. Таким образом, для любой пары типов TU, которые являются OrderedCollection, мы требуем, чтобы TU также был экземпляром Collection, а U — Ord. Таким образом, везде, где мы используем OrderedCollection, мы можем использовать методы из Collection и Ord по мере необходимости.

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

type OrderedCollection interface(T, U)
{
   first(c T) U
}

func[T] first(c T[]) T requires Collection(T[], T), Ord T
{...}

func[T] f(c T[]) requires OrderedCollection(T[], T), Collection(T[], T), Ord(T)
{...}

Но это может быть более читаемым:

type OrderedCollection interface(T, U) 
   requires Collection(T, U), Ord(U)
{
   first(c T) U
}

func[T] first(c T[]) T
{...}

func[T] f(c T[]) requires OrderedCollection(T[], T)
{...}

@keean (IMO) Пока для параметров типа существует обязательное значение по умолчанию, я чувствую себя счастливым. Таким образом, можно поддерживать обратную совместимость с сериями кода Go 1.x. Это главное, что я пытался сделать.

@киан

Интерфейсы Go — это не классы типов — это просто типы

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

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

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

@andrewcmyers

Да, они допускают полиморфизм подтипов.

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

https://docs.microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravariance

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

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

Поскольку Go имеет информацию о типах во время выполнения, нет необходимости в экзистенциальной квантификации. В Haskell типы распакованы (как нативные типы C), и это означает, что как только мы поместили что-то в экзистенциальную коллекцию, мы не можем (легко) восстановить тип содержимого, все, что мы можем сделать, это использовать предоставленные интерфейсы (классы типов ). Это реализуется путем хранения указателя на интерфейсы вместе с необработанными данными. В Go вместо этого хранится тип данных, данные «упакованы» (как в упакованных и распакованных данных С#). Таким образом, Go не ограничивается только интерфейсами, хранящимися с данными, потому что можно (с помощью регистра типов) восстановить тип данных в коллекции, что возможно только в Haskell путем реализации «Отражение». typeclass (хотя выводить данные неудобно, можно сериализовать тип и данные, скажем, строки, а затем десериализовать вне экзистенциальной коробки). Таким образом, я пришел к выводу, что интерфейсы Go ведут себя точно так же, как классы типов, если бы Haskell предоставил класс типов «Reflection» в качестве встроенного. Как такового экзистенциального блока нет, и мы по-прежнему можем регистрировать содержимое коллекций, но интерфейсы ведут себя точно так же, как классы типов. Разница между Haskell и Go заключается в семантике коробочных и неупакованных данных, а интерфейсы представляют собой классы типов с одним параметром. По сути, когда «Go» рассматривает интерфейс как тип, на самом деле он делает следующее:

Addable[] == exists T . T[] requires Addable[T], Reflection[T]

Вероятно, стоит отметить, что это то же самое, как «Объекты признаков» работают в Rust.

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

Идите, насколько я знаю, инвариантно, нет ни ковариации, ни контравариантности, это сильно говорит о том, что это не подтип.

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

Могу я предположить, что вы оба правы? В этом интерфейсы эквивалентны классам типов, но классы типов являются формой подтипа. Определения подтипов, которые я нашел до сих пор, довольно расплывчаты и неточны и сводятся к тому, что «A является подтипом B, если одно можно заменить другим». Что, ИМО, можно довольно легко утверждать, что оно удовлетворяется классами типов .

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

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

@Merovius Variance — это свойство, связанное с созданием подтипов. В языках без подтипов различий нет. Для того, чтобы была дисперсия, в первую очередь, вы должны иметь подтипы, что вводит проблему ковариации/контравариантности в конструкторы типов. Однако вы правы в том, что в языке с подтипами все конструкторы типов могут быть инвариантными.

Классы типов определенно не являются подтипами, потому что класс типов не является типом. Однако мы можем рассматривать «типы интерфейса» в Go как то, что Rust называет «объектом типажа», фактически тип, производный от класса типов.

Семантика Go, похоже, на данный момент соответствует любой модели, потому что у нее нет дисперсии и у нее есть неявные «объекты признаков». Так что, возможно, Go находится в переломном моменте, дженерики и система типов могут развиваться по пути создания подтипов, введения дисперсии и, в конечном итоге, чего-то вроде дженериков в C#. В качестве альтернативы Go мог бы ввести интерфейсы с несколькими параметрами, позволяя использовать интерфейсы для коллекций, и это разорвало бы непосредственную связь между интерфейсами и «типами интерфейсов». Например, если у вас есть:

type (T, U) Collection interface {
    member : (c T, e U) Bool
    insert: (c T, e U) T
}

member(c int32[], e int32) Bool {...}
insert(c int32[], e int32) int32[] {...}

member(c float32[], e float32) Bool {...}
insert(c float32[], e float32) float32[] {...}

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

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

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

Что касается того, есть ли в Go подтипы, рассмотрите следующий код:

package main

type Cloneable interface {
    Clone() Cloneable
}

type CloneableZ interface {
    Clone() Cloneable
    zero() int
}

type S struct {}

func (t S) Clone() Cloneable {
    c := t
    return c
}

func (t S) zero() int {
    return 0
}

var x CloneableZ = S{}
var y Cloneable = x

func main() {
    print("ok\n")
}

Присваивание от x к y демонстрирует, что тип y может использоваться там, где ожидается тип x . Это отношение подтипа, а именно: CloneableZ <: Cloneable , а также S <: CloneableZ . Даже если вы объясните интерфейсы с точки зрения классов типов, здесь все равно будет иметь место отношение подтипов, что-то вроде S <: ∃T.CloneableZ[T] <: ∃T.Cloneable[T] .

Обратите внимание, что для Go было бы совершенно безопасно позволить функции Clone возвращать S , но Go навязывает излишне ограничительные правила для соответствия интерфейсам: на самом деле, те же правила, что и в Java. изначально соблюдалось. Как заметил @Merovius , для подтипирования не требуются конструкторы неинвариантных типов.

@andrewcmyers Что происходит с многопараметрическими интерфейсами, например, необходимыми для абстрактных коллекций?

Кроме того, присваивание от x к y можно рассматривать как демонстрацию наследования интерфейса без подтипа вообще. В Haskell (у которого явно нет подтипов) вы бы написали:

class Cloneable t => CloneableZ t where...

Где у нас есть x , это тип, который реализует CloneableZ , который по определению также реализует Cloneable , поэтому, очевидно, может быть присвоен y .

Чтобы попытаться обобщить, вы можете либо рассматривать интерфейс как тип и Go, чтобы иметь ограниченное подтипирование без ковариантных или контравариантных конструкторов типов, либо вы можете рассматривать его как «объект признака», или, возможно, в Go мы бы назвали его " объект интерфейса», который фактически является полиморфным контейнером, ограниченным интерфейсом «класса типов». В модели классов типов нет подтипов, и поэтому нет причин думать о ковариантности и контравариантности.

Если мы придерживаемся модели подтипов, у нас не может быть типов коллекций, вот почему C++ пришлось ввести шаблоны, поскольку объектно-ориентированного подтипа недостаточно для общего определения таких понятий, как контейнеры. В итоге у нас есть два механизма абстракции: объекты и подтипы, а также шаблоны/черты и дженерики, и взаимодействие между ними становится сложным, посмотрите примеры на C++, C# и Scala. Будут продолжены призывы ввести ковариантные и контравариантные конструкторы для увеличения мощности дженериков в соответствии с этими другими языками.

Если нам нужны универсальные коллекции без введения отдельной системы дженериков, то нам следует подумать об интерфейсах, таких как классы типов. Многопараметрические интерфейсы означают, что больше не нужно думать о подтипах, а вместо этого думать о наследовании интерфейсов. Если мы хотим улучшить дженерики в Go и разрешить абстракции таких вещей, как коллекции, и нам не нужна сложность систем типов таких языков, как C++, C#, Scala и т. д., тогда многопараметрические интерфейсы и наследование интерфейсов — это путь. идти.

@киан

Что происходит с многопараметрическими интерфейсами, например, необходимыми для абстрагирования коллекций?

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

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

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

Где у нас есть x, это тип, который реализует CloneableZ, который по определению также реализует Cloneable, поэтому, очевидно, может быть присвоен y.

Нет, в моем примере x — это переменная , а y — другая переменная. Если я знаю, что y — это некоторый тип CloneableZ , а x — это некоторый тип Cloneable , это не означает, что я могу присваивать от y до x. Это то, что делает мой пример.

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

type Cloneable interface {
    Clone() Cloneable
}

type CloneableZ interface {
    Clone() Cloneable
    zero() int
}

type S struct {}

func (t S) Clone() Cloneable {
    c := t
    return c
}

type T struct { x int }

func (t T) Clone() Cloneable {
    c := t
    return c
}

func (t S) zero() int {
    return 0
}

var x CloneableZ = S{}
var y Cloneable = T{}
var a [2]Cloneable = [2]Cloneable{x, y}

@andrewcmyers

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

Посмотрите, почему C++ разработал шаблоны, модель подтипов OO не была способна выражать общие понятия, необходимые для обобщения таких вещей, как коллекции. C# и Java также должны были ввести полную систему обобщений, отдельную от объектов, подтипов и наследования, а затем должны были навести порядок в сложных взаимодействиях двух систем с такими вещами, как ковариантные и контравариантные конструкторы типов. Оглядываясь назад, мы можем избежать объектно-подтипов и вместо этого посмотреть, что произойдет, если мы добавим интерфейсы (классы типов) к просто типизированному языку. Это то, что сделал Rust, поэтому на это стоит взглянуть, но, конечно, это сложно из-за всей жизни. В Go есть GC, поэтому такой сложности не будет. Мое предложение состоит в том, что Go можно расширить, чтобы разрешить многопараметрические интерфейсы и избежать этой сложности.

Что касается вашего утверждения, что вы не можете выполнить этот пример в Haskell, вот код:

{-# LANGUAGE ExistentialQuantification #-}

class ICloneable t where
    clone :: t -> t

class ICloneable t => ICloneableZ t where
    zero :: t

data S = S deriving Show

instance ICloneable S where
    clone x = x

data T = T Int deriving Show

instance ICloneable T where
    clone x = x

instance ICloneableZ T where
    zero = T 0

data Cloneable = forall a . (ICloneable a, Show a) => ToCloneable a

instance Show Cloneable where
    show (ToCloneable x) = show x

main = do
    x <- return S
    y <- return (T 27)
    a <- return [ToCloneable x, ToCloneable y]
    putStrLn (show a)

Некоторые интересные отличия: Go автоматически выводит этот тип data Cloneable = forall a . (ICloneable a, Show a) => ToCloneable a , поскольку таким образом вы превращаете интерфейс (у которого нет хранилища) в тип (у которого есть хранилище), Rust также выводит эти типы и называет их «объектами свойств». . В других языках, таких как Java, C# и Scala, мы обнаружили, что вы не можете создавать экземпляры интерфейсов, что на самом деле «правильно», интерфейсы не являются типами, у них нет хранилища, Go автоматически выводит тип экзистенциального контейнера для вас, чтобы вы могли обрабатывать интерфейс похож на тип, и Go скрывает это от вас, давая экзистенциальному контейнеру то же имя, что и интерфейс, из которого он получен. Следует также отметить, что это [2]Cloneable{x, y} принуждает все члены к Cloneable , тогда как Haskell не имеет таких неявных приведения, и мы должны явно принуждать члены с помощью ToCloneable .

Мне также было указано, что мы не должны рассматривать S и T подтипы Cloneable , потому что S и T не являются конструктивно совместимы. Мы можем буквально объявить любой тип экземпляром Cloneable (просто объявив соответствующее определение функции clone в Go), и эти типы вообще не должны иметь никакого отношения друг к другу.

Большинство предложений для дженериков, похоже, включают дополнительные токены, которые, как мне кажется, ухудшают читабельность и простоту восприятия Go. Я хотел бы предложить другой синтаксис, который, по моему мнению, мог бы хорошо работать с существующей грамматикой Go (даже довольно хорошо подсвечивает синтаксис в Github Markdown).

Основные пункты предложения:

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

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

    • Неявность при правильном использовании может помочь языку стать менее многословным. Мы все время пропускаем объявления типов с := , потому что скрытая этим информация просто не настолько важна, чтобы ее каждый раз объяснять.

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

package main

import "fmt"

type LinkedList a struct {
  Head *Node a
  Tail *Node a
}

type Node a {
  Next *Node a
  Prev *Node a

  Value a
}

func main() {
  // Not sure about how recursive we could get with the inference
  ll := LinkedList string {
    // The string bit could be inferred
    Head: Node string { Value: "hello world" },
  }
}

func (l *LinkedList a) Append(value a) {
  newNode := &Node{Value: value}

  if l.Tail == nil {
    l.Head = newNode
    l.Tail = l.Head
    return
  }

  l.Tail.Next = newNode
  l.Tail = l.Tail.Next
}

Это взято из Gist, в котором есть немного больше деталей, а также предложенные здесь типы сумм: https://gist.github.com/aarondl/9b950373642fcf5072942cf0fca2c3a2 .

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

@аарондл
Для меня это нормально, используя этот синтаксис, мы бы получили:

type Collection a b interface {
   member(c a, e b) Bool
   insert(c a, e b) a
}

func insert(c *LinkedList a, e a) *LinkedList a {
   c.Append(e)
   return c
}

@keean Не могли бы вы немного объяснить тип Collection . Я не понимаю этого:

type Collection a b interface {
   member(c a, e b) Bool
   insert(c a, e b) a
}

@dc0d Коллекция — это интерфейс, абстрагирующий _все_ коллекции, то есть деревья, списки, срезы и т. д., поэтому у нас могут быть общие операции, такие как member и insert , которые будут работать с любой коллекцией, содержащей данные любого типа. Выше я привел пример определения вставки для типа LinkedList в предыдущем примере:

func insert(c *LinkedList a, e a) *LinkedList a {
   c.Append(e)
   return c
}

Мы могли бы также определить его для среза

func insert(c []a, e a) []a {
   return append(c, e)
}

Однако нам даже не нужны параметрические функции с переменными типа, как показано @aarondl с полиморфным типом a , чтобы это работало, поскольку вы можете просто определить для конкретных типов:

func insert(c *LinkedList int, e int) *LinkedList int {
   c.Append(e)
   return c
}

func insert(c *LinkedList float, e float) *LinkedList float {
   c.Append(e)
   return c
}

func insert(c int[], e int) int[] {
   return append(c, e)
}

func insert(c float[], e float) float[] {
   return append(c, e)
}

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

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

@aarondl Учитывая, что type LinkedList a уже является допустимым объявлением типа, я вижу только два способа сделать это разборчивым однозначно: сделать грамматику контекстно-зависимой (попадание в проблемы синтаксического анализа C, тьфу) или использовать неограниченный просмотр вперед ( чего старается избегать грамматика go из-за плохих сообщений об ошибках в случае сбоя). Я мог бы что-то неправильно понять, но ИМО говорит против подхода без токенов.

@keean Интерфейсы в Go используют методы, а не функции. В предложенном вами конкретном синтаксисе нет ничего, что связывало бы insert с *LinkedList для компилятора (в Haskell это делается с помощью объявлений instance ). Для методов также нормально изменять значение, над которым они работают. Ничто из этого не является Show-Stopper, просто указывает, что синтаксис, который вы предлагаете, плохо работает с Go. Скорее всего что-то вроде

type Collection e interface {
    Element(e) book
    Insert(e)
}

func (l *(LinkedList e)) Element(el e) book {
    // ...
}

func (l* (LinkedList e)) Insert(el e) {
    // ...
}

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

@aarondl , у меня есть еще вопросы по твоему предложению. Например, он не допускает ограничений, поэтому вы получаете только неограниченный полиморфизм. Что, в общем-то, не очень полезно, так как вам не разрешено что-либо делать с полученными значениями (например, вы не можете реализовать Collection с картой, так как не все типы являются допустимыми ключами карты). Что должно произойти, когда кто-то попытается сделать что-то подобное? Если это ошибка времени компиляции, жалуется ли она на создание экземпляра (сообщения об ошибках C++ впереди) или на определение (вы ничего не можете сделать в принципе, потому что нет ничего, что работало бы со всеми типами)?

@keean Тем не менее я не понимаю, как a может быть списком (или фрагментом, или любой другой коллекцией). Это контекстно-зависимая специальная грамматика для коллекций? Если да, то какова его ценность? Таким образом невозможно объявить пользовательские типы.

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

type Collection interface {
   type Element
   Member(e Element) Bool
   Insert(e Element) Collection
}

type IntSlice struct {
    value []Int,
}

type IntSlice.Element = Int

func (IntSlice) Member(e Int) Bool {...}
func (IntSlice) Insert(e Int) IntSlice {...}

func useIt(c Collection, e Collection.Element) {...}

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

func[A] useIt(c A, e A.Element) requires A:Collection

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

@dc0d a и b являются параметрами типа интерфейса, как и в классе типов Haskell. Чтобы что-то считалось Collection , оно должно определить методы, соответствующие типам в интерфейсе, где a и b могут быть любого типа. Однако, как указал @Merovius , интерфейсы Go основаны на методах и не поддерживают множественную отправку, поэтому интерфейсы с несколькими параметрами могут не подходить. С моделью метода с одной отправкой в ​​​​Go более подходящим вариантом будет наличие связанных типов в интерфейсах вместо нескольких параметров. Однако отсутствие множественной отправки затрудняет реализацию таких функций, как unify(x, y) , и вам приходится использовать шаблон двойной отправки, что не очень приятно.

Чтобы объяснить многопараметрическую вещь немного дальше:

type Cloneable[A] interface {
   clone(x A) A
}

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

func clone(x int) int {...}

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

func[A] test(x A) A requires Cloneable[A] {...}

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

type Cloneable interface {
   clone() Cloneable
}

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

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

@Merovius Спасибо, что рассмотрели предложение. Позвольте мне попытаться развеять ваши опасения. Мне грустно, что вы отвергли предложение, прежде чем мы обсудили его подробнее, я надеюсь, что смогу изменить ваше мнение - или, может быть, вы можете изменить мое :)

Неограниченный просмотр вперед:
Итак, как я упоминал в предложении, в настоящее время кажется, что грамматика Go имеет хороший способ обнаружения «конца» почти всего синтаксически. И мы все равно будем это делать из-за неявных универсальных аргументов. Однобуквенная строчная буква является синтаксической конструкцией, которая создает этот общий аргумент - или что бы мы ни решили сделать этот встроенный токен, может быть, мы даже откатимся к токенизированной вещи, такой как @a в предложении, если нам достаточно нравится синтаксис, но это не так. возможно, учитывая сложность компилятора без токенов, хотя предложение теряет много очарования, как только вы это делаете.

Несмотря на это, проблема с type LinkedList a в этом предложении не так уж сложна, потому что мы знаем, что a является аргументом универсального типа, и поэтому это приведет к ошибке компилятора, такой же, как type LinkedList сегодня завершается с ошибкой: prog.go:3:16: expected type, found newline (and 1 more errors) . Исходный пост на самом деле не вышел и не сказал этого, но вам больше не разрешено называть конкретный тип [a-z]{1} , который, я думаю, решает эту проблему и является жертвой, с которой, я думаю, мы все будем согласны. создание (сегодня я вижу только недостатки в создании реальных типов с однобуквенными именами в коде Go).

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

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

func Sort(slice []a, compare func (a, a) bool) { ... }

Вопросы о масштабе

Вот вы привели пример:

type Collection e interface {
    Element(e) book
    Insert(e)
}

func (l *(LinkedList e)) Element(el e) book {
    // ...
}

func (l* (LinkedList e)) Insert(el e) {
    // ...
}

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

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

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

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

В соответствии с этим предложением у нас по-прежнему будет та же проблема, что и сейчас, с такими операторами, как +

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

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

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

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

Истинный. Но довольно неэргономичный. Подумайте, сколько жалоб на sort API. Для многих универсальных контейнеров количество функций, которые вызывающая сторона должна реализовать и передать, кажется непомерно высокой. Подумайте, как будет выглядеть реализация container/heap в соответствии с этим предложением и чем она будет лучше текущей реализации с точки зрения эргономики? Казалось бы, выигрыши здесь в лучшем случае ничтожны. Вам нужно будет реализовать больше тривиальных функций (и дублировать/ссылаться на каждый сайт использования), а не меньше.

@Меровиус

думая об этом моменте от @aarondl

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

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

type Addable interface {
   + (x Addable, y Addable) Addable
}

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

type Addable[A] interface {
   + (x A, y A) A
}

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

Но, учитывая эти изменения, теперь вы можете написать:

type S struct {
   value: int
}

func (+) (x S, y S) S {
   return S {
      value: x.value + y.value
   }
}

func main() {
    println(S {value: 27} + S {value: 5})
}

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

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

Некоторые могут счесть это особенностью :) AFAIR есть какое-то предложение или ветка, обсуждающая, следует ли это делать. FWIW, я думаю, что это - снова - блуждание не по теме. Перегрузка операторов (или общие идеи «как сделать Go больше на Haskell») на самом деле не является предметом этой проблемы :)

Может ли множественная отправка (и, предположительно, перегрузка функций) когда-нибудь случиться в Go?

Никогда не говори никогда. Хотя лично я этого не ожидал.

@Меровиус

Кто-то может счесть это особенностью :)

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

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

Что-то вроде этого, казалось бы, правильно:

type Collection interface {
   type Element
   Member(e Element) Bool
   Insert(e Element) Collection
}

type IntSlice struct {
    value []Int,
}

type IntSlice.Element = Int

func (IntSlice) Member(e Int) Bool {...}
func (IntSlice) Insert(e Int) IntSlice {...}

func useIt<T>(c T, e T.Element) requires T:Collection {...}

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

f(useIt) // not okay with parametric types
f(useIt<List>) // okay with parametric types

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

Интересно, SFINAE упоминается только @bcmills. Даже не упоминается в предложении (хотя Sort есть в качестве примера).
Как тогда может выглядеть Sort для slice и linkedlist?

@киан
Я не могу понять, как можно определить общую коллекцию «Срез» с вашим предложением. Кажется, вы определяете «IntSlice», который может реализовывать «Collection» (хотя Insert возвращает тип, отличный от того, который требуется интерфейсу), но это не общий «срез», поскольку он кажется только для целых чисел , а реализации метода предназначены только для целых чисел. Нужно ли нам определять конкретную реализацию для каждого типа?

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

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

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

также учитывая проблемы синтаксического анализа с предложением @aarondl .

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

@urandom

Я не могу понять, как можно определить общую коллекцию «Срез» с вашим предложением.

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

type<T> []T.Element = Int

func<T> ([]T) Member(e T) Bool {...}
func<T> ([]T) Insert(e T) Collection {...}

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

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

@Меровиус

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

Если вы можете справиться с программой из 100 000 строк, то вы сможете сделать больше со 100 000 строк общего характера, чем со 100 000 неуниверсальных строк (из-за повторения). Таким образом, вы можете быть суперзвездным разработчиком, способным справляться с очень большими кодовыми базами, но вы все равно добьетесь большего с очень большой универсальной кодовой базой, поскольку устраните избыточность. Эта универсальная программа расширится до еще более крупной неуниверсальной программы. Мне просто кажется, что вы еще не достигли своего предела сложности.

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

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

Я согласен с этим.

вы сможете сделать больше со 100 000 общих строк, чем со 100 000 необобщенных строк (из-за повторения)

Мне любопытно, исходя из вашего гипотетического примера, какой % этих строк будет общей функцией?
По моему опыту, это менее 2% (из кодовой базы с 115 тыс. LOC), поэтому я не думаю, что это хороший аргумент, если вы не пишете библиотеку для «коллекций».

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

@киан

Что касается вашего утверждения, что вы не можете выполнить этот пример в Haskell, вот код:

Этот код морально не эквивалентен коду, который я написал. Он представляет новый тип оболочки Cloneable в дополнение к интерфейсу ICloneable. Код Go не нуждался в оболочке; как и другие языки, поддерживающие подтипы.

@andrewcmyers

Этот код морально не эквивалентен коду, который я написал. Он представляет новый тип оболочки Cloneable в дополнение к интерфейсу ICloneable.

Разве это не то, что делает этот код:

type Cloneable interface {...}

Он вводит тип данных Cloneable, полученный из интерфейса. Вы не видите «ICloneable», потому что у вас нет объявлений экземпляров для интерфейсов, вы просто объявляете методы.

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

@keean Я бы посчитал Cloneable просто типом, а не «типом данных». В таком языке, как Java, абстракция Cloneable практически не требует дополнительных затрат, потому что не будет никакой оболочки, в отличие от вашего кода.

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

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

потому что не будет никакой оболочки, в отличие от вашего кода.

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

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

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

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

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

@keean Я не понимаю твоего комментария. Сначала вы говорите мне, что не согласны и что я «просто еще не достиг своего предела сложности», а затем вы говорите мне, что согласны (в этом «необходимость» — слишком сильное слово). Я также думаю, что ваш аргумент ошибочен (вы предполагаете, что LOC является основной мерой сложности и что все строки кода равны). Но самое главное, я не думаю, что «кто пишет более сложные программы» действительно продуктивная линия обсуждения. Я просто пытался уточнить, что аргумент «если вы не согласны со мной, это должно означать, что вы не работаете над такими сложными или интересными проблемами» неубедителен и не выглядит добросовестным. Я надеюсь, что вы можете просто поверить, что люди могут не согласиться с вами по поводу важности этой функции, оставаясь при этом столь же компетентными и занимаясь не менее интересными вещами.

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

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

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

@Merovius Спасибо за продолжение диалога.

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

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

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

Меня на самом деле это совершенно не волнует. Я не думаю, что количество функций будет запредельным, но я определенно готов увидеть некоторые противоположные примеры. Напомним, что API, на который жаловались люди, был не тем, для которого вам нужно было предоставить функцию, а исходным здесь: https://golang.org/pkg/sort/#Interface , где вам нужно было создать новый тип, который был просто ваш слайс + тип, а затем реализуйте на нем 3 метода. В свете жалоб и проблем, связанных с этим интерфейсом, было создано следующее: https://golang.org/pkg/sort/#Slice , у меня, например, нет проблем с этим API, и мы бы восстановили потери производительности этого в соответствии с предложением, которое мы обсуждаем, просто изменив определение на func Slice(slice []a, less func(a, a) bool) .

С точки зрения структуры данных container/heap , независимо от того, какое универсальное предложение вы принимаете, оно требует полной перезаписи. container/heap , так же как и пакет sort , просто предоставляет алгоритмы поверх вашей собственной структуры данных, но ни один из пакетов никогда не владеет структурой данных, потому что в противном случае у нас было бы []interface{} и расходы, связанные с этим. Предположительно, мы бы изменили их, поскольку вы могли бы иметь Heap , которому принадлежит слайс с конкретным типом, благодаря дженерикам, и это верно для любого из предложений, которые я видел здесь (включая мое собственное) .

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

Рассмотрим следующее определение хеш-таблицы:

// Hasher turns a key into a hash
type Hasher interface {
  func Hash() []byte
}

type HashTable v struct {
   Keys   []Hasher
   Values []v
}

// Note that the generic arguments must be repeated here and immediately
// understood without reading another line of code, which to me
// is a readability win over the sudden appearance of the K and V which are
// defined elsewhere in the code in the example below. This is of course because
// the tokenized type declarations with constraints are fairly painful in general
// and repeating them everywhere is simply too much.
func (h (*HashTable v)) Insert(key Hasher, value v) { ... }

Говорим ли мы, что []Hasher не является стартовым из-за проблем с производительностью/хранением и что для успешной реализации Generics в Go у нас обязательно должно быть что-то вроде следующего?

// Without selecting another proposal I have no idea how the constraint might be defined or implemented so let's just pretend
type [K: Hasher, V] HashTable a struct {
   Keys   []K
   Values []V
}

func (h *HashTable) Insert(key K, value V) { ... }

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

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

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

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

В свете жалоб и боли, связанной с этим интерфейсом, было создано следующее: https://golang.org/pkg/sort/#Slice .

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

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

мы бы восстановили потери производительности в соответствии с обсуждаемым нами предложением, просто изменив определение на func Slice(slice []a, less func(a, a) bool).

Хотя это старый API. Вы говорите: «Мое предложение не допускает ограниченного полиморфизма, но это не проблема, потому что мы можем просто не использовать дженерики и вместо этого использовать существующие решения (отражение/интерфейсы)». Что ж, ответ на «ваше предложение не допускает самые основные варианты использования, для которых людям нужны дженерики» словами «мы можем просто делать то, что люди уже делают, без дженериков для этих самых основных вариантов использования», похоже, нас не доставит. где угодно, ТБХ. Предложение дженериков, которое не поможет вам написать даже базовые типы контейнеров, сортировку, макс... просто не стоит того.

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

Большинство предложений по дженерикам включают способ ограничения параметров типа. т.е. выразить "параметр типа должен иметь метод Less" или "параметр типа должен быть сопоставимым". Ваш - AFAICT - нет.

Рассмотрим следующее определение хеш-таблицы:

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

type hasherA uint64

func (a hasherA) Hash() []byte {
    b := make([]byte, 8)
    binary.BigEndian.PutUint64(b, uint64(a))
    return b
}

type hasherB string

func (b hasherB) Hash() []byte {
    return []byte(b)
}

h := new(HashTable int)
h.Insert(hasherA(42), 1)
h.Insert(hasherB("Hello world"), 2)

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

type HashTable k v struct {
    Keys []k
    Values []v
}

func (h *(HashTable k v)) Insert(key k, value v) {
    // You can't actually do anything with k, as it's unconstrained. i.e. you can't hash it, compare it…
    // Implementing this is impossible in your proposal.
}

// If it weren't impossible, you'd get this:
h := new(HashTable hasherA int)
h[hasherA(42)] = 1
h[hasherB("Hello world")] = 2 // compile error - can't use hasherB as hasherA

Или, если это поможет, представьте, что вы пытаетесь реализовать набор хэшей. Вы получите ту же проблему, но теперь в полученном контейнере нет дополнительной проверки типов по сравнению с interface{} .

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

что для успешной реализации Generics в Go у нас обязательно должно быть что-то вроде следующего?

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

@Merovius Хорошо. Кажется, я понял, чего ты хочешь. Имейте в виду, что ваши варианты использования очень далеки от того, что я хочу. На самом деле я не испытываю зуда в отношении безопасных контейнеров, хотя я подозреваю, как вы сказали, что это может быть мнение меньшинства. Некоторые из самых больших вещей, которые я хотел бы видеть, — это типы результатов вместо ошибок и простое манипулирование срезами без дублирования или отражения везде, что мое предложение делает разумную работу по решению. Тем не менее, я вижу, как с вашей точки зрения это «не касается самых основных вариантов использования», если ваш основной вариант использования — это написание универсальных контейнеров без использования интерфейсов,

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

Читая это, становится ясно, что вы совершенно неправильно поняли, как общие срезы будут/должны работать в соответствии с этим предложением. Именно из-за этого недоразумения вы пришли к ложному выводу, что «в вашем предложении этот интерфейс по-прежнему невозможен». При любом предложении должен быть возможен общий срез, вот что я думаю. И len() в мире, как я видел, будет определено как: func len(slice []a) , который является общим аргументом среза, означающим, что он может подсчитывать длину без отражения для любого среза. Как я уже сказал выше, это большая часть этого предложения (простое манипулирование срезами), и мне жаль, что я не смог хорошо передать это с помощью примеров, которые я привел, и сути, которую я сделал. Общий срез должен иметь возможность использоваться так же легко, как сегодня []int , я еще раз повторю, что любое предложение, которое не решает эту проблему (обмен срезами/массивами, присвоение, длина, ограничение и т. ) на мой взгляд маловато.

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

Лучший контрпример — функция сортировки, которую мы обсуждали ранее.

type Sort(slice []a:Lesser, less func(a:Lesser, a:Lesser)) { ... }

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

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

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

// Decorator style, follows the definition of the type thorugh all
// of it's methods.
<strong i="14">@a</strong>: Lesser, Hasher, Equaler
func Sort(slice []a) { ... }
<strong i="15">@k</strong>: Equaler, Hasher
type HashTable k v struct

// Inline, follows the definition of the type through
// all of it's methods.
func [a: Hasher, Equaler] Sort(slice []a) { ... }
type [k: Hasher, Equaler] HashTable k v struct

// File-scope global style, if k appears as a generic argument
// it's constrained by this that appears at the top of the file underneath
// the imports but before any other code.
<strong i="16">@k</strong>: Equaler, Hasher

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

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

func map(slice []a, mapper func(a) b) {
  for i := range slice {
    slice[i] = mapper(slice[i])
  }
}

type Result a b struct {
  Ok  a
  Err b
}

@aarondl Я попробую объяснить. Причина, по которой вам нужны ограничения типа, заключается в том, что это единственный способ вызова функций или методов для типа. рассмотрим неограниченный тип a , какой это может быть тип, ну, это может быть строка, или Int, или что-то еще. Поэтому мы не можем вызывать для него какие-либо функции или методы, потому что мы не знаем тип. Мы могли бы использовать переключатель типа и отражение во время выполнения, чтобы получить тип, а затем вызвать для него некоторые функции или методы, но этого мы хотим избежать с помощью дженериков. Когда вы ограничиваете тип, например, a является Animal, мы можем вызвать любой метод, определенный для животного на a .

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

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

func m(slice []a) []b {
   mapper := func(x a) b {...}
   return map(slice, mapper)
}

Какие функции мы можем вызвать для x при попытке определить mapper ?

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

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

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

@аарондл
Спасибо, я исправил синтаксис, однако, думаю, смысл был понятен.

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

Тогда я сомневаюсь, является ли ограниченная общность, возможная без таких больших изменений в языке, хорошей идеей, учитывая сложность, которую она добавит. Может быть, Go лучше оставаться простым, и люди могут добавлять макросы, такие как предварительная обработка, или другие языки, которые компилируются в Go, чтобы обеспечить эти функции? С другой стороны, добавление параметрического полиморфизма было бы хорошим первым шагом. Разрешение ограничивать параметры типа было бы хорошим следующим шагом. Затем вы можете добавить параметры ассоциированного типа к интерфейсам, и у вас будет что-то достаточно общее, но это, вероятно, все, что вы можете получить без множественной отправки. Разбивая на отдельные более мелкие функции, я думаю, вы увеличили бы шанс их принятия?

@киан
Нужна ли множественная отправка? Очень немногие языки изначально поддерживают его. Даже C++ не поддерживает его. C# как бы поддерживает его через dynamic , но я никогда не использовал его на практике, а ключевое слово вообще очень и очень редко встречается в реальном коде. Примеры, которые я помню, касаются чего-то вроде разбора JSON, а не написания дженериков.

Нужна ли множественная отправка?

ИМХО, я думаю, что @keean говорит о статической множественной отправке, предоставляемой классами типов/интерфейсами.
Это даже предусмотрено в C++ путем перегрузки методов (я не знаю для C#)

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

Можно ли указать тип как «просто» параметр?

func Append(t, t2 type, arr []t, value t2) []t {
    v := t(value) // conversion
    return append(arr, v)
}

var arr []float64
v := 0

arr = Append(float64, int, arr, v)

@Инуарт написал:

Можно ли указать тип как «просто» параметр?

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

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

func Append(arr []t, value s) []t  requires Convertible<s,t>{
    v := t(value) // conversion
    return append(arr, v)
}

var arr []int64
v := 0.5

arr = Append(arr, v)

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

func convert(value s) t requires Convertible<s,t>{
    return t(value);
}

f:float64:=2.0

i:int64=convert(f)

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

Я понимаю, что нотация Convertible<s,t> необходима для безопасности во время компиляции, но может быть снижена до проверки во время выполнения.

func Append(t, t2 type, arr []t, value t2) []t {
    v, ok := t(value) // conversion
    if !ok {
        panic(...) // or return an err
    }
    return append(arr, v)
}

var arr []float64
v := 0

arr = Append(float64, int, arr, v)

Но это больше похоже на синтаксический сахар для reflect .

@Inuart Дело в том, что компилятор может проверить тип, реализующий класс типов, во время компиляции, поэтому проверка во время выполнения не нужна. Преимущество заключается в лучшей производительности (так называемая абстракция с нулевой стоимостью). Если это проверка во время выполнения, вы также можете использовать reflect .

@крекер

Нужна ли множественная отправка?

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

type Equals<T> interface {eq(right T) bool}
(left I) eq(right I) bool {return left == right}
(left I) eq(right F) bool {return false}
(left F) eq(right I) bool {return false}
(left F) eq(right F) bool {return left == right}

func main() {
    x := []Equals<?>{I{2}, F{4.0}, I{2}, F{4.0}}
}

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

data Equals = forall a . IEquals a a => Equals a

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

data Equals = forall a b . IEquals a b => Equals a

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

Однако это очень упрощает расширение с помощью нового типа:

(left K) eq(right I) bool {return false}
(left K) eq(right F) bool {return false}
(left I) eq(right K) bool {return false}
(left F) eq(right K) bool {return false}
(left K) eq(right K) bool {return left == right}

И это было бы еще более лаконичным с экземплярами по умолчанию или специализацией.

С другой стороны, мы можем переписать это на «Go», который работает прямо сейчас:

package main

type I struct {v int}
type F struct {v float32}

type EqualsInt interface {eqInt(left I) bool}
func (right I) eqInt (left I) bool {return left == right}
func (right F) eqInt (left I) bool {return false}

type EqualsFloat interface {eqFloat(left F) bool}
func (right I) eqFloat (left F) bool {return false}
func (right F) eqFloat (left F) bool {return left == right}

type EqualsRight interface {
    EqualsInt
    EqualsFloat
}

type EqualsLeft interface {eq(right EqualsRight) bool}
func (left I) eq (right EqualsRight) bool {return right.eqInt(left)}
func (left F) eq (right EqualsRight) bool {return right.eqFloat(left)}

type Equals interface {
    EqualsLeft
    EqualsRight
}

func main() {
    x := []Equals{I{2}, F{4.0}, I{2}, F{4.0}}
    println(x[0].eq(x[1]))
    println(x[1].eq(x[0]))
    println(x[0].eq(x[2]))
    println(x[1].eq(x[3]))
}

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

type EqualsRight interface {
    EqualsInt
    EqualsFloat
}

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

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

Моя основная проблема со многими предлагаемыми синтаксисами (синтаксисами?) Blah[E] заключается в том, что базовый тип не показывает никакой информации о содержании дженериков.

Например:

type Comparer[C] interface {
    Compare(other C) bool
}
// or
type Comparer c interface {
    Compare(other c) bool
}
...

Это означает, что мы объявляем новый тип, который добавляет больше информации к базовому типу. Разве смысл объявления type не в том, чтобы определить имя на основе другого типа?

Я бы предложил синтаксис больше по линии

type Comparer interface[C] {
    Compare(other C) bool
}

Это означает, что на самом деле Comparer — это просто тип, основанный на interface[C] { ... } , а interface[C] { ... } — это, конечно, отдельный тип от interface { ... } . Это позволяет вам использовать общий интерфейс, не называя его, если хотите (что разрешено для обычных интерфейсов). Я думаю, что это решение немного более интуитивно понятно и хорошо работает с системой типов Go, хотя, пожалуйста, поправьте меня, если я ошибаюсь.

Примечание. Объявление универсального типа допустимо только для интерфейсов, структур и функций со следующим синтаксисом:
interface[G] { ... }
struct[G] { ... }
func[G] (vars...) { ... }

Тогда «реализация» дженериков будет иметь следующий синтаксис:
interface[G] { ... }[string]
struct[G] { ... }[string]
func[G] (vars...) { ... }[int](args...)

И с некоторыми примерами, чтобы сделать это немного более понятным:

Интерфейсы

package add

type Adder interface[E] {
    // Adds the element and returns the size
    Add(elem E) int
}

// Adds the integer 5 to any implementation of Adder[int].
func AddFiveTo(a Adder[int]) int {
    return a.Add(5)
}

Структуры

package heap

type List struct[T] {
    slice []T
}

func (l *List) Add(elem T) { // T is a type defined by the receiver
    l.slice = append(l.slice, elem)
}

Функции

func[A] AddManyTo(a Adder[A], many ...A) {
    for _, each := range a {
        a.Add(each)
    }
}

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

Встраивание параметров типа не должно быть разрешено.

Учитывать

type X(type T C) struct {
  R // A regular type with method Foo()
  T // Some type parameter
}
// X defines some methods other than Foo(),
// some of which invoke Foo.

для некоторого произвольного типа R и некоторого произвольного контракта C , который не содержит Foo() .

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

Предположим, что Bar — это структура, допустимая для C , в которой есть поле с именем Foo .

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

Методы X(Bar) могут продолжать разрешать ссылки на Foo как X(Bar).R.Foo . Это делает возможным написание универсального типа, но может сбить с толку читателя, не знакомого с придирками к правилам разрешения. Вне методов X селектор останется неоднозначным, поэтому, хотя interface { Foo() } не зависит от параметров X , некоторые экземпляры X будут не удовлетворить его.

Запретить встраивание параметра типа проще.

(Однако если это разрешено, имя поля будет T по той же причине, что имя поля встроенного поля S , определенного как type S = io.Reader , равно S , а не Reader , но также и потому, что тип, создающий экземпляр T , вообще не обязательно должен иметь имя.)

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

Итак, дано:

type R struct(type T) {
    io.Reader
    T
}

методы на R не смогут вызывать Read на R без косвенного обращения через Reader. Например:

func (r R) Do() {
     r.Read(buf)     // Illegal
     r.Reader.Read(buf)  // ok
}

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

func (r R) Do() {
    var x interface{} = r
    x.(io.Reader)    // Succeeds
}

@рогпеппе

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

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

contract C(t T) {
  interface { Foo() } (X(T){})
  // ...
}

type X(type T C) struct {
  R // A regular type with method Foo()
  T // Some type parameter
}
// X defines some methods other than Foo(),
// some of which invoke Foo.

Это позволит напрямую вызывать Foo на X . Конечно, это противоречило бы правилу "запрещено использовать местные имена в контрактах"...

@stevenblekinsop Хм, возможно, хотя и неудобно, сделать это, не обращаясь к X

contract C(t T) {
  struct{ R; T }{}.Foo
}

C по-прежнему привязан к реализации X , хотя и немного более свободно.

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

func (x X(T)) Fooer() interface { Foo() } {
  return x
}

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

Было бы проще просто запретить это.

Я начал работать над этим предложением до того, как был объявлен проект Go2.

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

Он расширяет синтаксис более ранних предложений @ianlancetaylor , поскольку именно он был доступен, когда я начинал. Это не принципиально. Его можно заменить синтаксисом (type T и т. д. или чем-то эквивалентным. Мне просто нужен был некоторый синтаксис в качестве обозначения семантики.

Он находится здесь: https://gist.github.com/jimmyfrasche/656f3f47f2496e6b49e041cd8ac716e4 .

Правило должно заключаться в том, что любой метод, продвинутый из большей глубины, чем метод встроенного параметра типа, не может быть вызван, если (1) неизвестна идентичность аргумента типа или (2) не утверждается, что метод может быть вызван во внешнем интерфейсе. тип по контракту, ограничивающему параметр типа. Компилятор также может определить верхнюю и нижнюю границы глубины, которую продвинутый метод должен иметь во внешнем типе O , и использовать их, чтобы определить, можно ли вызывать метод для типа, включающего O , т.е. есть ли потенциал для конфликта с другими продвигаемыми методами или нет. Нечто подобное также применимо к любому параметру типа, который, как утверждается, имеет вызываемые методы, где диапазоны глубины методов в параметре типа будут [0, inf).

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

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

type Wrapper(type T) { T }

вы можете поставить *Wrapper(T) в интерфейс во всех случаях, если ваш контракт говорит, что он удовлетворяет интерфейсу.

Разве ты не можешь просто сделать

type Interface interface {
  SomeMethod(int) error
}

contract MightBeAPointer(t T) {
  Interface(t)
}

func Example(type T MightBeAPointer)(v T) {
  var i Interface = v
  // ...
}

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

type S struct{}
func (s *S) SomeMethod(int) error { ... }
...
var s S
Example(S)(s)

Это не сработает, потому что S нельзя преобразовать в Interface , можно только *S .

Очевидно, ответ может быть «не делай этого». Тем не менее, предложение контрактов описывает такие контракты, как:

contract Contract(t T) {
    var _ error = t.SomeMethod(int(0))
}

S удовлетворит этому контракту из-за автоматической адресации, как и *S . То, что я пытаюсь решить, — это разрыв в возможностях между вызовами методов и преобразованиями интерфейса в контрактах.

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

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

Учитывать:

contract Embeddable(type X, Y) {
    type S struct {
        X
        Y
    }
}

type Embedded(type First, Second Embeddable) struct {
        First
        Second
}

// Error: First and Second both provide method Read.
// That must be diagnosed to the Embeddable contract, not the definition of Embedded itself.
type Boom = Embedded(*bytes.Buffer, *strings.Reader)

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

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

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

Хм, хорошая мысль. Мне не хватает еще одного ограничения, чтобы вызвать ошибку.¹

contract Embeddable(type X, Y) {
    type S struct {
        X
        Y
    }
    var _ io.Reader = S{}
}

¹ https://play.golang.org/p/3wSg5aRjcQc

Для этого требуется, чтобы один из X или Y , но не оба, был io.Reader . Интересно, что система контрактов достаточно выразительна, чтобы позволить это. Я рад, что мне не нужно выяснять правила вывода типов для такого зверя.

Но проблема не в этом.

Это когда ты делаешь

type S (type T C) struct {
  io.Reader
  T
}
func (s *S(T)) X() io.Reader {
  return s
}

Это не должно компилироваться, потому что T может иметь селектор Read , если C не имеет

struct{ io.Reader; T }.Read

Но тогда каковы правила, когда C не гарантирует, что наборы селекторов не пересекаются, а S не ссылается на селекторы? Возможно ли, чтобы каждый экземпляр S удовлетворял интерфейсу, за исключением типов, создающих неоднозначный селектор?

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

Да, похоже, это так. Интересно, подразумевает ли это что-то более глубокое... 🤔

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

type I interface { /* ... */ }
a := G(A) // ok, A satisfies contract
var _ I = a // ok, no selector overlap
b := G(B) // ok, B satisfies contract
var _ = b // error, selector overlap

Меня беспокоят сообщения об ошибках, когда G0(B) использует файл G1(B) использует файл . . . использует Gn(B) , а Gn вызывает ошибку. . . .

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

// Error: Duplicate field name Reader
type Boom = Embedded(*bytes.Reader, *strings.Reader)

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

На самом деле это указано в эскизном проекте в разделе о параметризованных типах :

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

type Lockable(type T) struct {
    T
    mu sync.Mutex
}

func (l *Lockable(T)) Get() T {
    l.mu.Lock()
    defer l.mu.Unlock()
    return l.T
}

(Примечание: это плохо работает, если вы пишете Lockable(X) в объявлении метода: должен ли метод возвращать lT или lX? Возможно, нам следует просто запретить встраивание параметра типа в структуру.)

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

Одна вещь, которую я не смущаюсь сказать, это то, что 90% этой дискуссии выше моей головы.

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

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

Я не мог ошибиться больше.

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

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

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

И это то, что мне нравится в Go. Это очень быстро..... Учиться. Мы все здесь знаем о его возможностях производительности. Но скорость, с которой его можно выучить, не имеет себе равных среди 8 других языков, которые я выучил за эти годы.

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

Простота и скорость обучения. Это настоящие убийственные черты языка.

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

Итак, есть две вещи, которые, я надеюсь, команда Go сможет принять во внимание:

1) Какую повседневную проблему мы хотим решить?

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

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

2) Будьте проще, как и все другие замечательные функции Go

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

Возможно, аргумент компилятора для включения таких расширенных функций? '--хардкор'

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

И это то, что мне нравится в Go. Это очень быстро..... Учиться. Мы все здесь знаем о его возможностях производительности. Но скорость, с которой его можно выучить, не имеет себе равных среди 8 других языков, которые я выучил за эти годы.

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

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

У меня есть два основных варианта использования дженериков: типовое исключение сложных структур данных, таких как двоичные деревья, наборы и sync.Map , и возможность писать типобезопасные функции во время компиляции, которые работают на основе исключительно на функциональности их аргументов, а не на их расположении в памяти. Есть некоторые более причудливые вещи, которые я был бы не против сделать, но я был бы не против _не_ иметь возможность их делать, если невозможно добавить их поддержку, не нарушая полностью простоты языка.

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

@камстюарт

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

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

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

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

Общая реализация функции:

/*

* "generic" is a KIND of types, just like "struct", "map", "interface", etc...
* "T" is a generic type (a type of kind generic).
* var t = T{int} is a value of type T, values of generic types looks like a "normal" type

*/

type T generic {
    int
    float64
    string
}

func Sum(a, b T{}) T{} {
    return a + b
}

Вызывающий функцию:

Sum(1, 1) // 2
// same as:
Sum(T{int}(1), T{int}(1)) // 2

Общая реализация структуры:

type ItemT generic {
    interface{}
}

type List struct {
    l []ItemT{}
}

func NewList(t ItemT) *List {
    l := make([]t)
    return &List{l}
}

func (p *List) Push(item ItemT{}) {
    p.l = append(p.l, item)
}

Звонивший:

list := NewList(ItemT{int})
list.Push(42)

Как человек, который только изучает Swift и не любит его, но имеет большой опыт работы с другими языками, такими как Go, C, Java и т. д.; Я действительно считаю, что дженерики (или шаблоны, или как бы вы это ни называли) — не очень хорошая вещь для добавления в язык Go.

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

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

@jlubawy
Знаете ли вы другой способ, которым мне никогда не придется реализовывать связанный список или алгоритм быстрой сортировки? Как указывает Александр Степанов, большинство программистов не могут правильно определить функции «min» и «max», так что на что нам надеяться правильно реализовать более сложные алгоритмы без большого количества времени на отладку. Я скорее вытащу стандартные версии этих алгоритмов из библиотеки и просто применю к имеющимся у меня типам. Какая альтернатива есть?

@jlubawy

или шаблоны, или как вы хотите это назвать

Все зависит от реализации. если мы говорим о шаблонах C++, то да, их сложно понять вообще. Даже писать их сложно. С другой стороны, если мы возьмем дженерики C#, то это совсем другое дело. Сама концепция здесь не проблема.

Если вы не знали, команда Go Team анонсировала проект Go 2.0:
https://golang.org/s/go2designs

Существует черновик дизайна Generics в Go 2.0 (контракт). Вы можете взглянуть и оставить отзыв на их Wiki .

Это соответствующий раздел:

Дженерики

Прочитав черновик, спрашиваю:

Почему

Т: Добавляемый

означает "тип T, реализующий контракт Addable"? Зачем добавлять новый
концепция, когда у нас уже есть ИНТЕРФЕЙСЫ для этого? Назначение интерфейсов
проверено во время сборки, поэтому у нас уже есть средства, чтобы не нуждаться ни в
здесь дополнительное понятие. Мы можем использовать этот термин, чтобы сказать что-то вроде: Любой
тип T, реализующий интерфейс Addable. Кроме того, T:_ или T:Any
(любое специальное ключевое слово или встроенный псевдоним интерфейса {}) подойдет
Хитрость.

Просто я не знаю, зачем заново реализовывать большинство подобных вещей. Делает нет
смысла и БУДЕТ избыточным (поскольку избыточным является новая обработка ошибок относительно
преодоление паники).

2018-09-14 6:15 GMT-05:00 Коала Йенг, уведомления@github.com :

Если вы не знали, команда Go Team анонсировала проект Go 2.0:
https://golang.org/s/go2designs

Существует черновик дизайна Generics в Go 2.0 (контракт). Вы можете хотеть
посмотреть и оставить отзыв
https://github.com/golang/go/wiki/Go2GenericsFeedback на их вики
https://github.com/golang/go/wiki/Go2GenericsFeedback .

Это соответствующий раздел:

Дженерики


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

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

Изменить: «[...] поможет, ЕСЛИ ВАМ НЕ НУЖНО КОНКРЕТНОЕ ТРЕБОВАНИЕ НА
АРГУМЕНТ ТИПА».

2018-09-17 11:10 GMT-05:00 Луис Масуэлли [email protected] :

Прочитав черновик, спрашиваю:

Почему

Т: Добавляемый

означает "тип T, реализующий контракт Addable"? Зачем добавлять новый
концепция, когда у нас уже есть ИНТЕРФЕЙСЫ для этого? Назначение интерфейсов
проверено во время сборки, поэтому у нас уже есть средства, чтобы не нуждаться ни в
здесь дополнительное понятие. Мы можем использовать этот термин, чтобы сказать что-то вроде: Любой
тип T, реализующий интерфейс Addable. Кроме того, T:_ или T:Any
(любое специальное ключевое слово или встроенный псевдоним интерфейса {}) подойдет
Хитрость.

Просто я не знаю, зачем заново реализовывать большинство подобных вещей. Делает нет
смысла и БУДЕТ избыточным (поскольку избыточным является новая обработка ошибок относительно
преодоление паники).

2018-09-14 6:15 GMT-05:00 Коала Йенг, уведомления@github.com :

Если вы не знали, команда Go Team анонсировала проект Go 2.0:
https://golang.org/s/go2designs

Существует черновик дизайна Generics в Go 2.0 (контракт). Ты можешь
хочу посмотреть и оставить отзыв
https://github.com/golang/go/wiki/Go2GenericsFeedback на их вики
https://github.com/golang/go/wiki/Go2GenericsFeedback .

Это соответствующий раздел:

Дженерики


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

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

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

@luismasuelli-jobsity Если я правильно прочитал историю универсальных реализаций в Go, то похоже, что причина введения контрактов заключается в том, что они не хотели перегружать операторы в интерфейсах.

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

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

Почему T:Addable означает «тип T, реализующий контракт Addable»?

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

func Example(type K, V someContract)(k K, v V) V

вы можете сделать что-то вроде

contract someContract(k K, v V) {
  k.someMethod(v)
}

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

type someMethoder(V) interface {
  someMethod(V)
}

func Example(type K: someMethoder(V), V)(k K, v V) V

Это как-то неловко. Синтаксис контракта позволяет вам по-прежнему делать это, если вам нужно, потому что «аргументы» контракта автоматически заполняются компилятором, если в контракте их столько же, сколько параметров типа функции. Однако вы можете указать их вручную, если хотите, что означает, что вы _могли бы_ сделать func Example(type K, V someContract(K, V))(k K, v V) V , если бы действительно хотели, хотя в данной ситуации это не особенно полезно.

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

contract Example(k K, v V) {
  k.someMethod(v)
}

func Example(type K, V)(k K, v V) V

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

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

type E(Node) interface {
  Nodes() []Node
}

type N(Edge) interface {
  Edges() (from, to Edge)
}

type Graph(type Node: N(Edge), Edge: E(Node)) struct { ... }
func New(type Node: N(Edge), Edge: E(Node))(nodes []Node) *Graph(Node, Edge) { ... }
func (*Graph(Node, Edge)) ShortestPath(from, to Node) []Edge { ... }

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

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

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

Уточнения. По эскизному проекту договор может применяться как к функциям, так и к видам .

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

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

Вы не можете сделать следующее?

contract Example(t T, v V) {
  t.(interface{
    SomeMethod() V
  })
}

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

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

t.(someInterface) также означает, что это должен быть интерфейс

Хорошая точка зрения. Упс.

Чем больше примеров этого я вижу, тем более подверженным ошибкам кажется «выяснить это из тела функции».

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

Я также отмечу, что

contract I(t T) {
  var i interface { Foo() }
  i = t
  t.(interface{})
}

выражает, что T должен быть интерфейсом по крайней мере с Foo() , но также может иметь любое другое количество дополнительных методов.

T должен быть интерфейсом по крайней мере с Foo() , но также может иметь любое другое количество дополнительных методов.

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

contract Example(t T) {
  t + t
}

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

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

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

Привет
Я сделал предложение по ограничениям для дженериков, которое я разместил в этой теме около ½ года назад.
Теперь я сделал версию 2 . Основные изменения:

  • Синтаксис был адаптирован к предложенному go-team.
  • Ограничение по полям было опущено, что допускает некоторое упрощение.
  • Абзацы, которые не являются строго необходимыми, были удалены.

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

func Foo() interface{} {
    type S struct {}
    return S{}
}

func Bar(type T)() interface{} {
    type S struct {}
    return S{}
}

func Baz(type T)() interface{} {
    type S struct{t T}
    return S{}
}

func main() {
    fmt.Println(Foo() == Foo()) // 1
    fmt.Println(Bar(int)() == Bar(string)()) // 2
    fmt.Println(Baz(int)() == Baz(string)()) // 3
}
  1. Выводит true , потому что типы возвращаемых значений происходят из одного и того же объявления типа.
  2. Отпечатки…?
  3. Я предполагаю, что печатает false .

т.е. вопрос в том, когда два типа, объявленные в универсальной функции, идентичны, а когда нет. Я не думаю, что это описано в дизайне ~spec~? По крайней мере, я не могу найти его прямо сейчас :)

@merovius Я предполагаю, что средний случай должен был быть:

fmt.Println(Bar(int)() == Bar(int)()) // 2

Это интересный случай, и он зависит от того, являются ли типы «генеративными» или «аппликативными». На самом деле существуют варианты ML, которые используют разные подходы. Аппликативные типы рассматривают дженерик как функцию типа, и, следовательно, f(int) == f(int). Генеративные типы рассматривают универсальный тип как шаблон типа, который создает новый уникальный тип «экземпляр» каждый раз, когда он используется, поэтому t<int> != t<int>. К этому следует подходить на уровне всей системы типов, поскольку это имеет тонкие последствия для унификации, логического вывода и надежности. Для получения дополнительной информации и примеров таких проблем я рекомендую прочитать статью Андреаса Россберга «Модули F-ing»: https://people.mpi-sws.org/~rossberg/f-ing/ , хотя в статье говорится о ML " функторы» это потому, что ML разделяет свою систему типов на два уровня, а функторы являются ML эквивалентными обобщенным и доступны только на уровне модуля.

@kean Вы ошибаетесь.

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

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

С аппликативными типами «S» для каждого экземпляра будет одним и тем же типом, и поэтому они будут равны.

Было бы странно, если бы результат в случае 2 изменился в зависимости от оптимизации компилятора. Похоже на УБ.

Это 2018 год, люди, я не могу поверить, что мне действительно нужно печатать это, как в 1982 году:

функция min(x, y int) int {
если х < у {
вернуть х
}
вернуть у
}

функция max(x, y int) int {
если х > у {
вернуть х
}
вернуть у
}

Я имею в виду, серьезно, чуваки MIN(INT,INT) INT, как это НЕ в языке?
Я зол.

@ dataf3l Если вы хотите, чтобы они работали должным образом с предварительными заказами, то:

func min(x, y int) int {
   if x <= y {
      return x
   }
   return y
}

Это означает, что пара (min(x, y), max(x, y)) всегда различна и равна либо (x, y), либо (y, x), и, следовательно, это стабильный вид двух элементов.

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

Я думал о < vs <= для целых чисел, я не уверен, что вижу разницу.
Может я просто тупой...

Я не уверен, что вижу разницу.

В данном случае нет.

@cznic true в этом случае, поскольку они являются целыми числами, однако, поскольку ветка была посвящена дженерикам, я предположил, что комментарий библиотеки касается наличия универсальных определений min и max, поэтому пользователям не нужно объявлять их самостоятельно. Перечитывая ОП, я вижу, что им просто нужны простые минимальные и максимальные значения для целых чисел, так что это плохо, но они не по теме, запрашивая простые функции интеграции в ветке о дженериках :-)

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

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

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

@веберн Спасибо. См. https://go.googlesource.com/proposal/+/master/design/go2draft.md .

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

Спасибо.

@webern Запишите это (большинство людей используют gists для формата уценки) и обновите вики здесь https://github.com/golang/go/wiki/Go2GenericsFeedback

Многие другие уже сделали это.

Я объединил (вопреки последнему совету) и загрузил CL нашей реализации прототипа парсера (и принтера) до Gophercon, реализующего проект контракта. Если вам интересно попробовать синтаксис, посмотрите: https://golang.org/cl/149638 .

Чтобы поиграть с ним:

1) Вишневый выбор CL в недавнем репо:
git fetch https://go.googlesource.com/go refs/changes/38/149638/2 && git cherry-pick FETCH_HEAD

2) Пересоберите и установите компилятор:
установить cmd/скомпилировать

3) Используйте компилятор:
go инструмент для компиляции foo.go

Подробности смотрите в описании CL. Наслаждаться!

contract Addable(t T) {
    t + t
}

func Sum(type T Addable)(x []T) T {
    var total T
    for _, v := range x {
        total += v
    }
    return total
}

Этот дизайн дженериков, func Sum(type T Addable)(x []T) T , ОЧЕНЬ ОЧЕНЬ ОЧЕНЬ УЖАСНЫЙ!!!

По сравнению с func Sum(type T Addable)(x []T) T , я думаю, что func Sum<T: Addable> (x []T) T более понятен и не обременяет программиста, пришедшего из других языков программирования.

Вы имеете в виду, что синтаксис более подробный?
Должна быть какая-то причина, по которой это не func Sum(T Addable)(x []T) T .

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

@urandom Это проблема только во время создания экземпляра, и здесь нам не требуется ключевое слово type , а просто живем с двусмысленностью AIUI.

Проблема в том, что без ключевого слова type func Foo(x T) (y T) можно проанализировать либо как объявление универсальной функции, принимающей T и ничего не возвращающей, либо как неуниверсальную функцию, принимающую T и возвращает T .

функция Сумма(х [] Т) Т

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

Я также думаю, что это сделало бы код немного более простым (читай: менее Lisp-y) для синтаксического анализа для людей, а также уменьшило бы шансы столкнуться с какой-то неясной двусмысленностью синтаксического анализа в дальнейшем (см. C++ «Самый неприятный анализ», чтобы помочь мотивировать обилие осторожности).

Это 2018 год, люди, я не могу поверить, что мне действительно нужно печатать это, как в 1982 году:

функция min(x, y int) int {
если х < у {
вернуть х
}
вернуть у
}

функция max(x, y int) int {
если х > у {
вернуть х
}
вернуть у
}

Я имею в виду, серьезно, чуваки MIN(INT,INT) INT, как это НЕ в языке?
Я зол.

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

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

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

@bcmills спасибо, вы делаете сообщество лучше.

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

Определение generics кажется неизбежным при введении таких вещей, как type's type , что делает Go довольно сложным.

Надеюсь, это не слишком не по теме, но функции function overload мне кажется достаточно.

Кстати, я знаю, что была дискуссия о перегрузке .

@xgfone Согласитесь, что в языке уже так много скобок, что делает код непонятным.
func Sum<T: Addable> (x []T) T или func Sum<type T Addable> (x []T) T лучше и понятнее.

Для согласованности (со встроенными дженериками) func Sum[T: Addable] (x []T) T лучше, чем func Sum<T: Addable> (x []T) T .

Возможно, на меня повлияла предыдущая работа на других языках, но Sum<T: Addable> (x []T) T на первый взгляд кажется более отчетливым и читабельным.

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

FWIW, вероятность того, что Go будет использовать угловые скобки для дженериков, составляет примерно 0%. Грамматика C++ неразборчива, потому что вы не можете отличить <b>c (законную, но бессмысленную серию сравнений) от универсального вызова, не понимая типы a, b и c. По этой причине другие языки избегают использования угловых скобок для дженериков.

func a < b Addable> (...
Я думаю, вы можете, если понимаете, что после func у вас может быть только имя функции, ( или < .

@carlmjohnson Надеюсь, ты прав

f := sum<int>(10)

Но тут вы знаете, что sum — это контракт..

Грамматика C++ неразборчива, потому что вы не можете отличить <b>c (законную, но бессмысленную серию сравнений) от универсального вызова, не понимая типы a, b и c.

Я думаю, стоит отметить, что хотя Go, в отличие от C++, запрещает это в системе типов, поскольку операторы < и > возвращают bool в Go и < и > нельзя использовать с bool s, это _является_ синтаксически допустимым, так что это все еще проблема.

Другая проблема с угловыми скобками — List<List<int>> , в которой >> обозначается как оператор сдвига вправо.

Какие проблемы были с использованием [] ? Мне кажется, что большая часть из вышеперечисленного решается с их помощью:

  • Синтаксически f := sum[int](10) , если использовать приведенный выше пример, однозначен, потому что он имеет тот же синтаксис, что и доступ к массиву или карте, а затем система типов может понять это позже, так же, как она уже должна сделать для разница между доступом к массиву и карте, например. Это отличается от случая <> , потому что один < допустим, что приводит к двусмысленности, а один [ — нет.
  • func Example[T](v T) T тоже однозначно.
  • ]] не является собственным токеном, поэтому этой проблемы также можно избежать.

В проекте проекта упоминается двусмысленность в объявлениях типов , например, в type A [T] int , но я думаю, что это можно относительно легко решить несколькими способами. Например, общее определение можно было бы переместить в само ключевое слово, а не в имя типа, т. е.:

  • func[T] Example(v T) T
  • type[T] A int

Сложность здесь может возникнуть из-за использования блоков объявления типа, например

type (
  A int
)

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

Я думаю, что было бы очень жаль писать

type[T] A []T
var s A[int]

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

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

Это не похоже на то, что отличается от синтаксиса типа массива и выражения [N]T против arr[i] с точки зрения того, как что-то объявляется, не соответствуя тому, как оно используется. Да, в var arr [N]T квадратные скобки заканчиваются на той же стороне arr , что и при использовании arr , но мы обычно думаем о синтаксисе с точки зрения синтаксиса типа и выражения. будучи противоположным.

Я расширил и улучшил некоторые свои старые незрелые идеи, чтобы попытаться объединить пользовательские и встроенные дженерики.

Я не уверен, является ли обсуждение ( vs < vs [ и использование type байкшерингом или действительно есть проблема с синтаксисом

@ianlancetaylor ... интересно, требуют ли отзывы каких-либо изменений в предлагаемом дизайне? По моему собственному мнению, многие считали, что интерфейсы и контракты можно комбинировать, по крайней мере, вначале. Казалось, что через некоторое время произошел сдвиг в том, что эти две концепции должны быть разделены. Но я мог неправильно читать тенденции. Хотелось бы увидеть экспериментальный вариант в выпуске в этом году!

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

Выступает, чтобы добавить отчет о практическом опыте:
Я реализовал дженерики как расширение языка в моем интерпретаторе Go https://github.com/cosmos72/gomacro. Интересно, что оба синтаксиса

type[T] Pair struct { First T; Second T }
type Pair[T] struct { First T; Second T }

оказалось, что в синтаксический анализатор вносится много неясностей: второе можно было разобрать как объявление о том, что Pair — это массив структур T , где T — некоторое постоянное целое число. Когда используется Pair , также возникают неоднозначности: Pair[int] также может быть проанализировано как выражение вместо типа: это может быть индексация массива/среза/карты с именем Pair с индексное выражение int (примечание: int и другие базовые типы НЕ являются зарезервированными ключевыми словами в Go), поэтому мне пришлось прибегнуть к новому синтаксису — по общему признанию, уродливому, но работающему:

template[T] type Pair struct { First T; Second T }
type pairOfInt = Pair#[int]
var p Pair#[int]

и аналогично для функций:

template[T] func Sum(args ...T) T { /*...*/ }
Sum#[int] (1,2,3)

Итак, хотя теоретически я согласен с тем, что синтаксис — поверхностный вопрос, я должен отметить, что:
1) с одной стороны, синтаксис - это то, с чем столкнутся программисты Go, поэтому он должен быть выразительным, простым и, возможно, приятным
2) с другой стороны, неправильный выбор синтаксиса усложнит парсер, проверку типов и компилятор для разрешения возникших неоднозначностей

Pair[int] также может быть проанализировано как выражение вместо типа: это может быть индексация массива/среза/карты с именем Pair с выражением индекса int

Это не двусмысленность синтаксического анализа, а только семантическая (до разрешения имени); синтаксическая структура одинакова в любом случае. Обратите внимание, что Sum#[int] также может быть либо типом, либо выражением в зависимости от того, что такое Sum . То же самое верно и для (*T) в существующем коде. Пока разрешение имени не влияет на структуру анализируемого объекта, все в порядке.

Сравните это с проблемами с <> :

f ( a < b , c < d >> (e) )

Вы даже не можете токенизировать это, так как >> может быть одним или двумя токенами. Затем вы не можете сказать, есть ли один или два аргумента для f ... структура выражения существенно меняется в зависимости от того, что обозначается a .

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

@cosmos72

Возможно, я ошибаюсь, но помимо того, что сказал @stevenblekinsop , возможно ли, что термин:

a b

также может подразумевать, что b не является типом, если известно, что b является буквенно-цифровым (без оператора/без разделителя) с необязательным добавлением [identifier] , а a не является специальным ключевым словом/специальным буквенно-цифровым (например, без импорта/ пакет/тип/функция)?.

Не знаю грамматики слишком много.

В некотором смысле такие типы, как int и Sum[int], все равно будут рассматриваться как выражения:

type (
    nodeList = []*Node  // nodeList and []*Node are identical types
    Polar    = polar    // Polar and polar denote identical types
)

Если бы go разрешал инфиксные функции, то действительно a type tag было бы неоднозначно, поскольку type могло бы быть инфиксной функцией или типом.

Сегодня я заметил, что в обзоре проблем этого предложения утверждается Swift:

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

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

protocol Equatable2 {
    static func == (lhs: Self, rhs: Self) -> Bool
}

class uniq: Equatable2 {
    static func == (lhs: uniq, rhs: uniq) -> Bool {
        return false
    }
}

let narf = uniq(), poit = uniq()

func !=<T: Equatable2> (lhs: T, rhs: T) -> Bool {
    return !(lhs == rhs)
}

print(narf != poit)

@сигхойя
Я говорил о неоднозначности синтаксиса a[b] , предложенного для дженериков, поскольку он уже используется для индексации срезов и карт, а не о a b .

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

К сожалению, у него довольно своеобразная схема именования, поэтому его не всегда легко понять с первого взгляда. Например, class на самом деле является ограничением для типов (универсальных или нет). Класс Eq является ограничением для типов, значения которых можно сравнивать с '==' и '/=':

class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool

означает, что тип a удовлетворяет ограничению Eq , если существует «специализация» (фактически «экземпляр» на языке Haskell) инфиксных функций == и /= , который принимает два аргумента, каждый из которых имеет тип a , и возвращает результат Bool .

В настоящее время я пытаюсь адаптировать некоторые идеи, найденные в дженериках Haskell, к предложению для дженериков Go и посмотреть, насколько хорошо они подходят. Я очень рад видеть, что исследования продолжаются с другими языками помимо C++ и Java:

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

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

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

type Comparer interface {
  Compare(v interface{}) (*int, error)
}
type PriorityQueue<T.(Comparer)> struct {
  things []T
}

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

@jesse-amano Интересно, что вам не нужны дженерики ни в одной версии Go. Мне трудно это понять, потому что как программист я действительно не люблю повторяться. Всякий раз, когда я программирую на «C», мне приходится реализовывать одни и те же базовые вещи, такие как список или дерево, в каком-то новом типе данных, и неизбежно мои реализации полны ошибок. С дженериками у нас может быть только одна версия любого алгоритма, и все сообщество может внести свой вклад в то, чтобы сделать эту версию лучшей. Каково ваше решение не повторяться?

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

Я предпочитаю второй вариант, потому что он делает синтаксис языка меньше и проще и позволяет объявлять новые числовые типы, которые могут использовать обычные операторы, например комплексные числа, которые вы можете добавить с помощью «+». Аргумент против этого, по-видимому, заключается в том, что люди могут злоупотреблять перегрузкой оператора, чтобы заставить «+» делать странные вещи, но мне это не кажется аргументом, потому что я уже могу злоупотреблять любым именем функции, например, я могу написать функцию с именем «print ', который стирает все данные на моем жестком диске и завершает работу программы. Я хотел бы иметь возможность ограничивать перегрузки как операторов, так и функций, чтобы они соответствовали определенным аксиоматическим свойствам, таким как коммутативность или ассоциативность, но если это не применимо ни к операторам, ни к функциям, я не вижу особого смысла. Оператор — это просто инфиксная функция, а функция — это всего лишь префиксный оператор.

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

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

@keean У меня нет такого сильного отвращения к повторению себя несколько раз, когда мне это нужно, и подход Go к обработке ошибок, приемникам методов и т. Д. В целом, похоже, хорошо справляется с устранением большинства ошибок.

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

Когда я читал отчеты об опыте, во многих случаях я думал, что go:generate или аналогичный инструмент мог бы решить проблему, а в некоторых других случаях я чувствовал, что, возможно, Go1 был не тем языком, и что-то еще могло быть вместо этого используется (возможно, с оболочкой плагина, если какой-то код Go должен использовать его). Но я знаю, что мне достаточно легко строить догадки о том, что я _мог бы_ сделать, что _могло_ сработать; До сих пор у меня не было никакого практического опыта, который заставлял меня желать, чтобы в Go1 было больше способов выражения универсальных типов, но, возможно, у меня странный способ думать о вещах, или, может быть, мне просто очень повезло работать только в проектах, которые на самом деле не нуждались в дженериках.

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

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

Когда я пишу программу, первое, что я делаю, это пытаюсь разбить проблему на хорошо известные куски, которые я могу составить, анализатор аргументов, некоторую потоковую передачу файлов, макет пользовательского интерфейса на основе ограничений. Люди делают ошибки не только в сложных алгоритмах, вряд ли кто-то может написать правильную реализацию "min" и "max" с первого раза (см.: http://componentsprogramming.com/writing-min-function-part5/).

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

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

Очень простой пример - "обмен". Я хочу просто поменять местами два значения, мне все равно, что они собой представляют:

swap<A>(x: *A, y: *A) {
   let tmp = *x
   *x = *y
   *y = tmp
}

Теперь я думаю, что тривиально убедиться, что эта функция верна, и тривиально увидеть, что она является универсальной и может быть применена к любому типу. Зачем мне когда-либо хотеть вводить эту функцию снова и снова для каждого типа указателя на значение, которое я мог бы использовать для замены. Конечно, тогда я могу построить из этого более крупные общие алгоритмы, такие как сортировка на месте. Я не думаю, что код go:generate даже для простого алгоритма будет легко увидеть, правильный ли он.

Я могу легко сделать ошибку, например:

let tmp = *x
*y = *x
*x = tmp

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

Я понимаю, что идиоматический способ делать такие вещи в Go — использовать пустой интерфейс, но это небезопасно для типов и работает медленно. Однако мне кажется, что Go не имеет правильных функций для элегантной поддержки такого рода универсального программирования, а пустые интерфейсы предоставляют лазейку для решения проблем. Вместо того, чтобы полностью менять стиль go, лучше разработать язык, подходящий для такого рода дженериков, с нуля. Интересно, что «Rust» правильно понимает многие общие вещи, но поскольку он использует управление статической памятью, а не сборку мусора, он добавляет много сложности, которая на самом деле не нужна для большинства программ. Я думаю, что между Haskell, Go и Rust, вероятно, есть все, что нужно для создания достойного основного универсального языка, просто все перемешано.

Для информации: в настоящее время я пишу список пожеланий по дженерикам Go,

с намерением фактически реализовать его в моем интерпретаторе Go gomacro , который уже имеет другую реализацию дженериков Go (по образцу шаблонов C++).

Это еще не все, отзывы приветствуются :)

@киан

Я прочитал сообщение в блоге, на которое вы ссылались, о функции min и четыре сообщения, предшествующие этому. Я не заметил даже попытки привести аргумент, что "вряд ли кто-нибудь сможет написать корректную реализацию 'min'...". Автор, кажется, действительно признает, что их первая реализация _правильна_... до тех пор, пока домен ограничен числами. Это введение объектов и классов и требование, чтобы их сравнивали только по одному измерению, если только значения в этом измерении не совпадают, за исключением случаев, когда - и так далее, что создает дополнительную сложность. Тонкие скрытые требования, связанные с необходимостью тщательного определения функций сравнения и сортировки для сложного объекта, являются именно тем, почему я _не_ люблю дженерики как концепцию (по крайней мере, в Go; кажется, что Java с Spring уже достаточно хорошая среда для создания объединить кучу зрелых библиотек в приложение).

Лично я не вижу необходимости в безопасности типов в генераторах макросов; если они генерируют разборчивый код ( gofmt помогает установить планку для этого довольно низко), то проверки ошибок во время компиляции должно быть достаточно. В любом случае, это не должно иметь значения для пользователя генератора (или кода, вызывающего его) для производства; за тот, по общему признанию, небольшой набор раз, когда мне приходилось писать общий алгоритм в виде макроса, несколько модульных тестов (обычно с плавающей запятой, строки и указателя на структуру — если есть какие-либо жестко запрограммированные типы, которые должны не быть жестко запрограммированным, один из этих трех будет несовместим с ним; если какой-либо из этих трех нельзя использовать в универсальном алгоритме, то это не универсальный алгоритм) было достаточно, чтобы макрос работал должным образом.

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

*y, *x = *x, *y

Также в стандартной библиотеке уже есть in-place sort . Он использует интерфейсы. Чтобы сделать версию специфичной для вашего типа, определите:

type myslice []mytype
func (s myslice) Len() int { return len(s) }
func (s myslice) Less(i, j int) bool { return s[i].whatWouldAlsoBeNeededInAGenericImpl(s[j]) }
func (s myslice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

По общему признанию, это на несколько байтов больше, чем SortableList<mytype>(myThings).Sort() , но это _много_ менее плотно для чтения, с меньшей вероятностью будет "заикаться" в остальной части приложения, и если ошибки действительно возникнут, я вряд ли нужно что-то столь же тяжелое, как трассировка стека, чтобы найти причину. Текущий подход имеет несколько преимуществ, и я опасаюсь, что мы потеряем их, если будем слишком сильно полагаться на дженерики.

@jesse-амано
Проблемы с 'min/max' возникают, даже если вы не понимаете необходимости стабильной сортировки. Например, один разработчик реализует min/max для некоторого типа данных в одном модуле, а затем он используется в сортировке или другом алгоритме другим членом команды без надлежащей проверки предположений и приводит к странным ошибкам, потому что он нестабилен.

Я думаю, что программирование — это в основном создание стандартных алгоритмов, очень редко программисты создают новые инновационные алгоритмы, поэтому min/max и sort — это просто примеры. Выделение пробелов в конкретных примерах, которые я выбрал, просто показывает, что я выбрал не очень хорошие примеры, и не затрагивает фактическую суть. Я выбрал «обмен», потому что это очень просто и быстро для меня. Я мог бы выбрать множество других, сортировку, поворот, разбиение, которые являются очень общими алгоритмами. Когда вы пишете программу, которая использует коллекцию, подобную красно-черному дереву, не требуется много времени, чтобы устать от необходимости переделывать дерево для каждого другого типа данных, из которого вы хотите получить коллекцию, потому что вам нужна безопасность типов и пустой интерфейс немногим лучше, чем "void*" в "C". Затем вы должны были бы сделать то же самое снова для каждого алгоритма, который использует каждое из этих деревьев, таких как предварительный порядок, порядок, итерация по порядку, поиск, и это до того, как мы перейдем к любым сложным вещам, таким как сетевые алгоритмы Тарьяна (непересекающиеся множества, кучи, минимальные остовные деревья, кратчайшие пути, потоки и т. д.)

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

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

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

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

@киан
Возможно, есть лучшее место для затянувшейся дискуссии. (Возможно, нет; я осмотрелся и, похоже, это _самое_ место для обсуждения дженериков в Go2.)

Я конечно понимаю необходимость стабильной сортировки! Я подозреваю, что авторы оригинальной стандартной библиотеки Go1 тоже это поняли, поскольку sort.Stable находится там с момента публичного выпуска.

Я думаю, что самое замечательное в пакете стандартной библиотеки sort состоит в том, что он работает не только со слайсами. Это, безусловно, проще всего, когда получателем является срез, но все, что вам действительно нужно, — это способ узнать, сколько значений находится в контейнере (метод Len() int ), как их сравнить (метод Less(int, int) bool ). Swap(int, int) ). Вы можете реализовать sort.Interface с помощью каналов! Это, конечно, медленно, потому что каналы не предназначены для эффективного индексирования, но его правильность может быть доказана, учитывая щедрый бюджет времени выполнения.

Я не хочу придираться, но проблема с плохим примером в том, что... это плохо. Такие вещи, как sort и min , — это всего лишь _не_ баллы в пользу высокоэффективной языковой функции, такой как дженерики. Я довольно сильно чувствую, что дырки в этих примерах _действительно_ обращают внимание на фактическую суть; Моя точка зрения заключается в том, что в дженериках нет необходимости, когда в языке уже существует лучшее решение.

@jesse-амано

лучшее решение уже существует в языке

Который из? Я не вижу ничего лучше, чем типобезопасные ограниченные дженерики. Генераторы — это не Go, это просто и понятно. Интерфейсы и отражение создают небезопасный, медленный и подверженный панике код. Эти решения достаточно хороши, потому что нет ничего другого. Обобщения решат проблему с шаблонными, небезопасными пустыми конструкциями интерфейса и, что хуже всего, устранят многие виды использования отражения, которые еще более подвержены панике во время выполнения. Даже новое предложение по пакету ошибок страдает от отсутствия дженериков, и его API сильно выиграет от них. Вы можете посмотреть на As в качестве примера - не идиоматичен, склонен к панике, сложен в использовании, требует ветеринарной проверки для правильного использования. Все потому, что в Go отсутствуют какие-либо дженерики.

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

@крекер

Который из?

Для сортировки вещей пакет sort . Все, что реализует sort.Interface , может быть отсортировано (с помощью стабильного или нестабильного алгоритма по вашему выбору; некоторые встроенные версии предоставляются через пакет sort , но вы можете написать свой собственный с помощью похожий или другой API). Поскольку стандартные библиотеки sort.Sort и sort.Stable работают со значением, переданным через список аргументов, значение, которое вы возвращаете, совпадает со значением, с которого вы начали, и, следовательно, обязательно тип вы получите обратно тот же тип, с которого вы начали. Это совершенно безопасно для типов, и компилятор выполняет всю работу по выводу, реализует ли ваш тип необходимый интерфейс и способен ли он _по крайней мере_ на столько оптимизаций во время компиляции, сколько было бы возможно с помощью функции sort<T> в стиле дженериков. .

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

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

Мне тоже не очень нравится пакет xerrors , но пакет xerrors.As не кажется мне неидиоматичным; в конце концов, это очень похоже на API json.Unmarshal . Возможно, потребуется лучшая документация и/или пример кода, но в остальном все в порядке.

Но нет, sort и min сами по себе довольно ужасные примеры. Первый уже существует в Go и прекрасно компонуется, и все это без использования дженериков. Последнее в самом широком смысле является одним из результатов sort (которое мы уже решили), и в случаях, когда может потребоваться более специализированное или оптимизированное решение, вы все равно напишете специализированное решение, а не будете опираться на него. дженерики. Опять же, в пакете стандартной библиотеки sort нет генераторов, интерфейса{}, отражения или "непонятных" языковых функций. Существуют непустые интерфейсы (которые четко определены в API, так что вы получаете ошибки времени компиляции, если вы используете их неправильно, выводятся, поэтому вам не нужны приведения, и проверяются во время компиляции, поэтому вам не нужно утверждения). Может быть какой-то шаблон, _если_ коллекция, которую вы сортируете, является срезом, но если это структура (например, представляющая корневой узел двоичного дерева поиска?), вы можете сделать так, чтобы она удовлетворяла sort.Interface тоже, так что на самом деле он _более_ гибок, чем универсальная коллекция.

@jesse-амано

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

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

@jesse-амано

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

Я согласен, мне нравится стандартная сортировка.

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

Это ложная дихотомия. Интерфейсы в Go уже являются разновидностью дженериков. Механизм не есть сама вещь. Загляните за пределы синтаксиса и увидьте цель, которая заключается в возможности выразить любой алгоритм в общем виде без ограничений. Абстракция интерфейса sort является универсальной, она позволяет сортировать любой тип данных, который может реализовать требуемые методы. Обозначение просто другое. Мы могли бы написать:

f<T>(x: T) requires Sortable(T)

Это означало бы, что тип «T» должен реализовывать интерфейс «Sortable». В Go это может быть написано func f(x Sortable) . Так что, по крайней мере, применение функции в Go может обрабатываться в общем, но есть операции, которые не любят арифметику или разыменование. В Go все хорошо, поскольку интерфейсы можно рассматривать как предикаты типов, но в Go нет решения для отношений между типами.

Легко увидеть ограничения Go, подумайте:

func merge(x, y Sortable)

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

merge<T>(x: T, y: T) requires Sortable(T)

Здесь ясно, что мы объединяем два одинаковых сортируемых типа. «Go» отбрасывает базовую информацию о типе и просто обрабатывает все, что «сортируется», как одно и то же.

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

Интерфейсы в Go уже являются разновидностью дженериков.

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

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

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

Рассмотрим: func merge(x, y Sortable)

Мне непонятно, почему слияние двух сортируемых вещей (или вещей, реализующих sort.Interface ) каким-либо образом отличается от слияния двух коллекций _вообще_. Для срезов это append ; для карт это for k, v := range m { n[k] = v } ; а для более сложных структур данных обязательно существуют более сложные стратегии слияния в зависимости от структуры (чье содержимое может потребоваться для реализации некоторых методов, необходимых структуре). Предполагая, что вы говорите о более сложном алгоритме сортировки, который разделяет и выбирает подалгоритмы для разделов, прежде чем объединять их вместе, вам нужно не то, чтобы разделы были «сортируемыми», а скорее какая-то гарантия того, что ваши разделы уже _отсортировано_ перед слиянием. Это совсем другая проблема, и синтаксис шаблона не помогает решить ее каким-либо очевидным образом; естественно, вы хотели бы, чтобы некоторые довольно строгие модульные тесты гарантировали надежность вашего алгоритма (алгоритмов) сортировки слиянием, но, конечно же, вы не хотели бы раскрывать _exported_ API, который обременяет разработчика такими вещами.

Вы поднимаете интересный вопрос о том, что в Go нет хорошего способа проверить, относятся ли два значения к одному и тому же типу без отражения, переключателей типов и т. д. Я действительно чувствую, что использование interface{} является вполне приемлемым решением в случай контейнеров общего назначения (например, круговой связанный список) в качестве шаблона, используемого для обертывания API для обеспечения безопасности типов, абсолютно тривиален:

type MyStack struct { stack Stack }
func (s *MyStack) Push(v MyType) error { return s.stack.Push(v) }
func (s *MyStack) Pop() (MyType, error) {
  v, err := s.stack.Pop()
  var m MyType
  if v != nil {
    if m, ok := v.(MyType); ok { return m, err; }
    panic("this code should be unreachable from the exported API")
  }
  return nil, err
}

Я изо всех сил пытаюсь представить, почему этот шаблон может быть проблемой, но если это так, разумной альтернативой может быть шаблон (text/). Вы можете аннотировать типы, для которых вы хотите определить стеки, с помощью комментария //go:generate stackify MyType github.com/me/myproject/mytype , и пусть go generate создаст шаблон для вас. Пока cmd/stackify/stackify_test.go пытается это сделать хотя бы с одной структурой и хотя бы с одним встроенным типом, и он компилируется и проходит, я не понимаю, почему это может быть проблемой — и, вероятно, довольно близко к тому, что любой компилятор сделал бы «под капотом», если бы вы определили шаблон. Единственная разница в том, что ошибки более полезны, потому что они менее плотные.

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

Предполагая, что шаблон _не_ проблема, я заинтересован в решении проблемы создания красно-черного дерева, которое так же легко использовать вызывающим абонентам, как и такие пакеты, как sort или encoding/json . Я обязательно потерплю неудачу, потому что... ну, я просто не такой умный. Но я взволнован, чтобы узнать, как близко я могу быть.

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

@jesse-амано

Если это так, то этот вопрос может быть закрыт как уже > решенный, потому что исходное утверждение было:

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

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

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

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

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

mul<T>(x, y T) T requires Addable(T) {
    r := 0
    for i := 0; i < y; ++i  {
        r = r + x
    }
    return r
}

Чтобы удовлетворить ограничения на «+», нам нужно убедиться, что x и y являются числовыми, но при этом оба имеют один и тот же базовый тип.

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

type Stack<T> interface {...}

Это позволило бы, среди прочего, объявить реализацию, которая является параметрической в T , чтобы мы могли поместить любые T в MyStack, используя интерфейс Stack, при условии, что все виды использования Push и Pop в том же экземпляре MyStack работает с одним и тем же типом «значения».

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

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

@jesse-амано
Глядя на пример с красным/черным деревом, мы действительно хотим, чтобы в общем виде было определение «Карты», красное/черное дерево - это всего лишь одна из возможных реализаций. Таким образом, мы можем ожидать такой интерфейс:

type Map<Key, Value> interface {
   put(x Key, y Value) 
   get(x Key) Value
}

Затем в качестве реализации можно было бы предоставить красно-черное дерево. В идеале мы хотим написать код, который не зависит от реализации, чтобы вы могли предоставить хеш-таблицу, или красно-черное дерево, или BTree. Затем мы напишем наш код:

f<K, V, T>(index T) T requires Map<K, V> {
   ...
}

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

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

type MapIntString interface {
   put(x Int, y String)
   get(x Int) String
}

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

Хотя я согласен с последним комментарием @keean , сложность заключается в написании красно-черного дерева в Go, которое реализует известный интерфейс, как, например, только что предложенный.

Хорошо известно, что без дженериков для реализации контейнеров, не зависящих от типа, необходимо использовать interface{} и/или отражение — к сожалению, оба подхода медленны и подвержены ошибкам.

@киан

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

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

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

Я тоже не согласен с этим. Как оплачиваемый профессионал, моя цель состоит в том, чтобы сократить затраты времени/усилий _для себя и других_, одновременно увеличивая выгоды моего работодателя, как бы они ни измерялись. «Скучная» задача плоха только в том случае, если она отнимает много времени; это не может быть трудным, иначе это не было бы скучно. Если это требует лишь немного времени, но устраняет будущие трудоемкие действия и/или позволяет быстрее выпустить продукт, то это все равно того стоит.

Затем в качестве реализации можно было бы предоставить красно-черное дерево.

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

Я знаю, что минимальный интерфейс, требуемый библиотекой красного/черного дерева, — это тот, в котором существует «слабый» порядок элементов, поэтому мне нужно что-то вроде функции с именем Less(v interface{}) bool , но если у вызывающей стороны есть метод, который делает что-то похожее, но не называется Less(v interface{}) bool , они должны написать шаблонные оболочки/прокладки, чтобы это работало.

Когда вы получаете доступ к элементам, содержащимся в красном/черном дереве, вы получаете interface{} , но если вы хотите доверять моей гарантии, что библиотека предоставила _является_ красно/черным деревом, я не понимаю, почему вы не Не верьте, что типы элементов, которые вы добавляете, будут точно такими же, какие вы получите. Если вы _доверяете_ обеим этим гарантиям, то библиотека вообще не подвержена ошибкам. Просто напишите (или вставьте) дюжину или около того строк кода, чтобы покрыть утверждения типа.

Теперь у вас есть совершенно безопасная библиотека (опять же, если предположить, что уровень доверия не превышает уровень доверия, который вы должны были бы предоставить, чтобы загрузить библиотеку в первую очередь), которая даже имеет точные имена функций, которые вы хотите. Это важно. В экосистеме в стиле Java, где авторы библиотек из кожи вон лезут, чтобы кодировать _точное_ определение интерфейса (они почти _должны_ делать это, потому что язык навязывает его посредством синтаксиса class MyClassImpl extends AbstractMyClass implements IMyClass ) и есть куча дополнительной бюрократии, вы должны приложить все усилия, чтобы сделать фасад для сторонней библиотеки, чтобы она соответствовала стандартам кодирования вашей организации (что такое же количество шаблонов, если не больше), или же позволить этому быть «исключением» для стандарты кодирования вашей организации (и, в конце концов, ваша организация имеет столько же исключений в своих стандартах, сколько и в своих кодовых базах), или же отказаться от использования совершенно хорошей библиотеки (предполагая, ради аргумента, что библиотека действительно хороша).

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

Я согласен с этим идеалом, но думаю, что Go уже его удовлетворяет. С интерфейсом вроде:

type MyStorage interface {
  Get(KeyType) (ValueType, error)
  Put(KeyType, ValueType) error
}

единственное, чего не хватает, — это возможности параметризации того, что такое KeyType и ValueType , и я не уверен, что это особенно важно.

Как (гипотетический) сопровождающий библиотеки красного/черного дерева, мне все равно, какие у вас типы. Я просто буду использовать interface{} для всех своих основных функций, которые обрабатывают «некоторые данные», и _может быть_ предоставлю некоторые экспортированные примеры функций, которые упростят их использование с такими распространенными типами, как string и int . Но вызывающая сторона должна обеспечить чрезвычайно тонкий слой вокруг этого API, чтобы сделать его безопасным для любых пользовательских типов, которые они могут в конечном итоге определить. Но единственная важная вещь в API, который я предоставляю, заключается в том, что он позволяет вызывающей стороне делать все то, что, по их мнению, может делать красное/черное дерево.

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

Кстати, код мог бы стать намного более запутанным, если бы он не был таким. Как вы сказали, существует множество возможных реализаций хранилища ключей/значений. Передача абстрактной «концепции» хранения ключей/значений скрывает сложность того, как осуществляется хранение ключей/значений, и разработчик в моей команде может выбрать неправильный вариант для своей задачи (включая будущую версию меня, чье знание ключа Реализация хранилища /value выгрузила из памяти!). Приложение или его модульные тесты могут, несмотря на все наши усилия по проверке кода, содержать незаметный код, зависящий от реализации, который перестает надежно работать, когда одни хранилища ключей/значений зависят от подключения к БД, а другие нет. Досадно, когда отчет об ошибке содержит большую трассировку стека, а единственная строка в трассировке стека, ссылающаяся на что-то в _реальной_ кодовой базе, указывает на строку, использующую значение интерфейса, и все потому, что реализация этого интерфейса представляет собой сгенерированный код (который вы можете видеть только во время выполнения) вместо обычной структуры с методами, возвращающими читаемые значения ошибок.

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

Я все еще хотел бы видеть следующие вещи на общем языке:

  • параметрические типы, например: RBTree<Int, String> , так как это обеспечит безопасность типов пользовательских коллекций.
  • переменные типа, такие как: f<T>(x, y T) T , потому что это необходимо для определения семейств связанных функций, таких как сложение, вычитание и т. д., где функция является полиморфной, но мы требуем, чтобы все аргументы были одного и того же базового типа.
  • ограничения типа, такие как: f<T: Addable>(x, y T) T , которые применяют интерфейсы к переменным типа, потому что, как только мы вводим переменные типа, нам нужен способ ограничить эти переменные типа вместо того, чтобы рассматривать Addable как тип. Если мы рассматриваем Addable как тип и пишем f(x, y Addable) Addable , мы не можем сказать, являются ли исходные базовые типы x и y такими же, как друг друга или возвращаемый тип.
  • многопараметрические интерфейсы, такие как: type<K, V> Map<K, V> interface {...} , которые можно использовать как merge<K, V, T: Map<K, V>>(x, y T) T , которые позволяют нам объявлять интерфейсы, параметризуемые не только типом контейнера, но в данном случае также ключом и значением виды карты.

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

Любой прогресс или график по этому вопросу?

@leaxoy На GopherCon запланировано выступление @ianlancetaylor «Generics in Go». Я ожидаю услышать больше о текущем положении дел в этом выступлении.

@griesemer Спасибо за эту ссылку.

@keean Мне бы также хотелось увидеть здесь предложение Where из Rust, которое может быть улучшением вашего предложения type constraints . Это позволяет использовать систему типов для ограничения поведения, такого как «запуск транзакции до запроса», для проверки типов без отражения во время выполнения. Посмотрите это видео на нем: https://www.youtube.com/watch?v=jSpio0x7024

@jadbox извините, если мое объяснение было неясным, но пункт «где» почти точно соответствует тому, что я предлагал. То, что после «где» в ржавчине — это ограничения типа, но я думаю, что вместо этого я использовал ключевое слово «требует» в предыдущем посте. Все это было сделано в Haskell по крайней мере десять лет назад, за исключением того, что Haskell использует оператор '=>' в сигнатурах типов для указания ограничений типа, но это тот же основной механизм.

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

merge<K, V, T>(x, y T) T requires T: Map<K, V>

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

merge<K, V, T: Map<K, V>>(x, y T) T

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

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

Это по-прежнему подходящая ветка для обратной связи/обсуждения текущего/последнего рабочего предложения Go 2 Generics , о котором недавно было объявлено?

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

Достаточно ли общих параметров типа для Go 2 Generics?

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

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

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

Go уникален именно потому, что он такой. Если он не сломан, то, пожалуйста , не пытайтесь его починить!

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

Go уникален именно потому, что он такой. Если он не сломан, то, пожалуйста, не пытайтесь его починить!

Он сломан, поэтому его следует починить.

Он сломан, поэтому его следует починить.

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

Участвуя в прошлых мега-проблемах, могу ли я предложить открыть канал на Gopher Slack для тех, кто хочет это обсудить, проблема временно заблокирована, а затем выложено время, когда проблема будет разморожена для всех, кто хочет закрепить обсуждение из Slack? Проблемы Github больше не работают как форум, когда появляется ужасная ссылка «478 скрытых элементов. Загрузить еще…».

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

Поучаствовав в прошлых мегавыпусках, могу ли я предложить открыть канал на Gopher Slack для тех, кто хочет это обсудить

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

не каждый может получить учетную запись Slack или может использовать их ужасные клиенты

Что здесь значит «может»? Есть ли какие-то ограничения в Slack, о которых я не знаю, или людям просто не нравится им пользоваться? Последнее, я думаю, хорошо, но некоторые люди также бойкотируют Github, потому что им не нравится Microsoft, поэтому вы теряете одних людей, но приобретаете других.

не каждый может получить учетную запись Slack или может использовать их ужасные клиенты

Что здесь значит «может»? Есть ли какие-то ограничения в Slack, о которых я не знаю, или людям просто не нравится им пользоваться? Последнее, я думаю, хорошо, но некоторые люди также бойкотируют Github, потому что им не нравится Microsoft, поэтому вы теряете одних людей, но приобретаете других.

Slack — американская компания, и поэтому она будет следовать любой внешней политике, навязанной США.

У Github та же проблема, и он только что попал в новости из-за того, что выгнал иранцев без предупреждения. К сожалению, если мы не будем использовать Tor или IPFS или что-то в этом роде, нам придется соблюдать законы США и Европы на любом практическом дискуссионном форуме.

У Github та же проблема, и он только что попал в новости из-за того, что выгнал иранцев без предупреждения. К сожалению, если мы не будем использовать Tor или IPFS или что-то в этом роде, нам придется соблюдать законы США и Европы на любом практическом дискуссионном форуме.

Да, мы застряли с GitHub и Google Groups. Давайте не будем добавлять в список больше проблемных сервисов. Также чат не является хорошим архивом; довольно трудно копаться в этих обсуждениях, когда они красиво переплетены и находятся на голанг-гайках (где они попадают прямо в ваш почтовый ящик). Slack означает, что если вы не находитесь в том же часовом поясе, что и все остальные, вам нужно пробираться через массу архивов чатов, один из несеквитеров и т. Д. Списки рассылки означают, что вы, по крайней мере, несколько организованы в потоки, и люди, как правило, принимают больше времени в их ответах, чтобы вы не получили кучу случайных разовых комментариев, оставленных случайно. Кроме того, у меня просто нет учетной записи Slack, и их дурацкие клиенты не будут работать ни на одной из машин, которые я использую. С другой стороны, Mutt (или ваш любимый почтовый клиент, ура, стандарты) работает везде.

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

Что касается уникальности Go: у Go есть несколько приятных особенностей, но он не настолько уникален, как некоторые думают. В качестве двух примеров, CLU и Modula-3 имеют схожие цели и одинаковую отдачу, и оба поддерживают дженерики в той или иной форме (с ~ 1975 года в случае CLU!) В ​​настоящее время они не имеют промышленной поддержки, но FWIW можно получить компилятор работает для них обоих.

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

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

вместо:

type Vector(type Element) []Element
var v Vector(int)
func (v *Vector(Element)) Push(x Element) { *v = append(*v, x) }
type VectorInt = Vector(int)

мы могли бы

type Vector<Element> []Element
var v Vector<int>
func (v *Vector<Element>) Push(x Element) { *v = append(*v, x) }
type VectorInt = Vector<int>

Синтаксис <> упоминается в черновике, @jnericks (ваше имя пользователя идеально подходит для этого обсуждения...). Основной аргумент против этого заключается в том, что он значительно увеличивает сложность синтаксического анализатора. В более общем плане это делает язык Go значительно более сложным для анализа с небольшой пользой. Большинство людей согласны с тем, что это действительно улучшает читаемость, но есть разногласия по поводу того, стоит ли идти на компромисс. Лично я так не думаю.

Использование ключевого слова type необходимо для устранения неоднозначности. В противном случае трудно определить разницу между func Example(T)(arg int) {} и func Example(arg int) (int) {} .

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

как мы знаем, в go мы всегда объявляем структуру или интерфейс следующим образом:

type MyStruct struct {
        a int
        s string
}

type MyInterface inteface {
    Method1() err
    Method2() string
}

но декларация контракта в последнем предложении выглядит так:

contract Ordered(T) {
    T int, int8
}

contract G(Node, Edge) {
    Node Edges() []Edge
    Edge Nodes() (from Node, to Node)
}

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

type Ordered(T) contract {
    T int, int8
}

if there is only one type parameter, the declaration above can be also wrote like this:

type Ordered contract {
    int , int8
}


if there are more than one type parameter, we have to use named parameter:

type G(Node, Edge) contract {
    Node Edges() []Edge
    Edge Nodes() (from Node, to Node)
}

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

type (
        Sequence contract {
                string, []byte
        }

    Stringer(T) contract {
        T String() string
    }

    Stringer contract { // equivalent with the above Stringer(T), single type parameter could be omitted
        String() string
    }

        MyStruct struct {
                a int
                b string
        }

    G(Node, Edge) contract {
        Node Edges() []Edge
        Edge Nodes() (from Node, to Node)
    }
)

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

@bigwhite Мы все еще обсуждаем эту запись. Аргумент в пользу нотации, предложенной в проекте проекта, заключается в том, что контракт не является типом (например, нельзя объявить переменную типа контракта), и, таким образом, контракт является новым типом сущности в том же смысле, что и константа. , функция, переменная или тип. Аргумент в пользу вашего предложения заключается в том, что контракт является просто «типом типа» (или метатипом) и, следовательно, должен следовать согласованной нотации. Еще один аргумент в пользу вашего предложения заключается в том, что оно позволит использовать «анонимные» литералы контрактов без необходимости их явного объявления. Короче, ИМХО это еще не решено. Но это также легко изменить в будущем.

FWIW, CL 187317 в настоящее время поддерживает обе нотации (хотя параметр контракта должен быть записан вместе с контрактом), например:

type C contract(X) { ... }

и

contract C (X) { ... }

принимаются и представляются одинаково внутри страны. Более последовательным подходом будет:

type C(type X) contract { ... }

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

В Go также есть объявления функций:

func Name(args) { body }

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

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

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

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

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

@keean Любой интерфейс уже служит «ограничением для типов», но они являются типами. Сторонники теории типов очень формально смотрят на ограничения типов как на типы. Как я уже упоминал выше , есть разумные аргументы, которые можно привести для любой точки зрения. Здесь нет «логических парадоксов» — на самом деле текущий незавершенный прототип моделирует контракт как тип внутри, поскольку это упрощает дело на данный момент.

Интерфейсы @griesemer в Go — это «подтипы», а не ограничения типов. Однако я нахожу потребность как в контрактах, так и в интерфейсах недостатком дизайна Go, однако может быть слишком поздно менять интерфейсы на ограничения типов, а не подтипы. Выше я утверждал, что интерфейсы Go не обязательно должны быть подтипами, но я не вижу большой поддержки этой идеи. Это сделало бы интерфейсы и контракты одним и тем же, если бы интерфейсы можно было объявлять и для операторов.

Здесь есть парадоксы, так что действуйте осторожно, парадокс Жирара является наиболее распространенным «кодированием» парадокса Рассела в теории типов. Теория типов вводит концепцию юниверсов, чтобы предотвратить эти парадоксы, и вам разрешено ссылаться на типы только в юниверсе «U» из юниверса «U+1». Внутри эти теории типов реализуются как логика более высокого порядка (например, Elf использует лямбда-пролог). Это, в свою очередь, сводится к решению ограничений для разрешимого подмножества логики более высокого порядка.

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

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

@griesemer Интересно, переделываем контракты как типы. Исходя из моей собственной ментальной модели, я бы думал об ограничениях как о метатипах, а о контрактах — как о своего рода структуре уровня типа.

type A int
func (a A) Foo() int {
    return int(a)
}

type C contract(T, U) {
    T int
    U int, uint
    U Foo() int
}

var B (int, uint; Foo() int).type = A
var C1 C = C(A, B)

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

@stevenblenkinsop , вы меня потеряли, почему вы передаете T в C contract , когда он не используется, и что пытаются сделать строки var ?

@griesemer спасибо за ваш ответ. Один из принципов дизайна Go — «обеспечить только один способ сделать что-то». Лучше оставить только одну форму декларации контракта. контракт типа C (тип X) { ... } лучше.

@Goodwine Я переименовал типы, чтобы отличать их от параметров контракта. Может быть, это помогает? Предполагается, что (int, uint; Foo() int).type является метатипом любого типа, который имеет базовый тип int или uint и реализует Foo() int . var B предназначен для демонстрации использования типа в качестве значения и присвоения его переменной, тип которой является метатипом (поскольку метатип похож на тип, значения которого являются типами). var C1 предназначен для отображения переменной, тип которой является контрактом, и показывает пример чего-то, что может быть присвоено такой переменной. По сути, пытаясь ответить на вопрос «если контракт является типом, как выглядят его значения?». Смысл в том, чтобы показать, что это значение само по себе не является типом.

У меня проблема с контрактами с несколькими типами.

Вы можете добавить или оставить его для контракта типа paremeter, как
type Graph (type Node, Edge) struct { ... }
и
type Graph (type Node, Edge G) struct { ... } в порядке.

Но что, если я хочу добавить контракт только для одного из двух параметров типа?

contract G(Node, Edge) {
    Node Edges() []Edge
    Edge Nodes() (from Node, to Node)
}

ПРОТИВ

contract G(Edge) {
    Edge Nodes() (from Node, to Node)
}

@themez Это в черновике. Вы можете использовать синтаксис (type T, U comparable(T)) , например, для ограничения только одного параметра типа.

@stevenblekinsop Понятно , спасибо.

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

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

@keean Я думаю, что мы по-разному интерпретируем слово «ограничение»; Я использую его довольно неформально. По определению интерфейсов, учитывая интерфейс I и переменную x типа I , только значения с типами, которые реализуют I , могут быть присвоены x . Таким образом, I можно рассматривать как «ограничение» для этих типов (конечно, существует бесконечно много типов, удовлетворяющих этому «ограничению»). Точно так же можно использовать I в качестве ограничения для параметра типа P универсальной функции; будут разрешены только фактические аргументы типа с наборами методов, которые реализуют I . Таким образом, I также ограничивает набор возможных фактических типов аргументов.

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

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

Как будет представлено предложение контрактов? Используя параметр go модуля go1.14 ? Переменная окружения GO114CONTRACTS ? Оба? Что-то другое..?

Извините, если это уже обсуждалось ранее, не стесняйтесь перенаправить меня туда.

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

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

  2. Contracts может иметь дело со встроенными операциями, преобразованиями и т. д., перечисляя типы, которые их поддерживают; interfaces может иметь дело только с методами, которых нет у самих встроенных типов.

  3. Какими бы они ни были в терминах теории типов, contracts не являются типами в том смысле, в каком мы обычно думаем о них в Go, т.е. вы не можете объявлять переменные типов contract и присваивать им какое-то значение. С другой стороны, interfaces являются типами, вы можете объявлять переменные этих типов и присваивать им соответствующие значения.

Хотя я вижу смысл contract , который требует, чтобы один параметр типа имел определенные методы, вместо этого должен быть представлен interface (это то, что я даже отстаивал в своем прошлом предложения), теперь я чувствую, что это был бы неудачный шаг, потому что он снова запутал бы воду между contracts и interfaces .

Раньше мне действительно не приходило в голову, что contracts может быть правдоподобно объявлено так, как @bigwhite предложил использовать существующий шаблон 'type'. Однако, опять же, мне не нравится эта идея, потому что я чувствую, что она пойдет на компромисс (3) выше. Кроме того, если необходимо (по причинам синтаксического анализа) повторить ключевое слово type при объявлении общей структуры следующим образом:

type List(type Element) struct {
    next *List(Element)
    val  Element
}

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

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

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

Я хотел бы предложить не называть предикаты над типами "контрактами". Есть две причины:

  • Термин «контракты» уже используется в компьютерных науках по-другому. Например, см.: (https://scholar.google.com/scholar?hl=en&as_sdt=0%2C33&q=contracts+languages&btnG=)
  • В литературе по информатике уже есть несколько названий этой идеи. Я знаю как минимум ~три~четыре: "наборы", "классы типов", "концепции" и "ограничения". Добавление другого еще больше запутает дело.

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

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

С ограничением типа pred(X) мы начинаем с полностью полиморфного X, а затем ограничиваем разрешенные значения. Так что скажите только X, который реализует «печать». Это ненаправленно и, следовательно, не имеет ковариантности или контравариантности. На самом деле он инвариантен в том смысле, что мы знаем базовый тип X во время компиляции.

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

Это ответ на ваш вопрос, или я пропустил момент?

Редактировать: я должен указать, что я имею в виду интерфейсы «Go» конкретно выше. Пункты о подтипах применимы ко всем языкам, у которых есть подтипы, но Go необычен тем, что делает интерфейсы типом и, следовательно, имеет отношение подтипа. В других языках, таких как Java, интерфейс явно не является типом (класс — это тип), поэтому интерфейсы — это ограничение на типы. Таким образом, хотя в целом правильно рассматривать интерфейсы как ограничения типов, это неправильно конкретно для Go.

@Inuart Еще слишком рано говорить, как это будет добавлено в реализацию. Предложений пока нет, только эскизный проект. Его точно не будет в 1.14.

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

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

Я никогда не понимал, почему С++ называет это «концепцией». Что это хотя бы значит?

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

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

Я никогда не понимал, почему С++ называет это «концепцией». Что это хотя бы значит?

Концепция — это абстракция экземпляров, имеющих некоторые общие черты, например подписи.

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

@sighoya Я также собирался упомянуть, что «концепции» являются концептуальными, поскольку они включают в себя «аксиомы», которые жизненно важны для предотвращения злоупотребления операторами. Например, сложение «+» должно быть ассоциативным и коммутативным. Эти аксиомы не могут быть представлены в C++, поэтому они существуют как абстрактные идеи, следовательно, «концепции». Итак, концепт — это синтаксический «контракт» плюс семантические аксиомы.

@ianlancetaylor «Ограничение» — это то, как мы назвали это в Genus (http://www.cs.cornell.edu/~yizhou/papers/genus-pldi2015.pdf), поэтому я неравнодушен к этой терминологии. Термин «контракт» был бы вполне разумным выбором, за исключением того, что он очень активно используется в сообществе PL для обозначения отношений между интерфейсами и реализациями, которые также имеют договорный характер.

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

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

Как насчет чего-то вроде:

typeConstraint C(T) {
}

или

типКонтракт C(T) {
}

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

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

1.

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

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

  1. Нужны ли нам другие ограничения помимо текущего вида типа и набора методов?
    Например, конвертируемые из одного типа в другой, присваиваемые из одного типа в другой,
    сравнимы между двумя типами, является каналом отправки, каналом приема,
    имеет указанный набор полей, ...

3.

Если универсальная функция использует строку, подобную следующей

v.Foo()

Как мы можем написать контракт, который позволяет Foo быть либо методом, либо полем функционального типа?

Ограничения типов @merovius должны разрешаться во время компиляции, иначе система типов может быть ненадежной. Это связано с тем, что у вас может быть тип, который зависит от другого, неизвестного до времени выполнения. Затем у вас есть два варианта: вы должны реализовать полностью зависимую систему типов (которая позволяет выполнять проверку типов во время выполнения, когда типы становятся известными) или вы должны добавить экзистенциальные типы в систему типов. Экзистенциалы кодируют разность фаз статически известных типов и типов, которые известны только во время выполнения (например, типы, которые зависят от чтения из ввода-вывода).

Подтипы, как указано выше, обычно неизвестны до времени выполнения, хотя во многих языках есть оптимизации на случай, если тип известен статически.

Если мы предположим, что одно из вышеперечисленных изменений введено в язык (зависимые типы или экзистенциальные типы), тогда нам все равно нужно разделить концепции подтипов и ограничений типов. Для Go конкретно ограничители типов инвариантны, мы можем игнорировать эти различия, и мы можем считать, что Go-интерфейсы _имеют_ ограничения на типы (статически).

Поэтому мы можем рассматривать Go-интерфейс как контракт с одним параметром, где параметр является получателем всех функций/методов. Так почему же в go есть и интерфейсы, и контракты? Мне кажется, это потому, что Go не хочет разрешать интерфейсы для операторов (например, «+»), и потому что Go не имеет ни зависимых, ни экзистенциальных типов.

Таким образом, есть два фактора, которые создают реальную разницу между ограничениями типов и подтипами. Один из них — это ко/противовариантность, которую мы можем игнорировать в Go из-за инвариантности конструктора типов, а другой — потребность в зависимых или экзистенциальных типах, чтобы сделать систему типов с ограничениями типов обоснованной, если существует полиморфизм параметров типа во время выполнения для ограничений типа.

@keean Круто, поэтому AIUI, по крайней мере, согласен с тем, что интерфейсы в Go можно считать ограничениями :)

В остальном: Выше вы утверждали:

«ограничения на типы» - это чисто время компиляции, потому что типы стираются перед выполнением. Ограничения приводят к тому, что универсальный код преобразуется в необобщенный код, который может быть выполнен.

Это утверждение более конкретно, чем ваше последнее, что ограничения должны быть разрешены во время компиляции. Все, что я пытался сказать, это то, что компилятор может выполнять такое разрешение (и все те же проверки типов), но затем все равно генерировать общий код. Это все равно было бы правильно, потому что семантика системы типов та же. Но ограничения по-прежнему будут иметь представление во время выполнения. Это немного придирчиво , но именно поэтому я считаю, что определение их на основе времени выполнения и времени компиляции - не лучший способ сделать это. Это смешивает проблемы реализации с обсуждением абстрактной семантики системы типов.

FWIW, ранее я утверждал, что предпочел бы использовать интерфейсы для выражения ограничений, а также пришел к выводу, что разрешение использования операторов в универсальном коде является основным препятствием для этого и, следовательно, основной причиной введения отдельного концепция в виде контрактов.

@keean Спасибо, но нет, ваш ответ не ответил на мой вопрос. Обратите внимание, что в своем комментарии я описал очень простой пример использования интерфейса вместо соответствующего контракта/"ограничения". Я попросил _простой_ _конкретный_ пример, почему этот сценарий не будет работать «без некоторых ограничений», как вы намекали в своем предыдущем комментарии . Вы не привели такого примера.

Обратите внимание, что я не упомянул подтипы, ко- или контрвариантность (которую мы все равно не допускаем в Go, сигнатуры всегда должны совпадать) и т. д. Вместо этого я использовал элементарную и устоявшуюся терминологию Go (интерфейсы, реализации, параметр типа и т. д.), чтобы объяснить, что я имею в виду под «ограничением», потому что это общий язык, который все здесь понимают, и поэтому каждый может следовать ему. (Кроме того, вопреки вашему утверждению здесь , в Java интерфейс выглядит для меня как тип в соответствии со спецификацией Java : «Объявление интерфейса указывает новый именованный ссылочный тип». Если это не говорит, что интерфейс является типом, тогда у разработчиков Java Spec есть над чем поработать.)

Но похоже, что вы косвенно ответили на мой вопрос своим последним комментарием , как уже заметил @Merovius , когда вы сказали: «Поэтому мы можем рассматривать Go-интерфейс как контракт с одним параметром, где параметр является получателем всех функций/методов. .". Это именно то, о чем я говорил в начале, так что спасибо за подтверждение того, что я сказал все это время.

@dotaheor

Когда универсальный тип A встраивает другой универсальный тип B или универсальная функция A вызывает другую универсальную функцию B, нужно ли нам также указывать контракты B на A?

Если универсальный тип A включает в себя другой универсальный тип B, то параметры типа, переданные в B, должны удовлетворять любому контракту, используемому B. Для этого контракт, используемый A, должен подразумевать контракт, используемый B. То есть все ограничения параметры типа, переданные B, должны быть выражены в контракте, используемом A. Это также применимо, когда универсальная функция вызывает другую универсальную функцию.

Если ответ верен, то если универсальный тип встраивает много других универсальных типов или универсальная функция вызывает множество других универсальных функций, то нам нужно объединить множество контрактов в один как контракт встраиваемого типа или вызывающей функции. Это может вызвать проблему, подобную отравлению const.

Я думаю, то, что вы говорите, верно, но проблема не в отравлении const. Проблема отравления константами заключается в том, что вы должны распространять const везде, где передается аргумент, а затем, если вы обнаружите какое-то место, где аргумент должен быть изменен, вы должны удалить const везде. Случай с дженериками больше похож на «если вы вызываете несколько функций, вы должны передать значения правильного типа каждой из этих функций».

В любом случае мне кажется крайне маловероятным, что люди будут писать универсальные функции, которые вызывают множество других универсальных функций, использующих разные контракты. Как это могло произойти естественным путем?

Нужны ли нам другие ограничения помимо текущего вида типа и набора методов? Например, конвертируемый из одного типа в другой, назначаемый из одного типа в другой, сравнимый между двумя типами, является каналом для отправки, каналом для приема, имеет указанный набор полей, ...

Такие ограничения, как конвертируемость, присваиваемость и сравнимость, выражаются в форме типов, как поясняется в проекте проекта. Ограничения, такие как отправляемый или получаемый канал, могут быть выражены только в форме chan T , где T — это некоторый параметр типа, как поясняется в проекте проекта. Невозможно выразить ограничение, что тип имеет указанный набор полей, но я сомневаюсь, что это будет встречаться очень часто. Нам нужно будет посмотреть, как это работает, написав реальный код, чтобы посмотреть, что произойдет.

Если универсальная функция использует строку, подобную следующей

v.Foo()
Как мы можем написать контракт, который позволяет Foo быть либо методом, либо полем функционального типа?

В текущем проекте дизайна вы не можете. Это кажется важным вариантом использования? (Я знаю, что предыдущий проект проекта поддерживал это.)

@griesemer , вы упустили момент, когда я сказал, что это действительно только в том случае, если вы вводите зависимые или экзистенциальные типы в систему типов.

В противном случае, если вы используете контракт в качестве интерфейса, вы можете потерпеть неудачу во время выполнения, потому что вам нужно отложить проверку типов до тех пор, пока вы не узнаете типы, и проверка типов может завершиться ошибкой, что, следовательно, небезопасно для типов.

Я также видел, как интерфейсы объясняются как подтипы, поэтому вы должны быть осторожны, чтобы кто-то не попытался в будущем внедрить ко/контравариантность в конструкторы типов. Лучше не иметь интерфейсов как типов, тогда такой возможности нет, и понятны замыслы проектировщиков, что это не подтипы.

Для меня было бы лучше объединить интерфейсы и контракты и сделать их явными ограничениями типов (предикаты для типов).

@ianlancetaylor

В любом случае мне кажется крайне маловероятным, что люди будут писать универсальные функции, которые вызывают множество других универсальных функций, использующих разные контракты. Как это могло произойти естественным путем?

Почему это будет необычно? Если я определяю функцию для типа «T», я захочу вызывать функции для «T». Например, если я определяю функцию «сумма» по «добавляемым типам» по контракту. Теперь я хочу создать общую функцию умножения, которая вызывает sum? Многие вещи в программировании имеют структуру суммы/произведения (все, что является «группой»).

Я не понимаю, какова будет цель интерфейса после того, как контракты будут на языке, похоже, что контракты будут служить той же цели, чтобы гарантировать, что для типа определен набор методов.

@keean Необычный случай - это функции, которые вызывают множество других универсальных функций, которые используют разные контракты. Ваш контрпример вызывает только одну функцию. Помните, что я выступаю против сходства с const-отравлением.

@mrkaspa Самый простой способ представить себе, что контракты похожи на шаблонные функции C++, а интерфейсы — на виртуальные методы C++. У обоих есть польза и цель.

@ianlancetaylor по опыту, возникают две проблемы, похожие на отравление константами. И то, и другое происходит из-за древовидной природы вложенных вызовов функций. Во-первых, когда вы хотите добавить отладку к глубоко вложенной функции, вам нужно добавить printable из листа до корня, что может потребовать касания нескольких сторонних библиотек. Во-вторых, вы можете накапливать большое количество контрактов в корне, что затрудняет чтение сигнатур функций. Часто лучше, чтобы компилятор выводил ограничения, как Haskell делает с классами типов, чтобы избежать этих двух проблем.

@ianlancetaylor Я не слишком много знаю о C ++, каковы будут варианты использования интерфейсов и контрактов в golang? когда я должен использовать интерфейс или контракт?

@keean Эта подтема посвящена конкретному проекту дизайна для языка Go. В Go все значения доступны для печати. Это не то, что должно быть выражено в контракте. И хотя я готов увидеть доказательства того, что для одной универсальной функции или типа может накапливаться множество контрактов, я не готов принять утверждение, что это произойдет. Суть черновика проекта состоит в том, чтобы попытаться написать реальный код, который его использует.

В черновике дизайна объясняется настолько ясно, насколько я могу, почему я считаю, что вывод ограничений — плохой выбор для такого языка, как Go, который предназначен для программирования в больших масштабах.

@mrkaspa Например, если у вас есть []io.Reader , вам нужно значение интерфейса, а не контракт. Контракт требует, чтобы все элементы в срезе были одного типа. Интерфейс позволит им быть разными типами, если все типы реализуют io.Reader .

@ianlancetaylor , насколько я понял, интерфейс создает новый тип, в то время как контракты ограничивают тип, но не создают новый, я прав?

@ianlancetaylor :

Не могли бы вы сделать что-то вроде следующего?

contract Reader(T) {
  T Read([]byte) (int, error)
}

func ReadAll(type T Reader)(readers []T) ([]byte, error) {
  // Use the readers...
}

Теперь ReadAll() должен принимать []io.Reader точно так же, как и []*os.File , не так ли? Кажется, что io.Reader удовлетворяет условиям контракта, и я не помню ничего в черновике о том, что значения интерфейса нельзя использовать в качестве аргументов типа.

Редактировать: Неважно. Я неправильно понял. Это все еще место, где вы будете использовать интерфейс, так что это ответ на вопрос @mrkaspa . Вы просто не используете интерфейс в сигнатуре функции; вы используете его только там, где он вызывается.

@mrkaspa Да, это правда.

@ianlancetaylor , если бы у меня был список []io.Reader и этот контракт:

contract Reader(T) {
  T Read([]byte) (int, error)
}

func ReadAll(type T Reader)(readers []T) ([]byte, error) {
  // Use the readers...
}

Я мог бы вызвать ReadAll для каждого интерфейса, потому что они удовлетворяют условиям контракта?

@ianlancetaylor уверен, что все можно распечатать, но легко придумать другие примеры, например, ведение журнала в файл или в сеть, мы хотим, чтобы ведение журнала было общим, чтобы мы могли изменить цель журнала между нулем, локальным файлом, сетевой службой и т. д. Добавление ведение журнала в листовую функцию требует добавления ограничений полностью назад к корню, включая необходимость изменения используемых сторонних библиотек.

Код не статичен, вы также должны разрешить обслуживание. На самом деле код находится в «сопровождении» намного дольше, чем требуется для первоначального написания, поэтому есть хороший аргумент в пользу того, что мы должны разрабатывать языки, чтобы упростить обслуживание, рефакторинг, добавление функций и т. д.

На самом деле эти проблемы будут проявляться только в большой кодовой базе, которая поддерживается с течением времени. Это не то, что вы можете написать для демонстрации в виде небольшого примера.

Эти проблемы существуют и в других универсальных языках, например в Аде. Вы можете перенести какое-нибудь большое приложение Ada на Go, в котором широко используются дженерики, но если проблема существует в Ada, я не вижу в Go ничего, что могло бы смягчить эту проблему.

@mrkaspa Да.

На данный момент я предлагаю, чтобы эта ветка разговора переместилась в голанг-орехи. Трекер проблем GitHub — плохое место для такого рода дискуссий.

@keean Возможно, ты прав. Время покажет. Мы явно просим людей попробовать написать код для черновика дизайна. В чисто гипотетических дискуссиях мало смысла.

@keean Я не понимаю ваш пример ведения журнала. Описанную вами проблему можно решить с помощью интерфейсов во время выполнения, а не с помощью дженериков во время компиляции.

Интерфейсы @bserdar имеют только один параметр типа, поэтому вы не можете сделать что-то, где один параметр — это то, что нужно зарегистрировать, а второй параметр типа — это тип журнала.

@keean IMO в этом примере вы бы сделали то же самое, что делаете сегодня, вообще без каких-либо параметров типа: используйте отражение, чтобы проверить то, что нужно зарегистрировать, и используйте context.Context для передачи значения журнала. Я знаю, что эти идеи отталкивают энтузиастов машинописи, но они оказываются довольно практичными. Конечно, есть ценность в ограниченных параметрах типа, поэтому мы ведем этот разговор, но я бы сказал, что причина, по которой вам приходят в голову случаи, это случаи, которые уже довольно хорошо работают в текущих кодовых базах Go в масштабе . , заключаются в том, что это не те случаи, которые действительно выигрывают от дополнительной строгой проверки типов. Что возвращается к точке зрения Янса - еще неизвестно, проявляется ли эта проблема на практике.

@merovius Если бы это зависело от меня, все отражения во время выполнения были бы запрещены, поскольку я не хочу, чтобы поставляемое программное обеспечение генерировало ошибки ввода во время выполнения, которые могут повлиять на пользователя. Это позволяет проводить более агрессивную оптимизацию компилятора, поскольку вам не нужно беспокоиться о согласовании модели времени выполнения со статической моделью.

Имея дело с миграцией крупных проектов с JavaScript на TypeScript, по моему опыту, строгая типизация становится более важной, чем больше проект и чем больше команда, работающая над ним. Это связано с тем, что вам нужно полагаться на интерфейс/контракт блока кода без необходимости смотреть на реализацию, чтобы поддерживать эффективность при работе с большой командой.

В сторону: конечно, это зависит от того, как вы достигаете масштабирования, сейчас я предпочитаю подход API-First, начиная с файла OpenAPI/Swagger JSON, а затем используя генерацию кода для создания серверных заглушек и клиентского SDK. Таким образом, OpenAPI фактически действует как ваша система типов для микросервисов.

@ianlancetaylor

Такие ограничения, как конвертируемость, присваиваемость и сравнимость, выражаются в форме типов.

Учитывая так много деталей в правилах преобразования типов Go, действительно сложно написать собственный контракт C , удовлетворяющий следующей общей функции преобразования слайсов:

func ConvertSlice(type In, Out C(In, Out)) (x []In) []Out {
    o := make([]Out, len(x))
    for i := range x {
        o[i] = Out(x[i])
    }
    return o
}

Идеальный C должен допускать конверсии:

  • между любыми целочисленными числовыми типами с плавающей запятой
  • между любыми сложными числовыми типами
  • между двумя типами, базовые типы которых идентичны
  • из типа Out , который реализует In
  • от типа канала к типу двунаправленного канала, и два типа каналов имеют идентичный тип элемента
  • связанный с тегом структуры, ...
  • ...

Насколько я понимаю, я не могу написать такой контракт. Так нужен ли нам встроенный convertible ?

Невозможно выразить ограничение, что тип имеет указанный набор полей, но я сомневаюсь, что это будет встречаться очень часто.

Учитывая, что встраивание типов часто используется в программировании на Go, я думаю, что потребности не будут редкими.

@keean Это правильное мнение, но оно явно не то, которым руководствуются при проектировании и разработке Go. Чтобы принять конструктивное участие, пожалуйста, примите это и начните работать с того места, где мы находимся , исходя из предположения, что любое развитие языка должно быть постепенным изменением статус-кво. Если вы не можете, то есть языки, которые более тесно связаны с вашими предпочтениями, и я чувствую, что все, в частности вы, были бы счастливее, если бы вы вложили в них свою энергию.

@merovius Я готов признать, что изменения в Go должны быть постепенными, и принять статус-кво.

Я просто отвечал на ваш комментарий в рамках разговора, соглашаясь с тем, что я энтузиаст печати. Я высказал мнение о runtime-рефлексии, я не предлагал Go отказаться от runtime-рефлексии. Я работаю над другими языками, использую много языков в своей работе. Я разрабатываю (медленно) свой собственный язык, но я всегда надеюсь, что развитие других языков сделает это ненужным.

@dotaheor Я согласен, что сегодня мы не можем написать генеральный контракт на конвертируемость. Мы должны увидеть, кажется ли это проблемой на практике.

Отвечая на @ianlancetaylor

Я не думаю, что пока ясно, как часто люди захотят параметризовать функции с постоянными значениями. Наиболее очевидным случаем были бы размеры массива, но вы уже можете сделать это, передав желаемый тип массива в качестве аргумента типа. Кроме этого случая, что мы действительно получаем, передавая константу в качестве аргумента времени компиляции, а не аргумента времени выполнения?

В случае с массивами просто передача (целого) типа массива в качестве аргумента типа кажется чрезвычайно ограничивающей, потому что контракт не сможет разложить ни измерение массива, ни тип элемента и наложить на них ограничения. Например, может ли контракт, принимающий «целый тип массива», требовать, чтобы тип элемента типа массива реализовывал определенные методы?

Но ваш призыв к более конкретным примерам того, как могут быть полезны нетиповые универсальные параметры, был хорошо принят, поэтому я расширил сообщение в блоге, включив в него раздел, охватывающий пару важных классов примеров использования и несколько конкретных примеров каждого из них. Поскольку прошло несколько дней, снова сообщение в блоге здесь:

Достаточно ли общих параметров типа для Go 2 Generics?

Новый раздел называется «Примеры использования дженериков вместо нетипов».

Вкратце можно сказать, что контракты на матричные и векторные операции могут налагать соответствующие ограничения как на размерность, так и на типы элементов массивов. Например, матричное умножение матрицы nxm на матрицу mxp, каждая из которых представлена ​​​​в виде двумерного массива, может правильно ограничить количество строк первой матрицы равным количеству столбцов второй матрицы и т. д.

В более общем плане дженерики могут использовать параметры, не являющиеся типом, для включения конфигурации во время компиляции и специализации кода и алгоритмов различными способами. Например, общий вариант math/big.Int может быть настроен во время компиляции для определенного бита с и/или знаком, удовлетворяя потребности в 128-битных целых числах и других неродных целых числах с фиксированной шириной с разумной эффективностью, вероятно, намного лучше. чем существующий big.Int, где все динамично. Универсальный вариант big.Float может аналогичным образом специализироваться во время компиляции с определенной точностью и/или другими параметрами времени компиляции, например, для обеспечения достаточно эффективных универсальных реализаций форматов binary16, binary128 и binary256 из IEEE 754-2008. что Go изначально не поддерживает. Многие библиотечные алгоритмы, которые могут оптимизировать свою работу на основе знания потребностей пользователя или конкретных аспектов обрабатываемых данных — например, оптимизация графового алгоритма, которая работает только с неотрицательными весами ребер или только с DAG или деревьями, или оптимизация обработки матриц, которая полагаться на матрицы, являющиеся верхним или нижним треугольным, или на арифметику больших целых чисел для криптографии, которую иногда необходимо реализовать в постоянное время, а иногда нет - можно использовать дженерики, чтобы сделать себя настраиваемым во время компиляции, чтобы зависеть от дополнительной декларативной информации, такой как это, при этом гарантируя, что все тесты этих параметров времени компиляции в реализации обычно компилируются посредством постоянного распространения.

@bford написал:

а именно, что параметры дженериков привязаны к константам во время компиляции.

Вот этот момент я не понимаю. Почему вам нужно это условие.
Теоретически можно переопределить переменные/параметры в теле. Это не имеет значения.
Интуитивно я предполагаю, что вы хотите указать, что первое приложение функции должно выполняться во время компиляции.

Но для этого требования лучше подходят такие ключевые слова, как comp или comptime.
Кроме того, если грамматика golang допускает не более двух кортежей параметров для функции, то эту аннотацию ключевого слова можно опустить, поскольку всегда будет оцениваться первый кортеж параметров типа и функции (в случае двух кортежей параметров). во время компиляции.

Еще один момент: что, если const будет расширен, чтобы разрешить выражения времени выполнения (настоящий единый вход)?

О методах Pointer vs value :

Если метод указан в контракте с простым T , а не с *T , то это может быть либо метод указателя, либо метод значения T . Чтобы не беспокоиться об этом различии, в теле универсальной функции все вызовы методов будут вызовами методов указателей. ...

Как это согласуется с реализацией интерфейса? Если T имеет некоторый метод указателя (например, MyInt в примере), можно ли T назначить интерфейсу с этим методом ( Stringer в пример)?

Разрешение этого означает наличие еще одной операции со скрытым адресом & , а не разрешение означает, что контракты и интерфейсы могут взаимодействовать только через явное переключение типа. Ни одно из решений не кажется мне хорошим.

(Примечание: нам следует пересмотреть это решение, если оно приведет к путанице или неправильному коду.)

Я вижу, что у команды уже есть некоторые сомнения по поводу этой двусмысленности в синтаксисе метода указателя. Я просто добавляю, что неоднозначность также влияет на реализацию интерфейса (и неявно добавляю свои оговорки по этому поводу).

@fJavierZunzunegui Вы правы, текущий текст подразумевает, что при присвоении значения параметра типа типу интерфейса может потребоваться неявная адресная операция. Это может быть еще одной причиной не использовать неявные адреса при вызове методов. Мы должны увидеть.

В Parameterized types , особенно в отношении параметров типа, встроенных в структуру в виде поля:

Учитывать

type Lockable(type T) struct {
    T
    sync.Locker
}

Что, если T имеет методы с именами Lock или Unlock ? Структура не будет компилироваться. Это отсутствие условия метода X не поддерживается контрактами, поэтому у нас есть недопустимый код, который не нарушает контракт (нарушая всю цель контрактов).

Это становится еще сложнее, если у вас есть несколько встроенных параметров (скажем, T1 и T2 ), поскольку у них не должно быть общих методов (опять же, не предусмотренных контрактами). Кроме того, поддержка произвольных методов в зависимости от встроенных типов способствует очень ограниченным ограничениям времени компиляции на переключатели типов для этих структур (очень похоже на утверждения и переключатели типов ).

Как я вижу, есть 2 хороших варианта:

  • запрет встраивания параметров типа вообще: просто, но за небольшие деньги (если метод нужен, его нужно явно прописать в структуре с полем).
  • ограничить вызываемые методы контрактными: аналогично встраиванию интерфейса. Это отличается от обычного go (не является целью), но бесплатно (методы не нужно явно писать в структуре с полем).

Структура не будет компилироваться.

Это будет компилироваться. Попробуй. Что не удается скомпилировать, так это вызов неоднозначного метода. Однако ваша точка зрения остается в силе.

Ваше второе решение, ограничивающее вызываемые методы теми, которые указаны в контракте, не будет работать: даже если в контракте T указаны Lock и Unlock , вы все равно не сможете не звоните им на Lockable .

@jba спасибо за советы по компиляции.

Под вторым решением я подразумеваю обращение с параметрами встроенного типа так же, как сейчас с интерфейсами, так что если метод не указан в контракте, он не будет доступен сразу после внедрения. В этом сценарии, поскольку T не имеет контракта, он фактически обрабатывается как interface{} , поэтому он не будет конфликтовать с sync.Locker , даже если экземпляр T был создан с помощью тип с этими методами. Это может помочь объяснить мою точку зрения .

В любом случае я предпочитаю первое решение (полный запрет на встраивание), поэтому, если это ваше предпочтение, нет смысла обсуждать второе! :смайлик:

Пример, предоставленный @JavierZunzunegui , также охватывает другой случай. Что, если T — это структура с полем noCopy noCopy ? Компилятор должен уметь обрабатывать и этот случай.

Не уверен, что это точно подходящее место для этого, но я хотел прокомментировать конкретный реальный вариант использования для универсальных типов, которые допускают «параметризацию нетиповых значений, таких как константы», и особенно для случая массивов . Я надеюсь, что это полезно.

В моем мире без дженериков я пишу много кода, который выглядит так:

import "math/bits"

// SigEl is the element type used in variable length bit vectors, 
// can be any unsigned integer type
type SigEl = uint

// SigElBits is the number of bits storable in each SigEl
const SigElBits = 8 << uint((^SigEl(0)>>32&1)+(^SigEl(0)>>16&1)+(^SigEl(0)>>8&1))

// HammingDist counts the number bitwise differences between two
// bit vectors b1 and b2. I want this to be generic
// Function will panic at runtime if b1 and b2 aren't of equal length.
func HammingDist(b1, b2 []SigEl) (sum int) {
    // Give the compiler a hint so it won't need to bounds check the slices in loops
    _ = b1[len(b2)-1]  
        // This switch is optimized away because SigElBits is const
    switch SigElBits {   // Yay no golang generics!
    case 64:
        _ = b2[len(b1)-1]
        for x := range b1 {
            sum += bits.OnesCount64(uint64(b1[x] ^ b2[x]))
        }
    case 32:
        _ = b2[len(b1)-1]
        for x := range b1 {
            sum += bits.OnesCount32(uint32(b1[x] ^ b2[x]))
        }
    case 16:
        _ = b2[len(b1)-1]
        for x := range b1 {
            sum += bits.OnesCount16(uint16(b1[x] ^ b2[x]))
        }
    case 8:
        _ = b2[len(b1)-1]
        for x := range b1 {
            sum += bits.OnesCount8(uint8(b1[x] ^ b2[x]))
        }
    }
    return sum
}

Это работает достаточно хорошо, с одной морщинкой. Мне часто нужны сотни миллионов []SigEl s, и их длина часто составляет 128-384 бита. Поскольку срезы создают фиксированные 192-битные служебные данные сверх размера базового массива, когда размер самого массива составляет 384 бита или меньше, это приводит к ненужным 50–150% служебных данных памяти, что, очевидно, ужасно.

Мое решение состоит в том, чтобы выделить часть Sig _arrays_, а затем нарезать их на лету в качестве параметров для HammingDist выше:

const SigBits = 256  // Any multiple of SigElBits is valid

// Sig is the bit vector array type
type Sig [SigBits/SigElBits]SigEl

bitVects := make([]Sig, 100000000)
// stuff happens ... 

// Note slicing below, just to make the arrays "generic" for the call 
dist := HammingDist(bitVects[x][:], bitVects[y][:])

Что бы я хотел сделать вместо всего этого, так это определить общий тип подписи и переписать все вышеперечисленное как (что-то вроде):

contract UnsignedInteger(T) {
    T uint, uint8, uint16, uint32, uint64
}

type Signature (type Element UnsignedInteger, n int) [n]Element

// HammingDist counts the number bitwise differences between two bit vectors
func HammingDist(b1, b2 *Signature) (sum int) {
    for x := range *b1 {
        // Assuming the std lib bits.OnesCount becomes generic over 
        // all UnsignedInteger types
        sum += bits.OnesCount(*b1[x] ^ *b2[x])
    }
    return sum
}

Итак, чтобы использовать эту библиотеку:

type sigEl = uint   // Any unsigned int type
const sigElBits = 8 << uint((^SigEl(0)>>32&1)+(^SigEl(0)>>16&1)+(^SigEl(0)>>8&1))
const sigBits = 256  // Any multiple of SigElBits is valid
type sig Signature(sigEl, sigBits/sigElBits)

bitVects := make([]sig, 100000000)
// stuff happens ... 

dist := HammingDist(&bitVects[x], &bitVects[y])

Инженер может мечтать... 🤖

Если вы знаете, насколько большой может быть максимальная длина бита, вы можете вместо этого использовать что-то вроде этого:

contract uintArrayOfFixedLength(ElemType,ArrayType)
{
    ArrayType [1]ElemType,[2]ElemType,...,[maxBit]ElemType
    ElemType uint8,uint16,uint32,uint64
}

func HammingDist(type ElemType,ArrayType uintArrayOfFixedLength)(t1,t2 ArrayType) (sum int)
{

}

@vsivsi Я не уверен, что понимаю, как, по вашему мнению, это улучшит ситуацию - возможно, вы предполагаете, что компилятор сгенерирует конкретизированную версию этой функции для каждой возможной длины массива? Потому что ISTM, что а) это маловероятно, поэтому б) вы получите точно такие же характеристики производительности, как сейчас. Наиболее вероятная реализация, IMO, по-прежнему будет заключаться в том, что компилятор передает длину и указатель на первый элемент, поэтому вы фактически все равно передаете срез в сгенерированном коде (я имею в виду, что вы не передаете емкость, но я не думаю, что дополнительное слово в стеке действительно имеет значение).

Честно говоря, ИМО, то, что вы говорите, является довольно хорошим примером чрезмерного использования дженериков, где они не нужны - «массив неопределенной длины» - это именно то, для чего нужны срезы.

@Merovius Спасибо, я думаю, что ваш комментарий раскрывает пару интересных тем для обсуждения.

«массив неопределенной длины» - это именно то, для чего нужны срезы.

Правильно, но в моем примере нет массивов неопределенной длины. Длина массива является известной константой во время компиляции. Именно для этого и нужны массивы, но они недостаточно используются в golang IMO, потому что они такие негибкие.

Чтобы было ясно, я не предлагаю

type Signature (type Element UnsignedInteger, n int) [n]Element

означает, что n является переменной времени выполнения. Он должен оставаться константой в том же смысле, что и сегодня:

const n = 10
type nArray [n]uint               // works
type nSigInt Signature(uint, n)   // works 

var m = int(n)
type mArray [m]uint               // error
type mSigInt Signature(uint, m)   // error 

Итак, давайте посмотрим на «стоимость» функции HammingDist на основе среза. Я согласен с тем, что разница между передачей массива как bitVects[x][:] и &bitVects[x] невелика (максимум в 3 раза). Настоящая разница заключается в проверке кода и времени выполнения, которая должна происходить внутри этой функции.

В версии на основе слайсов код среды выполнения должен проверять доступ к слайсам, чтобы обеспечить безопасность памяти. Это означает, что эта версия кода может вызвать панику (или для предотвращения этого необходим явный механизм проверки ошибок и возврата). Присвоения NOP ( _ = b1[len(b2)-1] ) существенно влияют на производительность, давая оптимизатору компилятора подсказку, что ему не нужно проверять границы каждого доступа к слайсу в цикле. Но эти минимальные проверки границ по-прежнему необходимы, даже несмотря на то, что передаваемые базовые массивы всегда имеют одинаковую длину. Кроме того, у компилятора могут возникнуть трудности с оптимизацией цикла for/range (скажем, с помощью unrolling ).

Напротив, универсальная версия функции на основе массива не может вызывать панику во время выполнения (не требует обработки ошибок) и обходит необходимость какой-либо логики проверки условных границ. Я очень сомневаюсь, что скомпилированная универсальная версия функции должна будет «передавать» длину массива, как вы предлагаете, потому что это буквально постоянное значение, которое является частью экземпляра типа во время компиляции.

Кроме того, для небольших размеров массива (что важно в моем случае) компилятору было бы легко развернуть или даже полностью оптимизировать цикл for/range для приличного прироста производительности, поскольку во время компиляции он будет знать, каковы эти размеры. .

Другим большим преимуществом универсальной версии кода является то, что он позволяет пользователю модуля HammingDist определять тип unsigned int в своем собственном коде. Неуниверсальная версия требует, чтобы сам модуль был изменен для изменения определенного типа SigEl , поскольку нет способа «передать» тип модулю. Следствием этого различия является то, что реализация функции расстояния становится проще, когда нет необходимости писать отдельный код для каждого из {8,16,32,64}-битных случаев uint.

Затраты на версию функции на основе слайсов и необходимость модифицировать код библиотеки для установки типа элемента являются весьма неоптимальными уступками, необходимыми для того, чтобы избежать необходимости реализовывать и поддерживать версии этой функции NxM. Общая поддержка (постоянных) параметризованных типов массивов решила бы эту проблему:

// With generics + parameterized constant array lengths:
type Signature (type Element UnsignedInteger, n int) [n]Element
func HammingDist(b1, b2 *Signature) (sum int) { ... }

// Without generics
func HammingDistL1Uint(b1, b2 [1]uint) (sum int) { ... }
func HammingDistL1Uint8(b1, b2 [1]uint8) (sum int) { ... }
func HammingDistL1Uint16(b1, b2 [1]uint16) (sum int) { ... }
func HammingDistL1Uint32(b1, b2 [1]uint32) (sum int) { ... }
func HammingDistL1Uint64(b1, b2 [1]uint64) (sum int) { ... }

func HammingDistL2Uint(b1, b2 [2]uint) (sum int) { ... }
func HammingDistL2Uint8(b1, b2 [2]uint8) (sum int) { ... }
func HammingDistL2Uint16(b1, b2 [2]uint16) (sum int) { ... }
func HammingDistL2Uint32(b1, b2 [2]uint32) (sum int) { ... }
func HammingDistL2Uint64(b1, b2 [2]uint64) (sum int) { ... }

func HammingDistL3Uint(b1, b2 [3]uint) (sum int) { ... }
func HammingDistL3Uint8(b1, b2 [3]uint8) (sum int) { ... }
func HammingDistL3Uint16(b1, b2 [3]uint16) (sum int) { ... }
func HammingDistL3Uint32(b1, b2 [3]uint32) (sum int) { ... }
func HammingDistL3Uint64(b1, b2 [3]uint64) (sum int) { ... }

// and L4, L5, L6 ... ad nauseum

Избегание вышеупомянутого кошмара или очень реальных затрат на текущие альтернативы кажется мне _противоположным_ "общему чрезмерному использованию". Я согласен с @sighoya в том, что перечисление всех допустимых длин массивов в контракте может работать для очень ограниченного набора случаев, но я считаю, что это слишком ограничено даже для моего случая, поскольку даже если я установлю верхнюю границу поддержки на уровне всего 384 бита, что потребовало бы почти 50 условий в пункте ArrayType [1]ElemType,[2]ElemType,...,[maxBit]ElemType контракта, чтобы покрыть случай uint8 .

Правильно, но в моем примере нет массивов неопределенной длины. Длина массива является известной константой во время компиляции.

Я понимаю это, но обратите внимание, что я не сказал «во время выполнения». Вы хотите написать код, не обращающий внимания на длину массива. Слайсы уже умеют это делать.

Я очень сомневаюсь, что скомпилированная универсальная версия функции должна будет «передавать» длину массива, как вы предлагаете, потому что это буквально постоянное значение, которое является частью экземпляра типа во время компиляции.

Обобщенная версия функции была бы такой, потому что каждый экземпляр этого типа использует другую константу. Вот почему у меня сложилось впечатление, что вы предполагаете, что сгенерированный код не будет общим, а будет расширен для каждого типа. то есть вы, кажется, предполагаете, что будет создано несколько экземпляров этой функции, для [1]Element , [2]Element и т. д. Я говорю, что это кажется мне маловероятным, что это кажется более вероятным что будет сгенерирована одна версия, которая по существу эквивалентна версии слайса.

Конечно, это не должно быть так. Итак, да, вы правы в том, что вам не нужно передавать длину массива. Я просто сильно предсказываю, что это будет реализовано таким образом, и кажется сомнительным предположение, что этого не произойдет. (FWIW, я бы также сказал, что если вы хотите, чтобы компилятор генерировал специализированные тела функций для отдельных длин, он мог бы также сделать это прозрачно и для срезов, но это другое обсуждение).

Другое большое преимущество универсальной версии кода

Чтобы уточнить: под «универсальной версией» вы имеете в виду общую идею дженериков, реализованную, например, в текущем проекте дизайна контрактов, или вы имеете в виду более конкретно дженерики с нетиповыми параметрами? Потому что преимущества, которые вы называете в этом абзаце, применимы и к текущему дизайн-проекту контрактов.

Я не пытаюсь здесь выступать против дженериков вообще. Я просто объясняю, почему я не думаю, что ваш пример служит для демонстрации того, что нам нужны другие типы параметров, кроме типов.

// With generics + parameterized constant array lengths:
// Without generics

Это ложная дихотомия (и настолько очевидная, что я немного разочарован вами). Также есть «с параметрами типа, но без целочисленных параметров»:

contract Unsigned(T) {
    T uint, uint8, uint16, uint32, uint64
}
func HammingDist(type T Unsigned) (b1, b2 []T) (sum int) {
    if len(b1) != len(b2) {
        panic("slices of different lengths passed to HammingDist")
    }
    for i := range b1 {
        sum += bits.OnesCount(b1[i]^b2[i]) // Same assumption about OnesCount being generic you made above
    }
    return sum
}

Что мне кажется нормальным. Это немного менее безопасно для типов, требуя паники во время выполнения, если типы не совпадают. Но, и это моя точка зрения, это единственное преимущество добавления нетиповых универсальных параметров в вашем примере (и это преимущество, которое уже было ясно, ИМО). Прирост производительности, который вы прогнозируете, зависит от довольно строгих предположений о том, как реализуются дженерики в целом и дженерики по сравнению с нетиповыми параметрами в частности. Это я лично не считаю очень вероятным, основываясь на том, что я слышал от команды Go до сих пор.

Я очень сомневаюсь, что скомпилированная универсальная версия функции должна будет «передавать» длину массива, как вы предлагаете, потому что это буквально постоянное значение, которое является частью экземпляра типа во время компиляции.

Вы просто предполагаете, что дженерики будут работать как шаблоны C++ и дублировать реализации функций, но это просто неправильно. Предложение явно допускает отдельные реализации со скрытыми параметрами.

Я думаю, что если вам действительно нужен шаблонный код для небольшого числа числовых типов, не так уж сложно использовать генератор кода. Обобщения действительно стоят сложности кода только для таких вещей, как типы контейнеров, где есть измеримое преимущество в производительности от использования примитивных типов, но вы не можете разумно ожидать, что просто сгенерируете небольшое количество шаблонов кода заранее.

Я, очевидно, понятия не имею, как сопровождающие golang в конечном итоге что-то реализуют, поэтому я воздержусь от дальнейших предположений и с радостью уступлю тем, у кого больше инсайдерских знаний.

Что я знаю, так это то, что для примера реальной проблемы, о которой я рассказал выше, потенциальная разница в производительности между текущей реализацией на основе слайсов и хорошо оптимизированной универсальной реализацией на основе массива существенна.

BenchmarkHD/256-bit_unrolled_array_HD-20            2000000000           1.05 ns/op        0 B/op          0 allocs/op
BenchmarkHD/256-bit_slice_HD-20                     300000000            5.10 ns/op        0 B/op          0 allocs/op

Код по адресу: https://github.com/vsivsi/hdtest

Это 5-кратная потенциальная разница в производительности для 4x64-битного случая (наилучшее место в моей работе) с небольшим развертыванием цикла (и, по сути, без дополнительного генерируемого кода) в случае с массивом. Эти вычисления находятся во внутренних циклах моих алгоритмов, производились буквально много триллионов раз, поэтому разница в производительности в 5 раз довольно велика. Но чтобы реализовать этот прирост эффективности сегодня, мне нужно написать каждую версию функции для каждого необходимого типа элемента и длины массива.

Но да, если мейнтейнеры никогда не реализуют такие оптимизации, то все усилия по добавлению параметризованных длин массивов к дженерикам будут бессмысленными, по крайней мере, поскольку это может принести пользу этому примеру.

В любом случае, интересное обсуждение. Я знаю, что это спорные вопросы, так что спасибо за вежливость!

@vsivsi FWIW, выигрыши, которые вы наблюдаете, исчезают, если вы не разворачиваете свои циклы вручную (или если вы также разворачиваете цикл по срезу) - так что это все еще на самом деле не поддерживает ваш аргумент о том, что целочисленные параметры помогают, потому что они позволяют компилятор сделает развертывание за вас. Мне кажется плохой наукой спорить X с Y, основываясь на том, что компилятор становится сколь угодно умным для X и остается сколь угодно тупым для Y. Мне непонятно, почему другая эвристика развертывания срабатывает в случае зацикливания массива , но не срабатывает в случае зацикливания фрагмента, длина которого известна во время компиляции. Вы не показываете преимущества одного вида дженериков по сравнению с другим, вы показываете преимущества этой другой эвристики развертывания.

Но в любом случае никто на самом деле не утверждал, что генерация специализированного кода для каждого экземпляра универсальной функции не будет потенциально быстрее — просто есть другие компромиссы, которые следует учитывать при принятии решения, хотите ли вы это сделать.

@Merovius Я думаю, что самым сильным аргументом в пользу дженериков в такого рода примерах является проработка времени компиляции (таким образом, выдача уникальной функции для каждого целого числа уровня типа), где код, который нужно специализировать, находится в библиотеке. Если пользователь библиотеки собирается использовать ограниченное количество экземпляров функции, он получает преимущество оптимизированной версии. Поэтому, если мой код использует только массивы длиной 64, я могу использовать оптимизированные разработки функций библиотеки для длины 64.

В этом конкретном случае это зависит от частотного распределения длин массивов, потому что мы можем не захотеть разрабатывать все возможные функции, если их тысячи из-за ограничений памяти и очистки кэша страниц, что может замедлить работу. Если, например, малые размеры являются обычным явлением, но возможны и большие (распределение с длинным хвостом по размеру), то мы можем разработать специализированные функции для небольших целых чисел с развернутыми циклами (скажем, от 1 до 64), а затем предоставить единую обобщенную версию со скрытым -параметр для остальных.

Мне не нравится идея «произвольно умного компилятора», и я думаю, что это плохой аргумент. Как долго мне придется ждать этого сколь угодно умного компилятора? Мне особенно не нравится идея компилятора, изменяющего типы, например, оптимизирующего слайс для массива, делая скрытые специализации в языке с отражением, поскольку, когда вы размышляете над этим слайсом, может произойти что-то неожиданное.

Что касается «общей дилеммы», лично я бы выбрал «заставить компилятор работать медленнее/выполнять больше работы», но постарайтесь сделать это как можно быстрее, используя хорошую реализацию и отдельную компиляцию. Rust, похоже, работает неплохо, и после недавнего объявления Intel кажется, что он может в конечном итоге заменить «C» в качестве основного языка системного программирования. Похоже, что время компиляции даже не повлияло на решение Intel, поскольку ключевыми факторами были оперативная память и безопасность параллелизма со скоростью, подобной «C». «Черты» Rust — это разумная реализация универсальных классов типов, у них есть несколько раздражающих угловых случаев, которые, я думаю, исходят из их дизайна системы типов.

Возвращаясь к нашему предыдущему обсуждению, я должен быть осторожным, чтобы отделить обсуждение дженериков в целом от того, как они могут конкретно применяться к Go. Таким образом, я не уверен, что в Go должны быть дженерики, поскольку это усложняет то, что является простым и элегантным языком, во многом так же, как «C» не имеет дженериков. Я все еще думаю, что на рынке существует пробел для языка, который имеет общие реализации в качестве основной функции, но остается простым и элегантным.

Мне интересно, есть ли в этом какой-то прогресс.

Как долго я могу пробовать дженерики. Я ждал долгое время

@Nsgj Вы можете проверить этот CL: https://go-review.googlesource.com/c/go/+/187317/

В текущей спецификации это возможно?

contract Point(T) {
  T struct { X, Y float64 }
}

Другими словами, тип должен быть структурой с двумя полями, X и Y, типа float64.

редактировать: с примером использования

func generate(type T Point)() T {
  return T{X: randomFloat64(), Y: randomFloat64()}
}

@abuchanan-nr Да, текущий проект дизайна позволяет это, хотя трудно понять, насколько это будет полезно.

Я также не уверен, что это полезно, но я не видел четкого примера использования пользовательского типа структуры в списке типов контракта. В большинстве примеров используются встроенные типы.

FWIW, я представлял себе библиотеку 2D-графики. Вы можете захотеть, чтобы каждая вершина имела несколько полей для конкретного приложения, таких как цвет, сила и т. д. Но вам также может понадобиться общая библиотека методов и алгоритмов только для геометрической части, которая на самом деле зависит только от координат X, Y. Было бы неплохо передать ваш пользовательский тип вершины в эту библиотеку, например

type MyVertex struct {
  X, Y float64
  Color color.Color
  OtherAttr int
}
p := geo.RandomPolygon(MyVertex)()

for _, vert := range p.Vertices() {
  p.Color = randColor()
}

Опять же, не уверен, что на практике это окажется хорошим дизайном, но в то время это было моим воображением :)

См. https://godoc.org/image#Image , чтобы узнать, как это делается в стандартном Go сегодня.

Что касается Операторов/Типов в контрактах :

Это приводит к дублированию многих универсальных методов, так как они нам понадобятся в формате оператора ( + , == , < , ...) и формате метода ( Plus(T) T , Equal(T) bool , LessThan(T) bool , ...).

Я предлагаю объединить эти два подхода в один, формат метода. Для этого предварительно объявленные типы ( int , int64 , string , ...) должны быть приведены к типам произвольными методами. Для (тривиального) простого случая это уже возможно ( type MyInt int; func (i MyInt) LessThan(o MyInt) bool {return int(i) < int(o)} ), но реальная ценность заключается в составных типах ( []int -> []MyInt , map[int]struct{} -> map[MyInt]struct{} и т. д. для канала, указателя и т. д.), что не допускается (см. FAQ ). Разрешение этих преобразований само по себе является значительным изменением, поэтому я расширил технические аспекты в Предложении по упрощенному преобразованию типов . Это позволило бы универсальным функциям не иметь дело с операторами и по-прежнему поддерживать все типы, включая предварительно объявленные.

Обратите внимание, что это изменение также полезно для не объявленных заранее типов. В соответствии с текущим предложением, учитывая type X struct{S string} (который исходит из внешней библиотеки, поэтому вы не можете добавлять к нему методы), скажите, что у вас есть []X и вы хотите передать его универсальной функции. ожидая []T , за T удовлетворяя Stringer . Для этого потребуется type X2 X; func(x X2) String() string {return x.S} и глубокая копия []X в []X2 . В соответствии с предлагаемыми изменениями в этом предложении вы полностью сохраняете глубокую копию.

ПРИМЕЧАНИЕ. Упомянутое Предложение о преобразовании ослабленного типа требует проверки.

@JavierZunzunegui Предоставление «формата метода» (или формата оператора) для основных унарных/бинарных операторов не является проблемой. Довольно просто ввести такие методы, как +(x int) int , просто разрешив символы операторов в качестве имен методов, и распространить это на встроенные типы (хотя даже это не работает для сдвигов, поскольку правый оператор может быть произвольный целочисленный тип — на данный момент у нас нет способа выразить это). Проблема в том, что этого недостаточно. Одной из вещей, которые должен выразить контракт, является то, может ли значение x типа X быть преобразовано в тип параметра типа T , как в T(x) (и наоборот). То есть нужно придумать "формат метода" допустимых преобразований. Кроме того, должен быть способ выразить, что нетипизированная константа c может быть присвоена (или преобразована) переменной типа параметра типа T : допустимо ли присваивать, скажем, 256 в t типа T ? Что, если T равно byte ? Есть еще несколько подобных вещей. Для этих вещей можно придумать нотацию "формат метода", но она быстро усложняется, и непонятно, что более понятно или читабельно.

Я не говорю, что это невозможно сделать, но мы не нашли удовлетворительного и четкого подхода. С другой стороны, текущий проект дизайна, который просто перечисляет типы, довольно прост для понимания.

@griesemer Это может быть сложно в Go из-за других приоритетов, но в целом это довольно хорошо решаемая проблема. Это одна из причин, по которой я считаю неявные преобразования плохими. Есть и другие причины, такие как волшебство, невидимое для тех, кто читает код.

Если в системе типов нет неявных преобразований, то я могу использовать перегрузку, чтобы точно контролировать диапазон принимаемых типов, а интерфейсы управляют перегрузкой.

Я склонен выражать сходство между типами с помощью интерфейсов, поэтому такие операции, как «+», будут выражаться в общем виде как операции над числовым интерфейсом, а не над типом. Вам нужны переменные типа, а также интерфейсы для выражения ограничения, согласно которому и аргументы, и результат сложения должны быть одного типа.

Итак, здесь объявлено, что оператор сложения работает с типами с числовым интерфейсом. Это прекрасно сочетается с математикой, где, например, «целые числа» и «сложение» образуют «группу».

В итоге вы получите что-то вроде:

+(T Addable)(x T, y T) T

Если вы разрешите неявный выбор интерфейса, то оператор «+» может быть просто методом числового интерфейса, но я думаю, что это вызовет проблемы с выбором метода в Go?

@griesemer по поводу конверсий:

Одной из вещей, которые должен выразить контракт, является то, может ли значение x типа X быть преобразовано в тип параметра типа T, как в T(x) (и наоборот). То есть нужно придумать "формат метода" допустимых преобразований

Я понимаю, что это будет осложнением, но я не думаю, что это необходимо. Как я понимаю, такие преобразования будут происходить вне общего кода вызывающей стороной. Пример (с использованием Stringify в соответствии с эскизным проектом):

Stringify(int)([]int{1,2}) // does not compile
type MyInt int
func (i MyInt) String() string {...}
Stringify(MyInt)([]MyInt([]int{1,2})) // OK. Generic type MyInt could be inferred

Выше, что касается Stringify , аргумент имеет тип []MyInt и соответствует контракту. Универсальный код не может преобразовывать универсальные типы во что-либо еще (кроме интерфейсов, которые они реализуют в соответствии с контрактом), именно потому, что в их контракте об этом ничего не сказано.

@JavierZunzunegui Я не понимаю, как вызывающая сторона может выполнять такие преобразования, не раскрывая их в интерфейсе/контракте. Например, я мог бы захотеть реализовать общий числовой алгоритм (параметризованную функцию), работающий с различными целыми числами или типами с плавающей запятой. В рамках этого алгоритма код функции должен присваивать постоянные значения c1 , c2 и т. д. значениям типа параметра T . Я не понимаю, как код может сделать это, не зная, что можно присвоить эти константы переменной типа T . (Конечно, не хотелось бы передавать эти константы в функцию.)

func NumericAlgorithm(type T SomeContract)(vector []T) T {
   ...
   vector[i] = 3.1415  // <<< how do we know this is valid without the contract telling us?
   ...
}

необходимо присвоить постоянные значения c1 , c2 и т. д. значениям типа параметра T

@griesemer Я бы (с моей точки зрения, какими дженериками являются/должны быть) сказал, что приведенное выше является неправильной постановкой проблемы. Вы требуете, чтобы T определялся как float32 , но в контракте указывается только, какие методы доступны для T , а не то, как он определен. Если вам это нужно, вы можете либо сохранить vector как []T и потребовать аргумент func(float32) T ( vector[i] = f(c1) ), либо гораздо лучше сохранить vector как []float32 и требует T по контракту, чтобы иметь метод DoSomething(float32) или DoSomething([]float32) , так как я предполагаю, что T и поплавки должны взаимодействовать в какой-то момент. Это означает, что T может быть определено или не определено как type T float32 , все, что мы можем сказать, это то, что у него есть методы, требуемые от него по контракту.

@JavierZunzunegui Я вовсе не говорю, что T можно определить как float32 — это может быть float32 , float64 или даже один из сложные виды. В более общем случае, если бы константа была целым числом, могло бы быть множество целочисленных типов, которые можно было бы передавать в эту функцию, а некоторые — нет. Это, конечно, не "неправильная постановка задачи". Проблема реальна - это, конечно, вовсе не надумано, чтобы иметь возможность писать такие функции - и проблема не исчезнет, ​​объявив ее "неправильной".

@griesemer Понятно , я думал, что вас интересует только преобразование, я не зарегистрировал ключевой элемент, который имеет дело с нетипизированными константами.

Вы можете сделать как в моем ответе выше, когда T имеет метод DoSomething(X) , а функция принимает дополнительный аргумент func(float64) X , поэтому общая форма определяется двумя типами ( T,X ). Как вы описываете проблему X обычно float32 или float64 и аргумент функции func(f float64) float32 {return float32(f)} или func(f float64) float64 {return f} .

Что еще более важно, как вы подчеркиваете, для целочисленного случая существует проблема, заключающаяся в том, что менее точных целочисленных форматов может быть недостаточно для данной константы. Самым безопасным подходом становится сохранение двухтипной ( T,X ) универсальной функции в секрете и публичное раскрытие только MyFunc32 / MyFunc64 / и т. д.

Я допускаю, что MyFunc32(int32) / MyFunc64(int64) /и т. д. менее практичен, чем одиночный MyFunc(type T Numeric) (противоположность неоправданна!). Но это только для универсальных реализаций, основанных на константе, и в первую очередь на целочисленной константе — сколько их? В остальном вы получаете дополнительную свободу, не ограничиваясь несколькими встроенными типами, за T .

И, конечно же, если функция не дорогая, вы можете совершенно нормально выполнять расчет как int64 / float64 и выставлять только это, сохраняя его простым и неограниченным на T .

Мы действительно не можем сказать людям: «Вы можете писать универсальные функции для любого типа T, но эти универсальные функции не могут использовать нетипизированные константы». Go — это, прежде всего, простой язык. Языки с такими причудливыми ограничениями непросты.

Каждый раз, когда предлагаемый подход к дженерикам становится трудно объяснить простым способом, мы должны отказаться от этого подхода. Гораздо важнее сохранить простоту языка, чем добавлять в него дженерики.

@JavierZunzunegui Одно из интересных свойств параметризованного (универсального) кода заключается в том, что компилятор может настраивать его на основе типов, с которыми создается код. Например, можно использовать тип byte , а не int , потому что это приводит к значительной экономии места (представьте себе функцию, которая выделяет огромные фрагменты универсального типа). Таким образом, простое ограничение кода «достаточно большим» типом является неудовлетворительным ответом, даже для «самоуверенного» языка, такого как Go.

Кроме того, речь идет не только об алгоритмах, использующих «большие» нетипизированные константы, которые могут быть не так распространены: отмахиваться от таких алгоритмов вопросом «сколько их вообще существует» — это просто махать рукой, чтобы отклонить проблему, которая действительно существует. Просто для вашего сведения: для большого количества алгоритмов кажется разумным использовать целочисленные константы, такие как -1, 0, 1. Обратите внимание, что нельзя использовать -1 в сочетании с нетипизированными целыми числами, просто чтобы дать вам простой пример. Ясно, что мы не можем просто игнорировать это. Нам нужно указать это в договоре.

@ianlancetaylor @griesemer спасибо за отзыв - я вижу, что в моем предложенном изменении есть значительный конфликт с нетипизированными константами и отрицательными целыми числами, я оставлю это позади.

Могу я обратить ваше внимание на второй пункт в https://github.com/golang/go/issues/15292#issuecomment -546313279:

Обратите внимание, что это изменение также полезно для не объявленных заранее типов. Согласно текущему предложению, учитывая тип X struct{S string} (который исходит из внешней библиотеки, поэтому вы не можете добавлять к нему методы), скажем, у вас есть []X и вы хотите передать его универсальной функции, ожидающей [ ]T, для T, удовлетворяющего контракту Стрингера. Для этого потребуется тип X2 X; func(x X2) String() string {return xS} и глубокая копия []X в []X2. В соответствии с предлагаемыми изменениями в этом предложении вы полностью сохраняете глубокую копию.

Ослабление правил преобразования (если это технически осуществимо) по-прежнему было бы полезным.

@JavierZunzunegui Обсуждение преобразований типа []B([]A) , если B(a)a типа A ) разрешено, по-видимому, в основном ортогональны общим функциям. Я думаю, нам не нужно приносить это сюда.

@ianlancetaylor Я не уверен, насколько это актуально для Go, но я не думаю, что константы на самом деле нетипизированы, они должны иметь тип, поскольку компилятор должен выбрать машинное представление. Я думаю, что более подходящим термином являются константы неопределенного типа, поскольку константа может быть представлена ​​несколькими различными типами. Одним из решений является использование типа объединения, чтобы константа, такая как 27 , имела бы тип, подобный int16|int32|float16|float32 , объединение всех возможных типов. Тогда T в универсальном типе может быть этим типом объединения. Единственное требование состоит в том, что мы должны в какой-то момент преобразовать объединение в один тип. Наиболее проблематичным случаем будет что-то вроде print(27) , потому что никогда не существует единственного типа для разрешения, в таких случаях подойдет любой тип в объединении, и мы можем выбрать на основе параметра оптимизации, такого как пространство/скорость и т. д. .

@keean Точное имя и обработка того, что спецификация называет «нетипизированными константами», не относятся к теме этого вопроса. Давайте, пожалуйста, перенесем это обсуждение в другое место. Спасибо.

@ianlancetaylor Я рад, однако это одна из причин, почему я думаю, что Go не может иметь чистую / простую реализацию дженериков, все эти проблемы взаимосвязаны, и первоначальный выбор, сделанный для Go, не был сделан с учетом универсального программирования. Я думаю, что необходим другой язык, разработанный для того, чтобы сделать дженерики простыми по дизайну, для Go дженерики всегда будут чем-то добавленным к языку позже, и лучшим вариантом для сохранения чистоты и простоты языка может быть их полное отсутствие.

Если бы я сегодня разработал простой язык с быстрым временем компиляции и сопоставимой гибкостью, я бы выбрал перегрузку методов и структурный полиморфизм (подтипирование) через интерфейсы golang, а не дженерики. На самом деле это позволило бы перегружать разные анонимные интерфейсы с разными полями.

Выбор дженериков имеет преимущество повторного использования чистого кода, но он вносит больше шума, который усложняется, если добавляются ограничения, иногда приводящие к трудно понятному коду.
Тогда, если у нас есть дженерики, почему бы не использовать расширенную систему ограничений, такую ​​как предложение where, типы с более высоким родством или, возможно, типы с более высоким рангом, а также зависимую типизацию?
Все эти вопросы рано или поздно возникнут, если мы перейдем на дженерики.

Ясно говоря, я не против дженериков, но я размышляю, стоит ли это делать, чтобы сохранить простоту go.

Если введение дженериков в go неизбежно, то было бы разумно задуматься о влиянии на время компиляции при мономорфизации дженериков.
Не было бы хорошим выбором по умолчанию для генериков коробки, т.е. создания одной копии для всех типов ввода вместе и специализации только в том случае, если пользователь явно запросил с некоторой аннотацией в определении или на сайте вызова?

Что касается влияния на производительность во время выполнения, это снизит производительность из-за проблемы с боксом/распаковкой, в противном случае есть инженеры С++ экспертного уровня, рекомендующие боксовые дженерики, такие как java, чтобы смягчить промахи кеша.

@ianlancetaylor @griesemer Я пересмотрел проблему нетипизированных констант и «неоператорных» дженериков (https://github.com/golang/go/issues/15292#issuecomment-547166519) и нашел лучший способ справиться с этим.

Дайте числовые типы ( type MyInt32 int32 , type MyInt64 int64 , ...), у них есть много методов, удовлетворяющих одному и тому же контракту ( Add(T) T , ...), но критически не другие, которые будет риск переполнения func(MyInt64) FromI64(int64) MyInt64 , но нет ~ func(MyInt32) FromI64(int64) MyInt32 ~. Это позволяет безопасно использовать числовые константы (явно присвоенные наименьшему требуемому значению точности) (1) , поскольку числовые типы с низкой точностью не будут удовлетворять требуемому контракту, но все более высокие будут. См. Игровую площадку , используя интерфейсы вместо дженериков.

Преимущество ослабления числовых дженериков помимо встроенных типов (не специфичных для этой последней версии, поэтому я должен был поделиться ими на прошлой неделе) заключается в том, что это позволяет создавать экземпляры универсальных методов с типами проверки переполнения — см. Playground . Проверка переполнения сама по себе является очень популярным запросом/предложением (https://github.com/golang/go/issues/31500 и связанные вопросы).


(1) : гарантия времени компиляции без переполнения для нетипизированных констант сильна в одной и той же «ветке» ( int[8/16/32/64] и uint[8/16/32/64] ). Пересекая ветки, константа uint[X] безопасно создается только в int[2X+] , а константа int[X] вообще не может быть безопасно создана ни одной uint[X] . Даже их ослабление (разрешение int[X]<->uint[X] ) было бы простым и безопасным в соответствии с некоторыми минимальными стандартами, и, что особенно важно, любая сложность ложится на автора универсального кода, а не на пользователя универсального (который занимается только контрактом). , и можно ожидать, что любой числовой тип, который ему соответствует, является допустимым).

Общие методы - это падение Java!

@ianlancetaylor Я рад, однако это одна из причин, почему я думаю, что Go не может иметь чистую / простую реализацию дженериков, все эти проблемы взаимосвязаны, и первоначальный выбор, сделанный для Go, не был сделан с учетом универсального программирования. Я думаю, что необходим другой язык, разработанный для того, чтобы сделать дженерики простыми по дизайну, для Go дженерики всегда будут чем-то добавленным к языку позже, и лучшим вариантом для сохранения чистоты и простоты языка может быть их полное отсутствие.

Я согласен на 100%. Как бы я ни хотел увидеть реализацию каких-то дженериков, я думаю, что то, что вы, ребята, сейчас готовите, разрушит простоту языка Go.

Текущая идея расширения интерфейсов выглядит так:

type I1(type P1) interface {
        m1(x P1)
}

type I2(type P1, P2) interface {
        m2(x P1) P2
        type int, float64
}

func f(type P1 I1(P1), P2 I2(P1, P2)) (x P1, y P2) P2

Извините всех, но, пожалуйста, не делайте этого! Это искажает красоту Go.

Написав почти 100 000 строк кода на Go, я согласен с тем, что у меня нет дженериков.

Однако такие мелочи, как поддержка

// Allow mulitple types in Slices and Maps declarations
func Reverse(s []<int,string>) {
    first := 0
    last := len(s) - 1
    for first < last {
        s[first], s[last] = s[last], s[first]
        first++
        last--
    }
}

//  Allow multiple types in variable declarations
func Index (s <string, []byte>, b byte) int {
    for i := 0; i < len(s); i++ {
        if s[i] == b {
            return i
        }
    }
    return -1
}

// Allow slices and maps declarations with interface values
func ToStrings (s []Stringer) []string {
    r := make([]string, len(s))
    for i, v := range s {
        r[i] = v.String()
    }
    return r
}

помог бы.

Предложение по синтаксису, позволяющее полностью отделить дженерики от обычного кода Go.

package graph

// Example how you would define generics completely separat from Go 1 code
contract (Node, Edge)G {
    Node Edges() []Edge
    Edge Nodes() (from, to Node)
}

type (type Node, Edge G) ( Graph )
func (type Node, Edge G) ( New )
const _ = (Node, Edge) Graph

// Unmodified Go 1 code
type Graph struct { ... }
func New(nodes []Node) *Graph { ... }
func (g *Graph) ShortestPath(from, to Node) []Edge { ... }

@martinrode Однако такие мелочи, как поддержка
... разрешить несколько типов в объявлениях Slices и Maps

Это не отвечает потребностям в некоторых функциональных общих функциях среза, например, head() , tail() , map(slice, func) , filter(slice, func)

Вы можете просто написать это самостоятельно для каждого проекта, в котором вам это нужно, но на этом этапе существует риск устаревания из-за повторения копирования и вставки, и это поощряет сложность кода Go для сохранения простоты языка.

(На личном уровне также утомительно знать, что у меня есть набор функций, которые я хочу реализовать, и у меня нет чистого способа выразить их, не отвечая также на языковые ограничения)

Рассмотрите следующее в текущем неуниверсальном подходе:

У меня есть переменная x типа externallib.Foo , полученная из библиотеки externallib , которую я не контролирую.
Я хочу передать его функции SomeFunc(fmt.Stringer) , но externallib.Foo не имеет метода String() string . Я могу просто сделать:

type MyFoo externallib.Foo
func (mf MyFoo) String() string {...}
// ...
SomeFunc(MyFoo(x))

Рассмотрим то же самое с дженериками.

У меня есть переменная x типа []externallib.Foo . Я хочу передать его AnotherFunc(type T Stringer)(s []T) . Это невозможно сделать без дорогостоящего глубокого копирования слайса в новый []MyFoo . Если бы вместо среза был более сложный тип (скажем, чан или карта), или метод модифицировал приемник, он становится еще более неэффективным и утомительным, если вообще возможно.

Это может не быть проблемой в стандартной библиотеке, но это только потому, что она не имеет внешних зависимостей. Это роскошь, которой практически нет ни в одном другом проекте.

Мое предложение состоит в том, чтобы ослабить преобразование, чтобы разрешить []Foo([]Bar{}) для любых Foo , определенных как type Foo Bar , или наоборот, а также рекурсивно для карт, массивов, каналов и указателей. Обратите внимание, что это все дешевые неглубокие копии. Больше технических деталей в Relaxed Type Conversion Proposal .


Впервые это было упомянуто как дополнительная функция в https://github.com/golang/go/issues/15292#issuecomment -546313279.

@JavierZunzunegui Я не думаю, что это вообще связано с дженериками. Да, вы можете предоставить пример с использованием дженериков, но вы можете предоставить аналогичный пример без использования дженериков. Думаю, этот вопрос нужно обсудить отдельно, не здесь. См. также https://golang.org/doc/faq#convert_slice_with_same_underlying_type. Спасибо.

Без дженериков такое преобразование почти не имеет никакой ценности, потому что в общем случае []Foo не будет соответствовать никакому интерфейсу или, по крайней мере, ни одному интерфейсу, который использует его как слайс. Исключением являются интерфейсы, которые имеют очень специфический шаблон для его использования, например sort.Interface , для которого вам все равно не нужно преобразовывать срез.

Необщая версия вышеизложенного ( func AnotherFunc(type T Stringer)(s []T) )

type SliceOfStringers interface {
  Len() int
  Get(int) fmt.Stringer
}
func AnotherFunc(s SliceOfStringers) {...}

Это может быть менее практично, чем общий подход, но его можно настроить для обработки любого фрагмента и сделать это без его копирования, независимо от того, что базовый тип на самом деле является fmt.Stringer . В нынешнем виде дженерики не могут, несмотря на то, что в принципе они являются гораздо более подходящим инструментом для работы. И, конечно же, если мы добавим дженерики, то именно для того, чтобы сделать срезы, карты и т. д. более распространенными в API и манипулировать ими с меньшим количеством шаблонов. Тем не менее, они вводят новую проблему, не имеющую эквивалента в мире, состоящем только из интерфейсов, которая _может_ даже не быть неизбежной, а искусственно навязана языком.

Упомянутое вами преобразование типов достаточно часто встречается в неуниверсальном коде, поэтому это часто задаваемые вопросы. Давайте перенесем это обсуждение в другое место. Спасибо.

Каково это состояние? Любой ОБНОВЛЕННЫЙ черновик? Я жду дженериков с тех пор
почти 2 года назад. Когда у нас появятся дженерики?

Эль мар, 4 февраля. де 2020 г. в 13:28, Ян Лэнс Тейлор (
уведомления@github.com) запись:

Упомянутое вами преобразование типов достаточно часто встречается в неуниверсальном коде.
что это FAQ. Давайте перенесем это обсуждение в другое место. Спасибо.


Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/golang/go/issues/15292?email_source=notifications&email_token=AJMFNBN3MFHDMENAFXIKBLDRBGXUTA5CNFSM4CA35RX2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEKYV5RI#issuecomment49 ,
или отписаться
https://github.com/notifications/unsubscribe-auth/AJMFNBO5UTKNPL3MSA3NESLRBGXUTANCNFSM4CA35RXQ
.

--
Это тест для почтовых подписей, которые будут использоваться в TripleMint.

Мы работаем над этим. Некоторые вещи требуют времени.

Работа выполняется в автономном режиме? Я бы хотел, чтобы он развивался с течением времени таким образом, чтобы «широкая публика», такая как я, не могла комментировать, чтобы избежать шума.

Хотя с тех пор он был закрыт, чтобы обсудить дженерики в одном месте, посмотрите #36177, где @Griesemer ссылается на прототип , над которым он работает, и делает несколько интересных комментариев по поводу своих мыслей по этому вопросу.

Я думаю, что прав, говоря, что прототип в настоящее время имеет дело только с аспектами проверки типа черновика предложения «контракты», но работа, безусловно, звучит многообещающе для меня.

@ianlancetaylor Каждый раз, когда предлагаемый подход к дженерикам становится трудно объяснить простым способом, мы должны отказаться от этого подхода. Гораздо важнее сохранить простоту языка, чем добавлять в него дженерики.

Это отличный идеал, к которому нужно стремиться, но на самом деле разработка программного обеспечения во времена по своей сути не _проста для объяснения_.

Когда язык ограничен в возможности выражения таких _непростых для выражения_ идей, инженеры-программисты в конечном итоге снова и снова изобретают эти средства, потому что эти чертовы _сложные для выражения_ идеи иногда необходимы для логики программ.

Посмотрите на Istio, Kubernetes, operator-sdk и в какой-то степени Terraform и даже библиотеку protobuf. Все они избегают системы типов Go, используя отражение, реализуя новую систему типов поверх Go, используя интерфейсы и генерацию кода, или их комбинацию.

@омейд

Посмотрите на Istio, Kubernetes

Вам когда-нибудь приходило в голову, что причина, по которой они занимаются этими абсурдными вещами, заключается в том, что их основной дизайн не имеет никакого смысла, и в результате им пришлось использовать reflect игры, чтобы реализовать его? ?

Я утверждаю, что лучший дизайн для программ golang (как на этапе проектирования, так и в API) не требует дженериков.

Пожалуйста, не добавляйте их в golang.

Программировать тяжело. Кубелет — темное место. Дженерики разделяют людей больше, чем американская политика. Я хочу верить.

Когда язык ограничен для выражения таких непростых для выражения идей, инженеры-программисты в конечном итоге снова и снова изобретают эти средства, потому что эти чертовски трудновыразимые идеи иногда необходимы для логики программ.

Посмотрите на Istio, Kubernetes, operator-sdk и в какой-то степени Terraform и даже библиотеку protobuf. Все они избегают системы типов Go, используя отражение, реализуя новую систему типов поверх Go, используя интерфейсы и генерацию кода, или их комбинацию.

Я не считаю это убедительным аргументом. В идеале язык Go должен быть легким для чтения, написания и понимания, но при этом позволять выполнять сколь угодно сложные операции. Это согласуется с тем, что вы говорите: упомянутые вами инструменты должны делать что-то сложное, и Go дает им способ сделать это.

В идеале язык Go должен быть легким для чтения, написания и понимания, но при этом позволять выполнять сколь угодно сложные операции.

Я согласен с этим, но поскольку это несколько целей, они иногда будут противоречить друг другу. Код, который естественным образом «хочет» быть написанным в общем стиле, часто становится менее удобным для чтения, чем мог бы быть в противном случае, когда ему приходится прибегать к таким методам, как отражение.

Код, который естественным образом «хочет» быть написанным в общем стиле, часто становится менее удобным для чтения, чем мог бы быть в противном случае, когда ему приходится прибегать к таким методам, как отражение.

Вот почему это предложение остается открытым, и поэтому у нас есть проект возможной реализации дженериков (https://blog.golang.org/why-generics).

Посмотрите на... даже библиотеку protobuf. Все они избегают системы типов Go, используя отражение, реализуя новую систему типов поверх Go, используя интерфейсы и генерацию кода, или их комбинацию.

Исходя из опыта работы с protobufs, есть несколько случаев, когда дженерики могут улучшить удобство использования и/или реализацию API, но подавляющее большинство логики от дженериков не выиграет . Обобщения предполагают, что конкретная информация о типе известна во время компиляции . Для протобуфов большая часть ситуаций связана со случаями, когда информация о типе известна только во время выполнения .

В общем, я замечаю, что люди часто указывают на любое использование рефлексии и утверждают, что это свидетельствует о необходимости дженериков. Это не так просто. Решающее различие заключается в том, известна ли информация о типе во время компиляции или нет. В ряде случаев это принципиально не так.

@dsnet Интересное спасибо, никогда не думал о том, что protobuf не будет универсальным. Всегда предполагалось, что каждый инструмент, генерирующий шаблонный код go, такой как, например, protoc, на основе предопределенной схемы, сможет генерировать общий код без отражения, используя текущее универсальное предложение. Не могли бы вы обновить это в спецификации с примером или в новом сообщении в блоге, где вы описываете эту проблему более подробно?

инструменты, которые вы упомянули, должны делать что-то сложное, и Go дает им способ сделать это.

Использование текстовых шаблонов для генерации кода Go вряд ли является средством по замыслу, я бы сказал, что это специальное вспомогательное средство, в идеале, по крайней мере, стандартные пакеты ast и parser должны позволять генерировать произвольный код Go.

Единственное, что вы можете утверждать, что Go дает возможность работать со сложной логикой, это, возможно, отражение, но это быстро показывает его ограничения, не говоря уже о критически важном для производительности коде, даже при использовании в стандартной библиотеке, например, обработка JSON в Go примитивна. в лучшем случае.

Трудно спорить с тем, что использование текстовых шаблонов или рефлексии для выполнения _чего-то уже сложного_ соответствует идеалу:

Каждый раз, когда предлагаемый подход к ~генерикам~ чего-то сложного становится трудно объяснить простым способом, мы должны отказаться от этого подхода.

Я думаю, что решение, которое упомянутые проекты пришли для решения своей проблемы, слишком сложное и непростое для понимания. Так что в этом отношении в Go отсутствуют средства, которые позволяют пользователям выражать сложные проблемы максимально простыми и прямыми словами.

В общем, я замечаю, что люди часто указывают на любое использование рефлексии и утверждают, что это свидетельствует о необходимости дженериков.

Может быть, есть такое общее заблуждение, но библиотека protobuf, особенно новый API, может быть намного проще, чем на дрожжах, с _generics_ или каким-то _sum type_.

Один из авторов этого нового API-интерфейса protobuf только что сказал, что «подавляющая часть логики не выиграет от дженериков», поэтому я не уверен, откуда вы взяли, что «особенно новый API может быть намного быстрее. просто с дженериками». На чем это основано? Можете ли вы предоставить какие-либо доказательства того, что это было бы намного проще?

Говоря как человек, который использовал API-интерфейсы protobuf на нескольких языках, включающих дженерики (Java, C++), я не могу сказать, что заметил какие-либо существенные различия в удобстве использования API Go и их API. Если бы ваше утверждение было правдой, я бы ожидал, что такая разница будет.

@dsnet Также сказал, что «есть несколько случаев, когда дженерики могут улучшить удобство использования и / или реализацию API».

Но если вам нужен пример того, как все может быть проще, начните с отказа от типа Value , так как он в основном является специальным типом суммы.

@omeid Эта проблема касается дженериков, а не типов сумм. Поэтому я не уверен, насколько этот пример актуален.

В частности, мой вопрос: как наличие дженериков приведет к реализации protobuf или API, который «намного проще», чем новый (или старый, если уж на то пошло) API?

Кажется, это не соответствует моему прочтению того, что сказал @dsnet выше, а также моему опыту работы с API-интерфейсами protobuf Java и C++.

Кроме того, ваш комментарий о примитивной обработке JSON в Go также кажется мне столь же странным. Можете ли вы объяснить, как, по вашему мнению, API encoding/json будет улучшен с помощью дженериков?

Насколько мне известно, реализации синтаксического анализа JSON в Java используют отражение (а не дженерики). Это правда, что API верхнего уровня в большинстве библиотек JSON, скорее всего, будет использовать общий метод (например, Gson ), но метод, который принимает неограниченный общий параметр T и возвращает значение типа T обеспечивает очень небольшую дополнительную проверку типов по сравнению с json.Unmarshal . На самом деле, я думаю, что единственная ошибка, которую json.Unmarshal не улавливает во время компиляции, — это единственный дополнительный сценарий ошибки, если вы передаете значение, не являющееся указателем. (Кроме того, обратите внимание на предостережения в документации Gson API по использованию разных функций для универсальных и неуниверсальных типов. Опять же, это доказывает, что дженерики усложняют свой API, а не упрощают его; в данном случае это поддержка сериализации/десериализации универсальных типов. виды).

(Поддержка JSON в C++ AFAICT хуже; различные подходы, о которых я знаю, либо используют значительное количество макросов, либо включают ручное написание функций синтаксического анализа/сериализации. Опять же, это не так)

Если вы ожидаете, что дженерики значительно добавят в Go поддержку JSON, боюсь, вы будете разочарованы.


@gertcuykens Каждая реализация protobuf на каждом известном мне языке использует генерацию кода, независимо от того, есть ли у них дженерики или нет. Сюда входят Java, C++, Swift, Rust, JS (и TS). Я не думаю, что дженерики автоматически удаляют все способы генерации кода (в качестве доказательства существования я написал генераторы кода, которые генерируют код Java и код C++); кажется нелогичным ожидать, что какое-либо решение для дженериков будет соответствовать этой планке.


Просто для полной ясности: я поддерживаю добавление дженериков в Go. Но я думаю, что мы должны иметь ясное представление о том, что мы собираемся получить от этого. Я не верю, что мы получим значительные улучшения ни в protobuf, ни в JSON API.

Я не думаю, что protobuf - особенно хороший случай для дженериков. Вам не нужны дженерики на целевом языке, так как вы можете просто сгенерировать специализированный код напрямую. Это применимо и к другим подобным системам, таким как Swagger/OpenAPI.

Где дженерики кажутся мне полезными и могут предложить как упрощение, так и безопасность типов, так это написание самого компилятора protobuf.

Что вам нужно, так это язык, способный к безопасному по типу представлению собственного абстрактного синтаксического дерева. Исходя из моего собственного опыта, для этого требуются как минимум дженерики и обобщенные абстрактные типы данных. Затем вы можете написать типобезопасный компилятор protobuf для языка на самом языке.

Где дженерики кажутся мне полезными и могут предложить как упрощение, так и безопасность типов, так это написание самого компилятора protobuf.

Я действительно не понимаю, как. Пакет go/ast уже обеспечивает представление AST Go. Компилятор Go protobuf не использует его, потому что работа с AST намного сложнее, чем просто генерация строк, даже если это более безопасно для типов.

Возможно, у вас есть пример из компилятора protobuf для какого-то другого языка?

@neild Я начал с того, что сказал, что не думаю, что protobuf был очень хорошим примером. Использование дженериков дает преимущества, но они во многом зависят от того, насколько важна для вас безопасность типов, и это может быть уравновешено тем, насколько навязчивой является реализация дженериков. Идеальная реализация не будет мешать вам, если вы не совершите ошибку, и в этом случае преимущества перевесят затраты на большее количество вариантов использования.

Глядя на пакет go/ast, он не имеет типизированного представления AST, потому что для этого требуются дженерики и GADT. Например, узел «добавить» должен быть общим по типу добавляемых терминов. В небезопасном по типам AST вся логика проверки типов должна быть написана вручную, что сделало бы ее громоздкой.

С хорошим синтаксисом шаблона и выражениями, безопасными для типов, вы можете сделать это так же просто, как генерировать строки, но также и безопасно для типов. Например, см. (это больше касается стороны синтаксического анализа): https://stackoverflow.com/questions/11104536/how-to-parse-strings-to-syntax-tree-using-gadts

Например, рассмотрите JSX как буквальный синтаксис для HTML Dom в JavaScript по сравнению с TSX как буквальный синтаксис для Dom в TypeScript.

Мы можем написать типизированные универсальные выражения, которые специализируются на окончательном коде. Так же легко написать, как строки, но с проверкой типов (в их общей форме).

Одна из ключевых проблем с генераторами кода заключается в том, что проверка типов происходит только в сгенерированном коде, что затрудняет написание правильных шаблонов. С помощью дженериков вы можете писать шаблоны как фактические выражения с проверкой типов, поэтому проверка выполняется непосредственно в шаблоне, а не в сгенерированном коде, что значительно упрощает его правильное выполнение и поддержку.

В текущем дизайне отсутствуют параметры вариативного типа, что выглядит как огромное отсутствие функциональности дженериков. Дополнительный дизайн (возможно) соответствует текущему дизайну контракта:

contract Comparables(Ts...) {
    if  len(Ts) > 0 {
        Comparables(Ts[1:]...)
    } else {
        Comparable(Ts[0])
    }
}

contract Comparable(T) {
    T int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        string
}

type Keys(type Ts ...Comparables) struct {
    fs ...Ts
}

type Metric(type Ts ...Comparables) struct {
    mu sync.Mutex
    m  map[Keys(Ts...)]int
}

func (m *Metric(Ts...)) Add(vs ...Ts) {
    m.mu.Lock()
    defer m.mu.Unlock()
    if m.m == nil {
        m.m = make(map[Keys(Ts...))]int)
    }
    m[Keys(Ts...){vs...}]++
}


// To use the metric

m := Metric(int, float64, string){m: make(map[Keys(int, float64, string)]int}
m.Add(1, 2.0, "variadic")

Пример навеян отсюда .

Мне не ясно, как это добавляет безопасности выше, просто используя interface{} . Есть ли реальная проблема с тем, что люди передают несопоставимые данные в метрику?

Мне не ясно, как это добавляет безопасности выше, просто используя interface{} . Есть ли реальная проблема с тем, что люди передают несопоставимые данные в метрику?

Comparables в этом примере требует, чтобы Keys состоял из ряда сопоставимых типов. Основная идея состоит в том, чтобы показать дизайн параметров вариативного типа, а не значение самого типа.

Я не хочу слишком зацикливаться на этом примере, но я придираюсь к нему, потому что я думаю, что многие примеры «расширения типа» просто в конечном итоге толкают бухгалтерию, не добавляя никакой практической безопасности. В этом случае, если вы видите плохой тип во время выполнения или, возможно, с помощью go vet, вы можете пожаловаться.

Кроме того, я немного обеспокоен тем, что разрешение открытых типов типов, подобных этому, приведет к проблеме парадоксальных ссылок, как это происходит в логике второго порядка. Не могли бы вы определить C как контракт всех типов, которых нет в C?

Кроме того, я немного обеспокоен тем, что разрешение открытых типов типов, подобных этому, приведет к проблеме парадоксальных ссылок, как это происходит в логике второго порядка. Не могли бы вы определить C как контракт всех типов, которых нет в C?

Извините, но я не понимаю, как этот пример допускает открытые типы и относится к парадоксу Рассела, Comparables определяется списком Comparable .

Мне не нравится идея писать код Go внутри контракта. Если я могу написать оператор if , могу ли я написать оператор for ? Могу ли я вызвать функцию? Могу ли я объявить переменные? Почему нет?

Тоже кажется ненужным. func F(a ...int) означает, что a равно []int . По аналогии, func F(type Ts ...comparable) будет означать, что каждый тип в списке равен comparable .

В этих строках

type Keys(type Ts ...Comparables) struct {
    fs ...Ts
}

вы, кажется, определяете структуру с несколькими полями с именами fs . Я не уверен, как это должно работать. Есть ли способ использовать ссылку на поля в этой структуре, кроме использования отражения?

Итак, вопрос: что можно сделать с параметрами вариативного типа? Что человек хочет делать?

Здесь я думаю, что вы используете параметры вариационного типа для определения типа кортежа с произвольным количеством полей.

Что еще можно сделать?

Мне не нравится идея писать код Go внутри контракта. Если я могу написать оператор if , могу ли я написать оператор for ? Могу ли я вызвать функцию? Могу ли я объявить переменные? Почему нет?

Тоже кажется ненужным. func F(a ...int) означает, что a равно []int . По аналогии, func F(type Ts ...comparable) будет означать, что каждый тип в списке равен comparable .

Просмотрев пример днем ​​позже, я думаю, что вы абсолютно правы. Comparables — глупая идея. В примере нужно только передать сообщение об использовании len(args) для определения количества параметров. Оказывается, для функций достаточно func F(type Ts ...Comparable) .

Обрезанный пример:

contract Comparable(T) {
    T int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        string
}

type Keys(type Ts ...Comparable) struct {
    fs ...Ts
}

type Metric(type Ts ...Comparable) struct {
    mu sync.Mutex
    m  map[Keys(Ts...)]int
}

func (m *Metric(Ts...)) Add(vs ...Ts) {
    m.mu.Lock()
    defer m.mu.Unlock()
    if m.m == nil {
        m.m = make(map[Keys(Ts...))]int)
    }
    m[Keys(Ts...){vs...}]++
}


// To use the metric

m := Metric(int, float64, string){m: make(map[Keys(int, float64, string)]int}
m.Add(1, 2.0, "variadic")

вы, кажется, определяете структуру с несколькими полями с именами fs . Я не уверен, как это должно работать. Есть ли способ использовать ссылку на поля в этой структуре, кроме использования отражения?

Итак, вопрос: что можно сделать с параметрами вариативного типа? Что человек хочет делать?

Здесь я думаю, что вы используете параметры вариационного типа для определения типа кортежа с произвольным количеством полей.

Что еще можно сделать?

Параметры типа Variadic предназначены для кортежей по его определению, если мы используем для него ... , что не означает, что кортежи являются единственным вариантом использования, но его можно использовать в любых структурах и любых функциях.

Поскольку есть только два места, которые появляются с параметрами вариативного типа: структура или функция, поэтому мы легко получаем то, что было ясно ранее для функций:

func F(type Ts ...Comparable) (args ...Ts) {
    if len(args) > 1 {
        F(args[1:])
        return
    }
    // ... do stuff with args[0]
}

Например, вариационная функция Min невозможна в текущем дизайне, но возможна с параметрами вариативного типа:

func Min(type T ...Comparable)(p1 T, pn ...T) T {
    switch l := len(pn); {
    case l > 1:
        return Min(pn[0], pn[1:]...)
    case l == 1:
        if p1 >= pn[0] { return pn[0] }
        return p1
    case l < 1:
        return p1
    }
}

Чтобы определить Tuple с переменными параметрами типа:

type Tuple(type Ts ...Comparable) struct {
    fs ...Ts
}

Когда три параметра типа создаются с помощью «Ts», их можно преобразовать в

type Tuple(type T1, T2, T3 Comparable) struct {
    fs_1 T1
    fs_2 T2
    fs_3 T3
}

как промежуточное представление. Есть несколько способов использовать fs :

  1. параметры распаковать
k := Tuple(int, float64, string){1, 2.0, "variadic"}
fs1, fs2, fs3 := k.fs // translated to fs1, fs2, fs3 := k.fs_1, k.fs_2, k.fs_3
println(fs1) // 1
println(fs2) // 2.0
println(fs3) // variadic
  1. использовать цикл for
for idx, f := range k.fs {
    println(idx, ": ", f)
}
// Output:
// 0: 1
// 1: 2.0
// 2: variadic
  1. использовать индекс (не уверен, видят ли люди, что это двусмысленность массива/среза или карты)
k.fs[0] = ... // translated to k.fs_1 = ...
f2 := k.fs[1] // translated to f2 := k.fs_2
  1. используйте пакет reflect , в основном работает как массив
t := Tuple(int, float64, string){1, 2.0, "variadic"}

fs := reflect.ValueOf(t).Elem().FieldByName("fs")
val := reflect.ValueOf(fs)
if val.Kind() == reflect.VariadicTypes {
    for i := 0; i < val.Len(); i++ {
        e := val.Index(i)
        switch e.Kind() {
        case reflect.Int:
            fmt.Printf("%v, ", e.Int())
        case reflect.Float64:
            fmt.Printf("%v, ", e.Float())
        case reflect.String:
            fmt.Printf("%v, ", e.String())
        }
    }
}

Ничего действительно нового по сравнению с использованием массива.

Например, вариационная функция Min невозможна в текущем проекте, но возможна с параметрами вариативного типа:

func Min(type T ...Comparable)(p1 T, pn ...T) T {
    switch l := len(pn); {
    case l > 1:
        return Min(pn[0], pn[1:]...)
    case l == 1:
        if p1 >= pn[0] { return pn[0] }
        return p1
    case l < 1:
        return p1
    }
}

Это не имеет смысла для меня. Параметры типа Variadic имеют смысл только в том случае, если типы могут быть разными типами. Но вызов Min в списке разных типов не имеет смысла. Go не поддерживает использование >= для значений разных типов. Даже если бы мы каким-то образом разрешили это, нас могут попросить Min(int, string)(1, "a") . На это нет никакого ответа.

Хотя текущий дизайн не допускает Min с переменным числом различных типов, он поддерживает вызов Min с переменным числом значений одного и того же типа. Я думаю, что это единственный разумный способ использовать Min любом случае.

func Min(type T comparable)(s ...T) T {
    if len(s) == 0 {
        panic("Min of no elements")
    }
    r := s[0]
    for _, v := range s[1:] {
        if v < r {
            r = v
        }
    }
    return r
}

Для некоторых других примеров в https://github.com/golang/go/issues/15292#issuecomment -599040081 важно отметить, что в срезах и массивах Go есть элементы одного типа. При использовании параметров переменных типов элементы имеют разные типы. Так что на самом деле это не то же самое, что срез или массив.

Хотя текущий дизайн не допускает Min с переменным числом различных типов, он поддерживает вызов Min с переменным числом значений одного и того же типа. Я думаю, что это единственный разумный способ использовать Min любом случае.

func Min(type T comparable)(s ...T) T {
    if len(s) == 0 {
        panic("Min of no elements")
    }
    r := s[0]
    for _, v := range s[1:] {
        if v < r {
            r = v
        }
    }
    return r
}

Истинный. Min был плохим примером. Он был добавлен поздно и не имел четкой мысли, как вы можете видеть из истории редактирования комментариев. Реальный пример — Metric , которые вы проигнорировали.

важно отметить, что в Go срезы и массивы содержат элементы одного типа. При использовании параметров переменных типов элементы имеют разные типы. Так что на самом деле это не то же самое, что срез или массив.

Видеть? Вы те люди, которые видят, что это двусмысленность массива/среза или карты. Как я сказал в https://github.com/golang/go/issues/15292#issuecomment -599040081, синтаксис очень похож на массив/срез и карту, но он обращается к элементам с разными типами. Это действительно имеет значение? Или можно доказать, что это двусмысленность? Что возможно в Go 1:

m := map[interface{}]int{1: 2, "2": 3, 3.0: 4}
for i, e := range m {
    println(i, e)
}

Считается ли i одним и тем же типом? По-видимому, мы говорим, что я interface{} , того же типа. Но действительно ли интерфейс выражает тип? Программистам приходится вручную проверять возможные типы. При использовании for , [] и распаковке они действительно имеют значение для пользователя, что они не обращаются к одному и тому же типу? Какие аргументы против этого? То же самое для fs :

for idx, f := range k.fs {
    switch f.(type) { // compare to interface{}, here is zero overhead.
    case int:
        // ...
    case float64:
        // ...
    case string:
        // ...
    }
}

Если вам нужно использовать переключатель типа для доступа к элементу вариативного универсального типа, я не вижу преимущества. Я вижу, как с некоторыми вариантами техники компиляции это может быть немного более эффективным во время выполнения, чем использование interface{} . Но я думаю, что разница будет довольно небольшой, и я не понимаю, почему это будет более безопасно для типов. Не сразу становится очевидным, что стоит усложнять язык.

Я не собирался игнорировать пример Metric , я просто пока не понимаю, как использовать универсальные типы с переменным числом переменных, чтобы упростить написание. Если мне нужно использовать переключатель типа в теле Metric , то я лучше напишу Metric2 и Metric3 .

Что означает «усложнение языка»? Мы все согласны с тем, что дженерики — сложная штука, и они никогда не сделают язык проще, чем Go 1. Вы уже приложили огромные усилия для его разработки и реализации, но пользователям Go совершенно непонятно: каково определение «похоже на писать... Идти"? Есть ли количественная метрика для ее измерения? Как предложение по языку может утверждать, что оно не усложняет язык? В шаблоне предложения по языку Go 2 цели при первом впечатлении довольно просты:

  1. решить важную для многих проблему,
  2. иметь минимальное влияние на всех остальных и
  3. приходят с четким и понятным решением.

Но могут возникнуть вопросы: сколько это «много»? Что значит «важно»? Как измерить воздействие на неизвестную популяцию? Когда проблема хорошо изучена? Go доминирует в облаке, но станет ли доминирование в других областях, таких как научные числовые вычисления (например, машинное обучение), графическая визуализация (например, огромный рынок 3D), одной из целей Go? Подходит ли проблема больше к «Я бы предпочел сделать А, чем Б в Go, и нет варианта использования, потому что мы можем сделать это по-другому» или «Б не предлагается, поэтому мы не используем Go и вариант использования». еще не существует, потому что язык не может легко выразить это»? ... Я нашел эти вопросы болезненными и бесконечными, а иногда даже не стоящими ответа на них.

Вернемся к примеру с Metric . Он не показывает необходимости доступа к отдельным лицам. Распаковка набора параметров кажется здесь не реальной необходимостью, хотя решения, которые «совпадают» с существующим языком, используют индексацию [ ] и вывод типов, которые могут решить проблему безопасности типов:

f2 := k.fs[1] // f2 is a float64

@changkun Если бы были четкие и объективные показатели, позволяющие решить, какие функции языка хороши, а какие плохи, нам не понадобились бы разработчики языков - мы могли бы просто написать программу для разработки оптимального для нас языка. Но нет - все всегда сводится к личным предпочтениям какого-то круга людей. Что также, кстати, почему нет смысла ссориться из-за того, «хороший» язык или нет - вопрос только в том, нравится ли он вам лично. В случае Go люди, которые определяют предпочтения, — это люди из команды Go, и то, что вы цитируете, — это не показатели, а направляющие вопросы, которые помогут вам убедить их.

Лично я, FWIW, чувствую, что параметры типа вариационного типа терпят неудачу в двух из этих трех. Я не думаю, что они решают важную проблему для многих людей - пример с метриками может извлечь из них пользу, но IMO лишь немного, и это очень специализированный вариант использования. И я не думаю, что они приходят с четким и понятным решением. Я не знаю ни одного языка, поддерживающего что-то подобное. Но я могу ошибаться. Было бы определенно полезно, если бы у кого-то были примеры других языков, поддерживающих это - это могло бы предоставить информацию о том, как это обычно реализуется и, что более важно, как это используется. Может быть, он используется более широко, чем я могу себе представить.

@Merovius Haskell имеет многовариантные функции, как мы показали в документе HList: http://okmij.org/ftp/Haskell/polyvariadic.html#polyvar -fn
Очевидно, что это сложно сделать в Haskell, но не невозможно.

Мотивирующим примером является типобезопасный доступ к базе данных, где можно выполнять такие вещи, как безопасные типы соединений и проекций, а также объявлять схему базы данных на языке.

Например, таблица базы данных очень похожа на запись, где есть имена столбцов и типы. Операция реляционного соединения берет две произвольные записи и создает запись с типами из обеих. Вы, конечно, можете сделать это вручную, но это подвержено ошибкам, очень утомительно, запутывает смысл кода со всеми типами записей, объявленными вручную, и, конечно, большая особенность базы данных SQL заключается в том, что она поддерживает ad-hoc запросы, поэтому вы не можете предварительно создать все возможные типы записей, поскольку вы не обязательно знаете, какие запросы вам нужны, пока не выполните их.

Таким образом, типобезопасный оператор реляционного соединения для записей и кортежей был бы хорошим вариантом использования. Здесь мы думаем только о типе функции - от программиста зависит, что на самом деле делает функция, будь то объединение двух массивов кортежей в памяти или генерируется SQL для запуска во внешней БД и маршалирования результатов. обратно типобезопасным способом.

Такого рода вещи намного лучше встраиваются в C# с помощью LINQ. Большинство людей, кажется, думают о LINQ как о добавлении лямбда-функций и монад к C#, но он не будет работать для своего основного варианта использования без поливариадики, поскольку вы просто не можете определить безопасный тип оператора соединения без аналогичной функциональности.

Я думаю, что реляционные операторы важны. После основных операций над булевыми, бинарными, int, float и строковыми типами, вероятно, идут множества, а затем отношения.

Кстати, C++ также предлагает это, хотя мы не хотим спорить, что нам нужна эта функция в Go, потому что она есть в XXX :)

Я думаю, было бы очень странно, если бы k.fs[0] и k.fs[1] имели разные типы. Это не то, как другие индексируемые значения работают в Go.

Пример метрик основан на https://medium.com/@sameer_74231/go-experience-report-for-generics-google-metrics-api-b019d597aaa4 . Я думаю, что код требует отражения для извлечения значений. Я думаю, что если мы собираемся добавить вариативные дженерики в Go, мы должны получить что-то лучшее, чем отражение для извлечения значений. В противном случае кажется, что это не очень помогает.

Я думаю, было бы очень странно, если бы k.fs[0] и k.fs[1] имели разные типы. Это не то, как другие индексируемые значения работают в Go.

Пример метрик основан на https://medium.com/@sameer_74231/go-experience-report-for-generics-google-metrics-api-b019d597aaa4 . Я думаю, что код требует отражения для извлечения значений. Я думаю, что если мы собираемся добавить вариативные дженерики в Go, мы должны получить что-то лучшее, чем отражение для извлечения значений. В противном случае кажется, что это не очень помогает.

Что ж. Вы запрашиваете то, чего не существует. Если вам не нравится [``] , осталось два варианта: ( ) или {``} , и я вижу, вы можете утверждать, что круглые скобки выглядят как вызов функции и фигурные скобки выглядят как инициализация переменной. Никто не любит args.0 args.1 , потому что это не похоже на Go. Синтаксис тривиален.

На самом деле, я провожу несколько выходных за чтением книги "Дизайн и эволюция C++", в ней много интересных идей о решениях и уроках, хотя она была написана в 1994 году:

_"[...] Оглядываясь назад, я недооценил важность ограничений в удобочитаемости и раннем обнаружении ошибок."_ ==> Отличный дизайн контракта

"_синтаксис функции на первый взгляд выглядит красивее и без дополнительного ключевого слова:_

T& index<class T>(vector<T>& v, int i) { /*...*/ }
int i = index(v1, 10);

_Похоже, с этим более простым синтаксисом возникают серьезные проблемы. Это слишком умно. Относительно трудно обнаружить объявление шаблона в программе, потому что […] Скобки <...> были выбраны вместо скобок, потому что пользователи сочли их более удобными для чтения. [...] Как оказалось, Том Пеннелло доказал, что скобки было бы легче разобрать, но это не меняет ключевого наблюдения, что читатели (люди) предпочитают <...> _
" ==> разве это не похоже на func F(type T C)(v T) T ?

_"Однако я думаю, что был слишком осторожен и консервативен, когда дело дошло до определения функций шаблона. Я мог бы включить такие функции, как [...]. Эти функции не сильно увеличили бы нагрузку на разработчиков, и пользователям бы помогли."_

Почему это кажется таким знакомым?

Индексирование параметров вариативного типа (или кортежа) необходимо отделить от индексирования времени выполнения и индексирования времени компиляции. Я думаю, вы можете просто возразить, что отсутствие поддержки индексации во время выполнения может сбить пользователей с толку, потому что это не согласуется с индексированием во время компиляции. Даже для индексации во время компиляции в текущем дизайне также отсутствует нетиповой параметр «шаблон».

Со всеми доказательствами предложение (кроме отчета об опыте) пытается избежать обсуждения этой функции, и я начинаю верить, что речь идет не о добавлении вариативных дженериков в Go, а просто об удалении по замыслу.

Я согласен с тем, что Design and Evolution of C++ — хорошая книга, но у C++ и Go разные цели. Последняя цитата там хорошая; Страуструп даже не упоминает цену сложности языка для пользователей этого языка. В Go мы всегда стараемся учитывать эту стоимость. Go задуман как простой язык. Если бы мы добавили каждую функцию, которая помогала бы пользователям, это было бы непросто. Поскольку C++ не прост.

Со всеми доказательствами предложение (кроме отчета об опыте) пытается избежать обсуждения этой функции, и я начинаю верить, что речь идет не о добавлении вариативных дженериков в Go, а просто об удалении по замыслу.

Извините, я не знаю, что вы имеете в виду здесь.

Лично я всегда рассматривал возможность вариативных универсальных типов, но никогда не тратил время на то, чтобы понять, как это будет работать. То, как это работает в C++, очень тонко. Я хотел бы посмотреть, сможем ли мы сначала заставить работать невариативные дженерики. Безусловно, есть время добавить вариативные дженерики, если возможно, позже.

Когда я критикую более ранние мысли, я не говорю, что вариативные типы не могут быть реализованы. Я указываю на проблемы, которые, по моему мнению, необходимо решить. Если они не могут быть разрешены, то я не уверен, что вариативные типы того стоят.

Страуструп даже не упоминает цену сложности языка для пользователей этого языка. В Go мы всегда стараемся учитывать эту стоимость. Go задуман как простой язык. Если бы мы добавили каждую функцию, которая помогала бы пользователям, это было бы непросто. Поскольку C++ не прост.

Неправда ИМО. Следует отметить, что C++ является первым практиком, который продвигает дженерики (ну, ML - это первый язык). Из того, что я прочитал в книге, я понял, что С++ задумывался как простой язык (не предлагать дженерики в начале, цикл Experiment-Simplify-Ship для разработки языка, та же история). C++ также имел фазу замораживания функций в течение нескольких лет, что мы и имеем в Go «Обещание совместимости». Но со временем он немного выходит из-под контроля из-за многих разумных причин, что непонятно для Go, если он пойдет по старому пути C++ после выпуска дженериков.

Безусловно, есть время добавить вариативные дженерики, если возможно, позже.

То же чувство ко мне. Генераторы Variadic также отсутствуют в первой стандартизированной версии шаблонов.

Я указываю на проблемы, которые, по моему мнению, необходимо решить. Если они не могут быть разрешены, то я не уверен, что вариативные типы того стоят.

Я понимаю ваши опасения. Но проблема в основном решена, осталось только правильно перевести на Go (а слово «переводить», наверное, никому не нравится). Что я прочитал из вашего исторического предложения по дженерикам, они в основном следуют тому, что не удалось в раннем предложении С++, и скомпрометировано тем, о чем сожалел Страуструп. Меня интересуют ваши контраргументы по этому поводу.

Нам придется не согласиться с целями C++. Возможно, первоначальные цели были более похожими, но, глядя на C++ сегодня, я думаю, становится ясно, что их цели сильно отличаются от целей Go, и я думаю, что так было по крайней мере 25 лет.

При написании различных предложений по добавлению дженериков в Go я, конечно же, смотрел на то, как работают шаблоны C++, а также смотрел на многие другие языки (в конце концов, C++ не изобретал дженерики). Я не смотрел, о чем сожалел Страуструп, так что если мы пришли в одно и то же место, тогда отлично. Я думаю, что дженерики в Go больше похожи на дженерики в Ada или D, чем на C++. Даже сегодня в C++ нет контрактов, которые называются концептами, но еще не добавлены в язык. Кроме того, C++ намеренно допускает сложное программирование во время компиляции, и на самом деле шаблоны C++ сами по себе являются полным языком по Тьюрингу (хотя я не знаю, было ли это сделано намеренно). Я всегда считал, что в Go этого следует избегать, поскольку сложность экстремальна (хотя в C++ она сложнее, чем в Go из-за перегрузки методов и разрешения, которых в Go нет).

После того, как около месяца я пробовал текущую реализацию контракта, мне немного интересно, какова судьба существующих встроенных функций. Все они могут быть реализованы в общем виде:

func Append(type T)(slice []T, elems ...T) []T {...}
func Copy(type T)(dst, src []T) int {...}
func Delete(type K, V)(m map[K]V, k K) {...}
func Make(type T, I Integer(I))(siz ...I) T {...}
func New(type T)() *T {...}
func Close(type T)(c chan<- T) {...}
func Panic(type T)(v T) {...}
func Recover(type T)() T {...}
func Print(type ...T)(args ...T) {...}
func Println(type ...T)(args ...T) {...}

Они исчезнут в Go2? Как Go 2 мог справиться с таким огромным влиянием на существующую кодовую базу Go 1? Кажется, это открытые вопросы.

Более того, эти двое немного особенные:

func Len(type T C)(t T) int {...}
func Cap(type T C)(t T) int {...}

Как реализовать такой контракт C с текущим дизайном, чтобы параметр типа мог быть только общим срезом []Ts , картой map[Tk]Tv и каналом chan Tc где T Ts Tk Tv Tc разные?

@changkun Я не думаю, что «их можно реализовать с помощью дженериков» является убедительной причиной для их удаления. И вы упоминаете довольно четкую и вескую причину, по которой их не следует удалять. Так что я не думаю, что они будут. Я думаю, что это делает остальные вопросы устаревшими.

@changkun Я не думаю, что «их можно реализовать с помощью дженериков» является убедительной причиной для их удаления. И вы упоминаете довольно четкую и вескую причину, по которой их не следует удалять.

Да, я согласен, что это не убедительно для их удаления, поэтому я сказал это прямо. Однако сохранение их вместе с дженериками «нарушает» существующую философию Go, особенности языка которой ортогональны. Совместимость является главной проблемой, но добавление контрактов, вероятно, убьет огромный текущий «устаревший» код.

Так что я не думаю, что они будут. Я думаю, что это делает остальные вопросы устаревшими.

Давайте попробуем не игнорировать этот вопрос и рассмотрим его как реальный вариант использования контрактов. Если кто-то выдвигает аналогичные требования, как мы можем реализовать их с текущим дизайном?

Ясно, что мы не собираемся избавляться от существующих заранее объявленных функций.

Хотя можно написать сигнатуру параметризованной функции для delete , close , panic , recover , print и println , я не думаю, что их можно реализовать, не полагаясь на внутренние магические функции.

Частичные версии Append и Copy доступны по адресу https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-contracts.md#append. Он неполный, поскольку append и copy имеют особые случаи для второго аргумента типа string , который не поддерживается текущим черновиком проекта.

Обратите внимание, что подпись для Make выше недействительна в соответствии с текущим проектом дизайна. New не совсем то же самое, что new , но достаточно близко.

С текущим черновиком проекта Len и Cap должны были бы принимать аргумент типа interface{} , и поэтому не были бы безопасными во время компиляции.

https://go-review.googlesource.com/c/go/+/187317

Пожалуйста, не используйте расширения файлов .go2 , у нас есть модули для таких версий? Я понимаю, если вы делаете это как временное решение, чтобы облегчить жизнь во время экспериментов с контрактами, но, пожалуйста, убедитесь, что в конце файл go.mod позаботится о смешивании пакетов go без необходимо расширение файла .go2 . Это было бы ударом по разработчикам модулей, которые изо всех сил стараются, чтобы модули работали как можно лучше. Использование расширений файлов .go2 равносильно тому, что я говорю: «Нет, меня не волнует, что ваш модуль все равно будет делать это по-моему, потому что я не хочу, чтобы мой 10-летний предмодульный динозавр-динозавр go сломался». .

Файлы @gertcuykens .go2 предназначены только для эксперимента; они не будут использоваться, когда дженерики попадут в компилятор.

(Я собираюсь скрыть наши комментарии, так как они на самом деле ничего не добавляют к обсуждению, а сами они достаточно длинные.)

Недавно я исследовал новый общий синтаксис в языке K , который я разработал, потому что K позаимствовал много грамматики из Go, поэтому эта общая грамматика также может иметь некоторое справочное значение для Go.

Проблема identifier<T> заключается в том, что он конфликтует с операторами сравнения, а также с битовыми операторами, поэтому я не согласен с этим дизайном.

identifier[T] в Scala выглядит лучше, чем предыдущий дизайн, но после разрешения вышеуказанного конфликта у него возник новый конфликт с дизайном индекса identifier[index] .
По этой причине дизайн индекса Scala был изменен на identifier(index) . Это плохо работает для языков, которые уже используют [] в качестве индекса.

В черновике Go было объявлено, что дженерики используют (type T) , что не вызовет конфликтов, поскольку type является ключевым словом, но компилятору по-прежнему требуется больше суждений, когда он вызывается для разрешения identifier(type)(params) . Хотя это лучше, чем вышеперечисленные решения, меня все равно это не удовлетворяет.

Случайно я вспомнил об особом дизайне вызова методов в OC, что вдохновило меня на новый дизайн.

Что, если мы поместим идентификатор и дженерик в целом и поместим их вместе в [] ?
Мы можем получить [identifier T] . Такое оформление не конфликтует с индексом, потому что в нем должно быть как минимум два элемента, разделенных пробелами.
Когда есть несколько дженериков, мы можем написать [identifier T V] вот так, и это не будет конфликтовать с существующим дизайном.

Подставив эту конструкцию в Go, мы можем получить следующий пример.
Например

type [Item T] struct {
    Value T
}

func (it [Item T]) Print() {
    println(it.Value)
}

func [TestGenerics T V]() {
    var a = [Item T]{}
    a.Print()
    var b = [Item V]{}
    b.Print()
}

func main() {
    [TestGenerics int string]()
}

Это выглядит очень ясно.

Еще одним преимуществом использования [] является то, что он унаследован от оригинального дизайна Go Slice and Map и не вызывает ощущения фрагментации.

[]int  ->  [slice int]

map[string]int  ->  [map string int]

Мы можем сделать более сложный пример

var a map[int][]map[string]map[string][]string

var b [map int [slice [map string [map string [slice string]]]]]

Этот пример по-прежнему сохраняет относительно четкий эффект и в то же время оказывает небольшое влияние на компиляцию.

Я реализовал и протестировал этот дизайн в K, и он работает хорошо.

Я думаю, что этот дизайн имеет определенную референсную ценность и может быть достоин обсуждения.

Недавно я исследовал новый общий синтаксис в языке K , который я разработал, потому что K позаимствовал много грамматики из Go, поэтому эта общая грамматика также может иметь некоторое справочное значение для Go.

Проблема identifier<T> заключается в том, что он конфликтует с операторами сравнения, а также с битовыми операторами, поэтому я не согласен с этим дизайном.

identifier[T] в Scala выглядит лучше, чем предыдущий дизайн, но после разрешения вышеуказанного конфликта у него возник новый конфликт с дизайном индекса identifier[index] .
По этой причине дизайн индекса Scala был изменен на identifier(index) . Это плохо работает для языков, которые уже используют [] в качестве индекса.

В черновике Go было объявлено, что дженерики используют (type T) , что не вызовет конфликтов, поскольку type является ключевым словом, но компилятору по-прежнему требуется больше суждений, когда он вызывается для разрешения identifier(type)(params) . Хотя это лучше, чем вышеперечисленные решения, меня все равно это не удовлетворяет.

Случайно я вспомнил об особом дизайне вызова методов в OC, что вдохновило меня на новый дизайн.

Что, если мы поместим идентификатор и дженерик в целом и поместим их вместе в [] ?
Мы можем получить [identifier T] . Такое оформление не конфликтует с индексом, потому что в нем должно быть как минимум два элемента, разделенных пробелами.
Когда есть несколько дженериков, мы можем написать [identifier T V] вот так, и это не будет конфликтовать с существующим дизайном.

Подставив эту конструкцию в Go, мы можем получить следующий пример.
Например

type [Item T] struct {
    Value T
}

func (it [Item T]) Print() {
    println(it.Value)
}

func [TestGenerics T V]() {
    var a = [Item T]{}
    a.Print()
    var b = [Item V]{}
    b.Print()
}

func main() {
    [TestGenerics int string]()
}

Это выглядит очень ясно.

Еще одним преимуществом использования [] является то, что он унаследован от оригинального дизайна Go Slice and Map и не вызывает ощущения фрагментации.

[]int  ->  [slice int]

map[string]int  ->  [map string int]

Мы можем сделать более сложный пример

var a map[int][]map[string]map[string][]string

var b [map int [slice [map string [map string [slice string]]]]]

Этот пример по-прежнему сохраняет относительно четкий эффект и в то же время оказывает небольшое влияние на компиляцию.

Я реализовал и протестировал этот дизайн в K, и он работает хорошо.

Я думаю, что этот дизайн имеет определенную референсную ценность и может быть достоин обсуждения.

отличный

После некоторых обсуждений и нескольких перечитываний я в целом поддерживаю текущий проект дизайна контрактов в Go. Я ценю количество времени и усилий, затраченных на это. Хотя объем, концепции, реализация и большинство компромиссов кажутся разумными, меня беспокоит то, что синтаксис необходимо пересмотреть, чтобы улучшить читаемость.

Я написал ряд предлагаемых изменений, чтобы решить эту проблему:

Ключевые моменты:

  • Синтаксис вызова метода/утверждения типа для объявления контракта
  • «Пустой контракт»
  • Разделители без скобок

Рискуя опередить эссе, я приведу несколько фрагментов синтаксиса без объяснений, преобразованных из образцов в текущем проекте дизайна Contracts. Обратите внимание, что форма разделителей F«T» является иллюстративной, а не предписывающей; подробности см. в записи.

type List«type Element contract{}» struct {
    next *List«Element»
    val  Element
}

и

contract viaStrings«To, From» {
    To.Set(string)
    From.String() string
}

func SetViaStrings«type To, From viaStrings»(s []From) []To {
    r := make([]To, len(s))
    for i, v := range s {
        r[i].Set(v.String())
    }
    return r
}

и

func Keys«type K comparable, V contract{}»(m map[K]V) []K {
    r := make([]K, 0, len(m))
    for k := range m {
        r = append(r, k)
    }
    return r
}

k := maps.Keys(map[int]int{1:2, 2:4})

и

contract Numeric«T» {
    T.(int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        complex64, complex128)
}

func DotProduct«type T Numeric»(s1, s2 []T) T {
    if len(s1) != len(s2) {
        panic("DotProduct: slices of unequal length")
    }
    var r T
    for i := range s1 {
        r += s1[i] * s2[i]
    }
    return r
}

Без реального изменения контрактов под капотом это гораздо более читабельно для меня как для Go-разработчика. Я также чувствую себя гораздо более уверенно, обучая этой форме кого-то, кто изучает го (хотя и поздно в учебной программе).

@ianlancetaylor Основываясь на вашем комментарии на https://github.com/golang/go/issues/36533#issuecomment -579484523, я пишу в этой теме, а не начинаю новую проблему. Он также указан на странице отзывов о дженериках . Не уверен, что мне нужно сделать что-то еще, чтобы его «официально рассмотрели» (т. е. группа по рассмотрению предложений Go 2 ?), или если обратная связь все еще активно собирается.

Из проектов договоров:

Почему бы не использовать синтаксис F<T> , как в C++ и Java?
При синтаксическом анализе кода внутри функции, такой как v := F<T> , в момент появления < неоднозначно, видим ли мы экземпляр типа или выражение, использующее оператор < . Решение этого требует эффективного неограниченного просмотра вперед. В целом мы стремимся сделать парсер Go простым.

Не особо противоречит моему последнему сообщению: Разделители угловых скобок для контрактов Go

Просто несколько идей о том, как обойти этот момент, когда парсер запутался. Пара образцов:

// Lifted from the design draft
func New<type K, V>(compare func(K, K) int) *Map<K, V> {
    return &Map{<K, V> compare: compare}
}

// ...

func (m *Map<K, V>) InOrder() *Iterator<K, V> {
    sender, receiver := chans.Ranger(<keyValue<K, V>>)
    var f func(*node<K, V>) bool
    f = func(n *node<K, V>) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue{<K, V> n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

По сути, просто другая позиция для параметров типа в сценариях, где < может быть неоднозначным.

@toolbox Относительно вашего комментария к угловой скобке. Спасибо, но лично для меня этот синтаксис читается как принятие решения о том, что мы должны использовать угловые скобки для параметров типа и аргументов типа, а затем выяснить, как их вбить. Я думаю, что если мы добавим дженерики в Go, нам нужно нацелиться для чего-то, что легко и просто вписывается в существующий язык. Я не думаю, что перемещение угловых скобок внутри фигурных скобок достигает этой цели.

Да, это второстепенная деталь, но я думаю, что когда дело доходит до синтаксиса, второстепенные детали очень важны. Я думаю, что если мы собираемся добавить аргументы и параметры типов, они должны работать простым и интуитивно понятным образом.

Я, конечно, не утверждаю, что синтаксис в текущем проекте проекта идеален, но я утверждаю, что он легко вписывается в существующий язык. Теперь нам нужно написать больше примеров кода, чтобы увидеть, насколько хорошо он работает на практике. Ключевой момент: как часто людям на самом деле приходится писать аргументы типа вне объявлений функций, и насколько запутанны эти случаи? Я не думаю, что мы знаем.

Стоит ли использовать [] для универсальных типов и использовать () для универсальных функций? Это было бы более совместимо с текущими базовыми дженериками.

Может ли сообщество проголосовать за это? Лично я бы предпочел _все_, чем добавлять больше скобок, уже трудно читать некоторые определения функций для закрытия и т. д., это добавляет больше беспорядка

Я не думаю, что голосование — хороший способ разработать язык. Особенно с очень трудным (вероятно, невозможным) определением и невероятно большим набором имеющих право голоса.

Я доверяю разработчикам и сообществу Go найти лучшее решение и
поэтому не чувствовал необходимости взвешивать что-либо в этом разговоре.
Однако я просто должен был сказать, как я был неожиданно рад
предложение синтаксиса F «T».

(Другие скобки Unicode:
https://unicode-search.net/unicode-namesearch.pl?term=BRACKET.)

Ваше здоровье,

  • Боб

В пятницу, 1 мая 2020 г., в 19:43 Мэтт Мак, [email protected] , написал:

После некоторых обсуждений и нескольких перечитываний я в целом поддерживаю
текущий проект дизайна контрактов в Go. Я ценю количество времени
и усилие, которое было вложено в это. В то время как объем, концепции,
реализации, и большинство компромиссов кажутся разумными, меня беспокоит то, что
синтаксис необходимо пересмотреть, чтобы улучшить читаемость.

Я написал ряд предлагаемых изменений, чтобы решить эту проблему:

Ключевые моменты:

  • Синтаксис вызова метода/утверждения типа для объявления контракта
  • «Пустой контракт»
  • Разделители без скобок

Рискуя опередить эссе, приведу несколько неподтвержденных фактов.
синтаксис, преобразованный из образцов в текущем проекте контрактов. Примечание
что форма разделителей F «T» носит иллюстративный, а не предписывающий характер; видеть
запись для деталей.

type List«type Element Contract{}» struct {
следующий *Список«Элемент»
элемент val
}

и

контракт viaStrings«To, From» {
To.Set(строка)
Строка From.String()
}
func SetViaStrings «тип To, From viaStrings»(s []From) []To {
r := make([]To, len(s))
для i, v := диапазон s {
г[я].Set(v.String())
}
вернуть г
}

и

func Keys«тип K сопоставимый, V контракт{}»(m map[K]V) []K {
r := make([]K, 0, len(m))
для k := диапазон m {
г = добавить (г, к)
}
вернуть р
}
k := maps.Keys(map[int]int{1:2, 2:4})

и

договор Числовой «Т» {
T.(int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
поплавок32, поплавок64,
комплекс64, комплекс128)
}
func DotProduct «тип T Числовой» (s1, s2 []T) T {
если len(s1) != len(s2) {
panic("DotProduct: части разной длины")
}
вар г Т
для я: = диапазон s1 {
г += s1[i] * s2[i]
}
вернуть р
}

Без реального изменения Контрактов под капотом это гораздо больше.
читается мне как разработчику Go. Я также чувствую себя намного увереннее
преподавание этой формы тому, кто изучает го (хотя и поздно
учебный план).

@ianlancetaylor https://github.com/ianlancetaylor На основе вашего комментария
в #36533 (комментарий)
https://github.com/golang/go/issues/36533#issuecomment-579484523 Я
пишите в этой ветке, а не создавайте новую тему. Это также указано
на странице отзывов о дженериках
https://github.com/golang/go/wiki/Go2GenericsFeedback . Не уверен, что я
нужно сделать что-нибудь еще, чтобы это "официально считалось" (т.е. Go 2
группа рассмотрения предложений https://github.com/golang/go/issues/33892 ?) или если
отзывы все еще активно собираются.


Вы получаете это, потому что подписаны на эту тему.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/golang/go/issues/15292#issuecomment-622657596 или
отписаться
https://github.com/notifications/unsubscribe-auth/AACQ2NJRBNLLDGY2XGCCQCLRPOCEHANCNFSM4CA35RXQ
.

Нам всем нужен наилучший синтаксис для Go. В черновом проекте используются круглые скобки, потому что он работает с остальной частью Go, не вызывая значительных неоднозначностей при синтаксическом анализе. Мы остались с ними, потому что в то время они были для нас лучшим решением, и потому что нужно было поджарить рыбу покрупнее. До сих пор они (круглые скобки) держались довольно хорошо.

В конце концов, если будет найдена гораздо лучшая нотация, ее очень легко изменить, если у нас нет гарантии совместимости, которой нужно придерживаться (парсер тривиально настраивается, и любой фрагмент кода может быть преобразован легко с gofmt).

@ianlancetaylor Спасибо за ответ, это ценно.

Ты прав; этот синтаксис был «не использовать круглые скобки для аргументов типа» и выбрать то, что, по моему мнению, было лучшим кандидатом, а затем внести изменения, чтобы попытаться облегчить проблемы реализации с помощью синтаксического анализатора.

Если синтаксис трудно читать (трудно понять, что происходит с первого взгляда), действительно ли он легко вписывается в существующий язык? Вот где я думаю, что позиция терпит неудачу.

Это правда, как вы заметили, вывод типа может значительно уменьшить количество аргументов типа, которые необходимо передать в клиентском коде. Я лично считаю, что автор библиотеки должен стремиться требовать передачи аргументов нулевого типа при использовании своего кода, и тем не менее это будет происходить на практике.

Прошлой ночью я случайно наткнулся на синтаксис шаблона для D , который в некоторых отношениях удивительно похож:

template Square(T) {
    T Square(T t) {
        return t * t;
    }
}

writefln("The square of %s is %s", 3, Square!(int)(3));

template TCopy(T) {
    void copy(out T to, T from) {
        to = from;
    }
}

int i;
TCopy!(int).copy(i, 3);

Я вижу два основных отличия:

  1. У них есть ! в качестве оператора инстанцирования для использования шаблонов.
  2. Их стиль объявления (без множественных возвращаемых значений, методы, вложенные в классы) означает, что в обычном коде изначально меньше круглых скобок, поэтому использование круглых скобок для параметров типа не создает такой же визуальной двусмысленности.

Оператор инстанцирования

При использовании контрактов основная визуальная неоднозначность возникает между созданием экземпляра и вызовом функции (или преобразованием типа, или...?). Одна из причин, по которой это проблематично, заключается в том, что создание экземпляров происходит во время компиляции, а вызовы функций — во время выполнения. В Go есть много визуальных подсказок, которые сообщают читателю, к какому лагерю принадлежит каждое предложение, но новый синтаксис запутывает их, поэтому это не очевидно, если вы смотрите на типы или ход выполнения программы.

Один надуманный пример:

// Instantiation with unexported types and then function call,
// or chained method call?
a := draw(square, ellipse)(canvas, color)

Предложение: используйте оператор инстанцирования для указания параметров типа. ! , которые использует D, кажется вполне приемлемым. Некоторый пример синтаксиса:

// Lifted from the design draft
func New(type K, V)(compare func(K, K) int) *Map!(K, V) {
    return &Map!(K, V){compare: compare}
}

// ...

func (m *Map(K, V)) InOrder() *Iterator!(K, V) {
    sender, receiver := chans.Ranger!(keyValue!(K, V))()
    var f func(*node!(K, V)) bool
    f = func(n *node!(K, V)) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue!(K, V){n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

С моей личной точки зрения, приведенный выше код на порядок легче читать. Я думаю, что это устраняет все неясности, как визуально, так и для парсера. Кроме того, я задаюсь вопросом, может ли это быть единственным наиболее важным изменением, которое можно было бы внести в контракты.

Стиль объявления

При объявлении типов, функций и методов меньше «времени выполнения или времени компиляции?» проблема. Gopher видит строку, начинающуюся с type или func , и знает, что он смотрит на объявление, а не на поведение программы.

Тем не менее, некоторые визуальные неясности все еще существуют:

// Type-parameterized function,
// or function with multiple return values?
func Draw(cvs canvas, t tool)(canvas, tool) {
    // ...
}
func Draw(type canvas, tool)(cvs canvas, t tool) {
    // ...
}

// Type-parameterized struct, or function call?
func Set(elem constructible) rect {
    // ...
}
type Set(type Elem comparable) struct{
    // ...
}

// Method call, or type-parameterized function?
func Map(type Element)(s []Element, f func(Element) Element) (results []Element) {
    // ...
}
func (t Element) Map(s []Element, f func(Element) Element) (results []Element) {
    // ...
}

Мысли:

  • Я думаю, что эти вопросы менее важны, чем проблема инстанцирования.
  • Наиболее очевидным решением будет изменение разделителей, используемых для аргументов типа.
  • Возможно, добавление туда какого-то другого оператора или символа ( ! может потеряться, а как насчет # ?) может устранить неоднозначность.

РЕДАКТИРОВАТЬ: @griesemer спасибо за дополнительные разъяснения!

Спасибо. Просто задам естественный вопрос: почему важно знать, оценивается ли конкретный вызов во время выполнения или во время компиляции? Почему это ключевой вопрос?

@тулбокс

// Instantiation with unexported types and then function call,
// or chained method call?
a := draw(square, ellipse)(canvas, color)

Почему это имеет значение в любом случае? Для случайного читателя не имеет значения, был ли это фрагмент кода, который выполнялся во время компиляции или во время выполнения. Все остальные могут просто взглянуть на определение функции, чтобы понять, что происходит. Ваши более поздние примеры вовсе не кажутся двусмысленными.

На самом деле, использование () для параметров типа имеет некоторый смысл, поскольку похоже, что вы вызываете функцию, которая возвращает функцию, и это более или менее правильно. Разница в том, что первая функция принимает типы, которые обычно пишутся в верхнем регистре или хорошо известны.

На данном этапе гораздо важнее определиться с размерами сарая, а не с его цветом.

Я не думаю, что @toolbox говорит о разнице между временем компиляции и временем выполнения. Да, это одно отличие, но оно не главное. Важный вопрос: это вызов функции или объявление типа? Вы хотите знать, потому что они ведут себя по-разному, и вам не нужно делать вывод, вызывает ли некоторое выражение два вызова функции или один, потому что это большая разница. Т.е. выражение типа a := draw(square, ellipse)(canvas, color) неоднозначно без выполнения работы по исследованию окружающей среды.

Важно иметь возможность визуально анализировать поток управления программы. Я думаю, что Go был отличным примером этого.

Спасибо. Просто задам естественный вопрос: почему важно знать, оценивается ли конкретный вызов во время выполнения или во время компиляции? Почему это ключевой вопрос?

Извините, кажется, я напортачил с общением. Это ключевой момент, который я пытался донести:

это не очевидно, если вы смотрите на типы или поток программ

(На данный момент одно решается во время компиляции, а другое возникает во время выполнения, но это… характеристики, а не ключевой момент, который @infogulch правильно уловил — спасибо!)


В нескольких местах я встречал мнение, что дженерики в черновике можно сравнить с вызовами функций: это своего рода функция времени компиляции, которая возвращает реальную функцию или тип. Хотя это полезно как мысленная модель того, что происходит во время компиляции, синтаксически это не переводится. Синтаксически они должны называться как функции. Вот пример:

// Example from the Contracts draft
Print(int)([]int{1, 2, 3})

// New naming that communicates behavior and intent
MakePrintFunc(int)([]int{1, 2, 3}) // Chained function call, great!

Там это на самом деле выглядит как функция, которая возвращает функцию; Я думаю, это вполне читабельно.

Другой способ сделать это - добавить ко всему суффикс Type , поэтому из названия ясно, что когда вы «вызываете» функцию, вы получаете тип. В противном случае не очевидно, что (например) Pair(...) создает тип структуры, а не структуру. Но если это соглашение действует, код становится понятным: a := drawType(square, ellipse)(canvas, color)

(Я понимаю, что прецедентом является соглашение «-er» для интерфейсов.)

Обратите внимание, что я не особенно поддерживаю вышеизложенное как решение, я просто иллюстрирую, как я думаю, что «дженерики как функции» не полностью и недвусмысленно выражены текущим синтаксисом.


Опять же, @infogulch очень хорошо резюмировал мою точку зрения. Я поддерживаю визуальное разделение аргументов типа, чтобы было ясно, что они являются частью типа .

Возможно, визуальная часть будет улучшена подсветкой синтаксиса редактора.

Я мало знаю о синтаксических анализаторах и о том, как вы не можете слишком много смотреть вперед.

С точки зрения пользователя я не хочу видеть еще один символ в своем коде, поэтому «» не получит моей поддержки (я не нашел их на своей клавиатуре!).

Однако видеть круглые скобки, за которыми следуют круглые скобки, тоже не очень приятно.

Как насчет простого использования фигурных скобок?

a := draw{square, ellipse}(canvas, color)

Однако в Print(int)([]int{1,2,3}) единственная разница в поведении - это "время компиляции и время выполнения". Да, MakePrintFunc вместо Print еще больше подчеркнет это сходство, но… разве это не аргумент в пользу того, чтобы не использовать MakePrintFunc ? Потому что это на самом деле скрывает реальную разницу в поведении.

FWIW, во всяком случае, вы, кажется, приводите аргумент в пользу использования разных разделителей для параметрических функций и параметрических типов. Потому что Print(int) на самом деле можно рассматривать как эквивалент функции, возвращающей функцию (оцениваемую во время компиляции), тогда как Pair(int, string) нельзя — это функция, возвращающая тип . Print(int) на самом деле является допустимым выражением, которое оценивается как func -значение, тогда как Pair(int, string) не является допустимым выражением, это спецификация типа. Таким образом, реальная разница в использовании заключается не в «универсальных и неуниверсальных функциях», а в «универсальных функциях и универсальных типах». И из этого POV я думаю, что есть веские основания использовать () по крайней мере для параметрических функций в любом случае, потому что это подчеркивает природу параметрических функций для фактического представления значений - и, возможно, нам следует использовать <> для параметрических типов.

Я думаю, что аргумент в пользу () для параметрических типов исходит из функционального программирования, где эти функции-возвращающие-типы являются реальной концепцией, называемой конструкторами типов, и на самом деле могут использоваться и упоминаться как функции. И FWIW, именно поэтому я бы не стал спорить с тем, чтобы не использовать () для параметрических типов. Лично мне очень нравится эта концепция, и я бы предпочел преимущество меньшего количества различных разделителей, а не преимущество устранения неоднозначности параметрических функций из параметрических типов - в конце концов, у нас нет проблем с чистыми идентификаторами , относящимися как к типам, так и к значениям. .

Я не думаю, что @toolbox говорит о разнице между временем компиляции и временем выполнения. Да, это одно отличие, но оно не главное. Важный вопрос: это вызов функции или объявление типа? Вы _хотите_ знать, потому что они ведут себя по-разному, и вам не нужно делать вывод, вызывает ли некоторое выражение два вызова функции или один, потому что это большая разница. Т.е. выражение типа a := draw(square, ellipse)(canvas, color) неоднозначно без выполнения работы по исследованию окружающей среды.

Важно иметь возможность визуально анализировать поток управления программы. Я думаю, что Go был отличным примером этого.

Объявления типов было бы очень легко увидеть, так как все они начинаются с ключевого слова type . Ваш пример явно не из таких.

Возможно, визуальная часть будет улучшена подсветкой синтаксиса редактора.

Думаю, в идеале синтаксис должен быть понятным, какого бы цвета он ни был. Так было и с Go, и я не думаю, что было бы хорошо отказываться от этого стандарта.

Как насчет простого использования фигурных скобок?

Я считаю, что это, к сожалению, конфликтует с литералом структуры.

Однако в Print(int)([]int{1,2,3}) единственная разница в поведении - это "время компиляции и время выполнения". Да, MakePrintFunc вместо Print еще больше подчеркнет это сходство, но… разве это не аргумент в пользу того, чтобы не использовать MakePrintFunc ? Потому что это на самом деле скрывает реальную разницу в поведении.

Ну, во-первых, именно поэтому я бы поддержал Print!(int)([]int{1,2,3}) вместо MakePrintFunc(int)([]int{1,2,3}) . Понятно, что происходит что-то уникальное.

Но опять же, вопрос, который @ianlancetaylor задал ранее: какое значение имеет, является ли тип инстанцирования/функции-возвращения-функции временем компиляции или временем выполнения?

Подумайте об этом, если бы вы написали несколько вызовов функций, и компилятор смог бы их оптимизировать и вычислить их результат во время компиляции, вы были бы довольны приростом производительности! Скорее, важным аспектом является то, что делает код, каково его поведение? Это должно быть очевидно с первого взгляда.

Когда я вижу Print(...) , мой первый инстинкт — «это вызов функции, которая куда-то записывает». Он не сообщает «это вернет функцию». На мой взгляд, любой из них лучше, потому что он может передать поведение и намерение:

  • MakePrintFunc(...)
  • Print!(...)
  • Print<...>

Другими словами, этот фрагмент кода «ссылается» или каким-то образом «дает мне» функцию, которую теперь можно вызвать в следующем фрагменте кода.

FWIW, во всяком случае, вы, кажется, приводите аргумент в пользу использования разных разделителей для параметрических функций и параметрических типов. ...

Нет, я знаю, что последние несколько примеров были о функциях, но я бы рекомендовал согласованный синтаксис для параметрических функций и параметрических типов. Я не верю, что команда Go добавит в Go обобщения, если только они не представляют собой единую концепцию с единым синтаксисом.

Когда я вижу Print(...) , мой первый инстинкт — «это вызов функции, которая куда-то записывает». Он не сообщает «это вернет функцию».

Также не работает func Print(…) func(…) , когда вызывается как Print(…) . Тем не менее, мы коллективно согласны с этим. Без специального синтаксиса вызова, если функция возвращает func .
Синтаксис Print(…) почти точно говорит вам, что он делает сегодня: что Print — это функция, которая возвращает некоторое значение, которое вычисляет Print(…) . Если вас интересует тип, возвращаемый функцией, посмотрите его определение.
Или, что более вероятно, используйте тот факт, что на самом деле это Print(…)(…) , как индикатор того, что он возвращает функцию.

Подумайте об этом, если бы вы написали несколько вызовов функций, и компилятор смог бы их оптимизировать и вычислить их результат во время компиляции, вы были бы довольны приростом производительности!

Конечно. У нас это уже есть. И я очень рад, что мне не нужны специальные синтаксические аннотации, чтобы сделать их особенными, но я могу просто верить, что компилятор будет постоянно улучшать эвристику того, какие функции это.

На мой взгляд, любой из них лучше, потому что он может передать поведение и намерение:

Обратите внимание, что первый как минимум на 100% совместим с дизайном. Он не предписывает какую-либо форму для используемых идентификаторов, и я надеюсь, что вы не предлагаете предписывать это (и если вы это сделаете, мне было бы интересно, почему те же правила не применяются к простому возврату func ).

Нет, я знаю, что последние несколько примеров были о функциях, но я бы рекомендовал согласованный синтаксис для параметрических функций и параметрических типов.

Что ж, я согласен, как я уже сказал :) Я просто говорю, что не понимаю, как аргументы, которые вы приводите, могут быть применены по оси «общий против не общего», поскольку между ними нет важных поведенческих изменений. два. Они имели бы смысл по оси «тип против функции», потому что то, является ли что-то спецификацией типа или выражением, очень важно для контекста, в котором оно может использоваться. Я все еще не согласен, но, по крайней мере, я бы понял их :)

@Merovius спасибо за ваш комментарий.

Также не работает func Print(…) func(…) , когда вызывается как Print(…) . Тем не менее, мы коллективно согласны с этим. Без специального синтаксиса вызова, если функция возвращает func.
Синтаксис Print(…) почти точно говорит вам, что он делает сегодня: что Print — это функция, которая возвращает некоторое значение, которое вычисляет Print(…) . Если вас интересует тип, возвращаемый функцией, посмотрите его определение.

Я придерживаюсь мнения, что имя функции должно быть связано с тем, что она делает. Поэтому я ожидаю, что Print(...) что-то напечатает, независимо от того, что он возвращает. Я считаю, что это разумное ожидание, и его можно найти в большинстве существующих кодов Go.

Если я вижу Print(...)(...) , это сообщает, что первая () что-то напечатала и что функция вернула какую-то функцию, а вторая () выполняет это дополнительное поведение. .

(Я был бы удивлен, если бы это было необычное или редкое мнение, но я бы не стал спорить с некоторыми результатами опроса.)

Обратите внимание, что первый как минимум на 100% совместим с дизайном. Он не предписывает какую-либо форму для используемых идентификаторов, и я надеюсь, что вы не предлагаете предписывать это (и если вы это сделаете, мне было бы интересно, почему те же правила не применяются к простому возврату func).

Вы чертовски правы, я предложил это :)

Послушайте, я перечислил 3 способа, которые я мог бы придумать, чтобы исправить визуальную двусмысленность, вызванную параметрами типа для функций и типов. Если вы не видите никакой двусмысленности, то ни одно из предложений вам не понравится!

Я просто говорю, что не понимаю, как аргументы, которые вы приводите, могут быть применены по оси «общий против не общего», поскольку между ними нет важных поведенческих изменений. Они имели бы смысл по оси «тип против функции», потому что то, является ли что-то спецификацией типа или выражением, очень важно для контекста, в котором оно может использоваться.

См. выше пункты о неоднозначности и 3 предлагаемых решения.

Параметры типа — это новая вещь.

  • Если мы хотим рассуждать о них как о чем-то новом, я предлагаю изменить разделители или добавить оператор инстанцирования, чтобы полностью отличить их от обычного кода: вызовы функций, преобразования типов и т. д.
  • Если мы хотим рассуждать о них как о еще одной функции, то я предлагаю четко называть эти функции так, чтобы identifier в identifier(...) сообщало о поведении и возвращаемом значении.

Я предпочитаю первое. В обоих случаях изменения будут глобальными для синтаксиса параметра типа, как обсуждалось.

Есть еще несколько способов пролить свет на это:

  1. Опрос
  2. Руководство

1. Опрос

Предисловие: Это не демократия. Я действительно думаю, что решения основаны на данных, и как сформулированная логика, так и общие данные опроса могут помочь процессу принятия решений.

У меня нет для этого средств, но мне было бы интересно узнать, что произойдет, если вы опросите несколько тысяч сусликов на тему «ранжировать их по чистоте».

Исходный уровень:

// Lifted from the design draft
func New(type K, V)(compare func(K, K) int) *Map(K, V) {
    return &Map(K, V){compare: compare}
}

// ...

func (m *Map(K, V)) InOrder() *Iterator(K, V) {
    sender, receiver := chans.Ranger(keyValue(K, V))()
    var f func(*node(K, V)) bool
    f = func(n *node(K, V)) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue(K, V){n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

Оператор инстанцирования:

// Lifted from the design draft
func New(type K, V)(compare func(K, K) int) *Map!(K, V) {
    return &Map!(K, V){compare: compare}
}

// ...

func (m *Map(K, V)) InOrder() *Iterator!(K, V) {
    sender, receiver := chans.Ranger!(keyValue!(K, V))()
    var f func(*node!(K, V)) bool
    f = func(n *node!(K, V)) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue!(K, V){n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

Угловые скобки: (или двойные угловые скобки, в любом случае)

// Lifted from the design draft
func New<type K, V>(compare func(K, K) int) *Map<K, V> {
    return &Map<K, V>{compare: compare}
}

// ...

func (m *Map<K, V>) InOrder() *Iterator<K, V> {
    sender, receiver := chans.Ranger<keyValue<K, V>>()
    var f func(*node<K, V>) bool
    f = func(n *node<K, V>) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue<K, V>{n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

Правильно названные функции:

// Lifted from the design draft
func NewConstructor(type K, V)(compare func(K, K) int) *MapType(K, V) {
    return &MapType(K, V){compare: compare}
}

// ...

func (m *MapType(K, V)) InOrder() *IteratorType(K, V) {
    sender, receiver := chans.RangerType(keyValueType(K, V))()
    var f func(*nodeType(K, V)) bool
    f = func(n *nodeType(K, V)) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValueType(K, V){n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

... Забавно, мне на самом деле очень нравится последний.

(Как вы думаете, как они поведут себя в широком мире Gophers @Merovius ?)

2. Учебник

Я думаю, что это было бы очень полезным упражнением: напишите удобный для начинающих учебник по вашему любимому синтаксису, и пусть несколько человек прочитают его и применят. Насколько легко передаются концепции? Что такое часто задаваемые вопросы и как вы на них отвечаете?

Проект дизайна предназначен для того, чтобы донести концепцию до опытных сусликов. Он следует логической цепочке, медленно погружая вас. Какая краткая версия? Как вы объясните Золотые правила контрактов в одном легко усваиваемом посте в блоге?

Это может представить своего рода другой угол или часть данных, чем типичные отчеты обратной связи.

@toolbox Я думаю, что вы еще не ответили: почему это проблема для параметрических функций, но не для непараметрических функций, возвращающих func ? Я могу сегодня написать

func Print(a string) func(string) {
    return func(b string) {
        fmt.Println(a+b)
    }
}

func main() {
    Print("foo")("bar")
}

Почему это нормально и не приводит вас в замешательство из-за двусмысленности, но как только Print принимает параметр-тип вместо параметра-значения, это становится невыносимым? И не могли бы вы (оставляя в стороне очевидные вопросы совместимости) также предложить нам добавить ограничение для правильной работы, что это не должно быть возможно, если только Print не будет переименован в MakeXFunc для некоторого X ? Если нет, то почему?

@toolbox действительно ли это будет проблемой, если предположить, что вывод типа вполне может устранить необходимость указывать параметрические типы для функций, оставив только простой вызов функции?

@Merovius Я не думаю, что проблема в самом синтаксисе Print("foo")("bar") , потому что это уже возможно в Go 1 именно потому, что у него есть единственная возможная интерпретация . Проблема в том, что с немодифицированным предложением выражение Foo(X)(Y) теперь неоднозначно и может означать, что вы делаете два вызова функции (как в Go 1), или это может означать, что вы делаете один вызов функции с аргументами типа . Проблема заключается в том, чтобы локально определить, что делает программа, и эти две возможные семантические интерпретации очень разные .

@urandom Я согласен с тем, что вывод типа может устранить большую часть явно предоставленных параметров типа, но я не думаю, что запихивать всю когнитивную сложность в темные уголки языка только потому, что они редко используются, это хорошая идея либо. Даже если они настолько редки, что большинство людей обычно не сталкиваются с ними, они все же иногда сталкиваются с ними, и если какой-то код имеет запутанный поток управления, если это не «большинство» кода, у меня остается неприятный привкус во рту. Тем более, что Go в настоящее время настолько доступен при чтении «санитарного» кода, включая stdlib. Может быть, вывод типов настолько хорош, что «редко» становится «никогда», а программисты Go остаются очень дисциплинированными и никогда не разрабатывают систему, в которой необходимы параметры типа; тогда весь этот вопрос в принципе спорный. Но я бы не стал на это ставить.

Я думаю, что основной смысл аргумента @toolbox заключается в том, что мы не должны беспечно перегружать существующий синтаксис контекстно-зависимой семантикой, и вместо этого мы должны найти какой-то другой синтаксис, который не является двусмысленным (даже если это просто небольшое дополнение, такое как Foo(X)!(Y) .) Я думаю, что это важная мера при рассмотрении вариантов синтаксиса.

Я использовал и читал немного кода D еще в те дни (~ 2008-2009), и я должен сказать, что ! всегда сбивал меня с толку.

позвольте мне вместо этого нарисовать этот сарай с помощью # , $ или @ (поскольку они не имеют никакого значения в Go или C).
тогда это может открыть возможность использовать фигурные скобки без какой-либо путаницы с картами, срезами или структурами.

  • Foo@{X}(Y)
  • Foo${X}(Y)
  • Foo#{X}(Y)
    или квадратные скобки.

В таких обсуждениях важно смотреть на реальный код.

Например, учтите, что мало кто пишет Foo(X)(Y) . В Go имена типов, имена переменных и имена функций выглядят одинаково, но люди редко путаются в том, на что смотрят. Люди понимают, что int64(v) — это преобразование типов, а F(v) — это вызов функции, даже если они выглядят совершенно одинаково.

Нам нужно посмотреть на реальный код, чтобы увидеть, действительно ли аргументы типа на практике сбивают с толку. Если они есть, то мы должны настроить синтаксис. В отсутствие реального кода мы просто не знаем.

В среду, 6 мая 2020 г., в 13:00 Ян Лэнс Тейлор написал:

Люди понимают, что int64(v) — это преобразование типов, а F(v) — это
вызов функции, даже если они выглядят совершенно одинаково.

У меня нет мнения так или иначе прямо сейчас по предложению
синтаксис, но я не думаю, что этот конкретный пример очень хорош. Это может
верно для встроенных типов, но я действительно запутался в этом
точную проблему несколько раз (я искал функцию
определение и очень запутался в том, как код работал раньше
Я понял, что это, вероятно, тип, и я не мог найти функцию, потому что
это вообще не был вызов функции). Не конец света, а
вероятно, это не проблема для людей, которым нравятся модные IDE, но я
потратил впустую 5 минут или около того, пытаясь найти это несколько раз.

-Сэм

--
Сэм Уайтд

@ianlancetaylor одна вещь, которую я заметил в вашем примере, заключается в том, что вы можете написать функцию, которая принимает тип и возвращает другой тип с тем же значением, поэтому вызов типа как базового преобразования типа, такого как int64(v) , имеет смысл в так же, как strconv.Atoi(v) имеет смысл.

Но в то время как вы можете сделать UseConverter(strconv.Atoi) , UseConverter(int64) невозможно в Go 1. Наличие круглых скобок для параметра типа может открыть некоторые возможности, если дженерик можно использовать для приведения, например:

func StrToNumber(type K)(s string) K {
  asInt := strconb.Atoi(s)
  return K(asInt)
}

Почему это нормально и не приводит вас в замешательство из-за двусмысленности

Ваш пример не годится. Меня не волнует, принимает ли первый вызов аргументы или параметры типа. У вас есть функция Print , которая ничего не печатает. Можете ли вы представить себе чтение/просмотр этого кода? Print("foo") с опущенным вторым набором круглых скобок выглядит нормально, но тайно не работает.

Если бы вы предоставили мне этот код в PR, я бы посоветовал вам изменить имя на PrintFunc , или MakePrintFunc , или PrintPlusFunc , или на что-то, что отражало бы его поведение.

Я использовал и читал немного кода D еще в те дни (~ 2008-2009), и я должен сказать, что ! всегда сбивал меня с толку.

Ха, интересно. У меня нет особых предпочтений в отношении оператора инстанцирования; те кажутся достойными вариантами.

В Go имена типов, имена переменных и имена функций выглядят одинаково, но люди редко путаются в том, на что смотрят. Люди понимают, что int64(v) — это преобразование типов, а F(v) — это вызов функции, даже если они выглядят совершенно одинаково.

Я согласен, люди обычно могут быстро отличить преобразование типов от вызовов функций. Как вы думаете, почему?

Моя личная теория заключается в том, что типы обычно представляют собой существительные, а функции — глаголы. Итак, когда вы видите Noun(...) , совершенно ясно, что это преобразование типов, а когда вы видите Verb(...) , это вызов функции.

Нам нужно посмотреть на реальный код, чтобы увидеть, действительно ли аргументы типа на практике сбивают с толку. Если они есть, то мы должны настроить синтаксис. В отсутствие реального кода мы просто не знаем.

В этом есть смысл.

Лично я пришел в эту ветку, потому что я прочитал черновик контрактов (вероятно, 5 раз, каждый раз отскакивая, а затем углубляясь, когда я возвращался позже) и нашел синтаксис запутанным и незнакомым. Мне понравились концепции , когда я, наконец, их разобрала, но возник огромный барьер из-за неоднозначного синтаксиса.

В нижней части проекта Contracts есть много «реального кода», обрабатывающего все эти распространенные варианты использования, и это здорово! Однако мне сложно визуально анализировать; Я медленнее читаю и понимаю код. Мне кажется, что я должен смотреть на аргументы вещей и более широкий контекст, чтобы знать, что такое вещи и каков поток управления, и мне кажется, что это шаг назад по сравнению с обычным кодом.

Возьмем этот реальный код:

import "container/orderedmap"

var m = orderedmap.New(string, string)(strings.Compare)

func Add(a, b string) {
    m.Insert(a, b)
}

Когда я читаю orderedmap.New( , я ожидаю, что дальше будут аргументы для функции New , те ключевые фрагменты информации, которые необходимы упорядоченной карте для функционирования. Но на самом деле они находятся во втором наборе скобок. Меня это бросает. Это делает код более трудным для понимания.

(Это всего лишь один пример, не все , что я вижу, неоднозначно, но трудно провести подробное обсуждение широкого набора вопросов.)

Вот что я бы предложил:

// Instantiation operator
var m = orderedmap.New!(string, string)(strings.Compare)
// Alternate delimiters -- notice I don't insist on any particular kind
var m = orderedmap.New<|string, string|>(strings.Compare)
// Appropriately named function
var m = orderedmap.MakeConstructor(string, string)(strings.Compare)

В первых двух примерах другой синтаксис опровергает мое предположение о том, что первый набор круглых скобок содержит аргументы для New() , поэтому код менее удивителен, а поток более заметен с высокого уровня.

Третий вариант использует именование, чтобы сделать поток неудивительным. Теперь я ожидаю, что первый набор скобок будет содержать аргументы, необходимые для создания функции -конструктора, и я ожидаю, что возвращаемое значение является функцией-конструктором, которую, в свою очередь, можно вызвать для создания упорядоченной карты.


Я точно могу читать код в текущем стиле. Я смог прочитать весь код в черновике Contracts. Это просто медленнее, потому что мне требуется больше времени, чтобы обработать его. Я изо всех сил пытался проанализировать, почему это так, и сообщить об этом: в дополнение к примеру orderedmap.New https://github.com/golang/go/issues/15292#issuecomment -623649521 содержит хорошее резюме. , хотя я, вероятно, мог бы придумать больше. Степень неоднозначности варьируется между различными примерами.

Я признаю, что не получу всеобщего согласия, потому что удобочитаемость и ясность несколько субъективны и, возможно, зависят от происхождения человека и любимых языков. Я действительно думаю, что 4 вида неоднозначности синтаксического анализа — хороший показатель того, что у нас есть проблема.

import "container/orderedmap"

var m = orderedmap.NewOf(string, string)(strings.Compare)

func Add(a, b string) {
    m.Insert(a, b)
}

Я думаю, что NewOf читается лучше, чем New , потому что New обычно возвращает экземпляр, а не общий, который создает экземпляр.


У вас есть функция Print , которая ничего не печатает.

Чтобы было ясно, поскольку существует некоторый автоматический вывод типа, универсальный Print(foo) будет либо реальным вызовом печати через вывод, либо ошибкой. В Go сегодня запрещены голые идентификаторы:

package main

import (
    "fmt"
)

func main() {
    fmt.Println
}

./prog.go:8:5: fmt.Println evaluated but not used

Мне интересно, есть ли способ сделать общий вывод менее запутанным.

@тулбокс

Ваш пример не годится. Меня не волнует, принимает ли первый вызов аргументы или параметры типа. У вас есть функция печати, которая ничего не печатает. Можете ли вы представить себе чтение/просмотр этого кода?

Вы пропустили здесь соответствующие дополнительные вопросы. Я согласен с вами, что это не очень читабельно. Но вы выступаете за соблюдение этого ограничения на уровне языка. Я не говорил «у вас все в порядке с этим», имея в виду «у вас все в порядке с этим кодом», но имел в виду «у вас все в порядке с языком, допускающим этот код».

Это был мой дополнительный вопрос. Считаете ли вы, что язык Go хуже, потому что в нем нет ограничения на имена для функций-которые-возвращают- func ? Если нет, то почему язык будет хуже, если мы не наложим это ограничение на такие функции, когда они принимают аргумент типа вместо аргумента значения?

@Меровиус

Но вы выступаете за соблюдение этого ограничения на уровне языка.

Нет, он утверждает, что опора на стандарты именования является потенциально правильным решением проблемы. Неофициальное правило, такое как «авторам типов рекомендуется называть свои универсальные типы таким образом, чтобы их было легче спутать с именем функции», является допустимым решением проблемы неоднозначности, поскольку оно буквально решит проблему в отдельных случаях.

Он нигде не намекает, что это решение должно быть реализовано языком, он говорит, что если сопровождающие решат оставить текущее предложение как есть, даже тогда существуют потенциальные практические решения проблемы неоднозначности. И он утверждает, что проблема двусмысленности реальна и ее важно учитывать.

Редактировать: я думаю, что мы немного отклоняемся от курса. Я думаю, что более «реальный» пример кода был бы очень полезен для разговора на этом этапе.

Нет, он утверждает, что опора на стандарты именования является потенциально правильным решением проблемы.

Они? Я пытался конкретно спросить:

Обратите внимание, что первый как минимум на 100% совместим с дизайном. Он не предписывает какую-либо форму для используемых идентификаторов, и я надеюсь, что вы не предлагаете предписывать это (и если вы это сделаете, мне было бы интересно, почему те же правила не применяются к простому возврату func).

Вы чертовски правы, я предложил это :)

Я согласен, что «предписать» здесь не очень конкретно, но это, по крайней мере, вопрос, который я имел в виду. Если они действительно не выступают за требование уровня языка, встроенное в дизайн, я, конечно, извиняюсь за недопонимание. Но я чувствую себя вправе предположить, что «предписание» по крайней мере сильнее, чем «неформальное правило». Особенно, если поместить их в контекст двух других предложений, которые они выдвинули (на том же основании), которые представляют собой конструкции на уровне языка, поскольку они даже не используют действительные в настоящее время идентификаторы.

Будет ли план, похожий на vgo , позволяющий сообществу опробовать последнее универсальное предложение?

Изучите и попробуйте. Информация:
https://blog.tempus-ex.com/generics-in-go-how-they-work-and-how-to-play-with-them/

Поиграв немного с игровой площадкой с поддержкой контрактов, я действительно не понимаю, в чем заключается вся эта суета, связанная с необходимостью различать аргументы типа и обычные аргументы.

Рассмотрим этот пример . Я оставил инициализаторы типов во всех функциях, хотя я мог бы опустить их все, и все равно компилировался бы нормально. Это, кажется, указывает на то, что подавляющее большинство такого потенциального кода даже не будет включать их, что, в свою очередь, не вызовет путаницы.

Однако в случае включения этих типовых параметров можно сделать некоторые замечания:
а) типы либо встроенные, которые все знают и сразу могут идентифицировать
б) типы являются сторонними, и в этом случае они будут иметь заглавный регистр, что сделает их немного выделяющимися. Да, было бы возможно, хотя и маловероятно, что это может быть функция, которая возвращает другую функцию, и первый вызов использует экспортированные переменные сторонних производителей, но я думаю, что это крайне редко.
c) типы являются некоторыми частными типами. В этом случае они будут больше похожи на обычные идентификаторы переменных. Однако, поскольку они не экспортируются, это будет означать, что код, на который смотрит читатель, не является частью какой-либо документации, которую он пытается расшифровать, и, что более важно, он уже читает код. Поэтому они могут сделать дополнительный шаг и просто перейти к определению функции, чтобы устранить любую двусмысленность.

Суета в том, как это выглядит без дженериков https://play.golang.org/p/7BRdM2S5dwQ и для тех, кто плохо знаком с программированием отдельного стека для каждого типа, например StackString, StackInt,... намного проще программировать затем стек (T) в текущем предложении общего синтаксиса. Я не сомневаюсь, что нынешнее предложение хорошо продумано, как показано на вашем примере, но ценность простоты и ясности значительно снижается. Я понимаю, что первоочередной задачей является выяснить, работает ли это путем тестирования, но как только мы приходим к согласию, что текущее предложение охватывает большинство случаев, и нет никаких технических сложностей с компилятором, еще более высокий приоритет заключается в том, чтобы сделать его понятным для всех, что всегда было причиной номер один Идите к успеху с самого начала.

@Merovius Нет, как сказал @infogulch , я имел в виду создание соглашения в стиле -er для интерфейсов. Я упоминал об этом выше, извините за сумбурность. (Кстати, я "он").

Рассмотрим этот пример. Я оставил инициализаторы типов во всех функциях, хотя я мог бы опустить их все, и все равно компилировался бы нормально. Это, кажется, указывает на то, что подавляющее большинство такого потенциального кода даже не будет включать их, что, в свою очередь, не вызовет путаницы.

Как насчет того же примера в разветвленной версии игровой площадки дженериков?

Я использовал ::<> для предложения параметра типа, и если есть один тип, вы можете опустить <> . Угловые скобки не должны вызывать двусмысленности синтаксического анализатора, и это облегчает мне чтение кода — как дженериков, так и кода, использующего дженерики. (И если параметры типа выводятся, тем лучше.)

Как я уже говорил ранее, я не застрял на ! для создания экземпляра типа (и я думаю, что :: выглядит лучше при просмотре). И это помогает только там, где используются дженерики, а не в объявлениях. Таким образом, это несколько объединяет два, опуская <> там, где это не нужно, что-то вроде пропуска () для параметров возврата функции, если есть только один.

Образец выдержки:

type Stack::<type E> []E

func (s Stack::E) Peek() E {
    return s[len(s)-1]
}

func (s *Stack::E) Pop() {
    *s = (*s)[:len(*s)-1]
}

func (s *Stack::E) Push(value E) {
    *s = append(*s, value)
}

type StackIterator::<type E> struct{
    stack Stack::E
    current int
}

func (s *Stack::E) Iter() Iterator::E {
    it := StackIterator::E{stack: *s, current: len(*s)}

    return &it
}

func (i *StackIterator::E) Next() (bool) { 
    i.current--

    if i.current < 0 { 
        return false
    }

    return true
}

func (i *StackIterator::E) Value() E { 
    if i.current < 0 {
        var zero E
        return zero
    }

    return i.stack[i.current]
}

// ...

var it Iterator::string = stack.Iter()

it = Filter::string(it, func(s string) bool {
    return s == "foo" || s == "beta" || s == "delta"
})

it = Map::<string, string>(it, func(s string) string {
    return s + ":1"
})

it = Distinct::string(it)

println(Reduce(it, "", func(a, b string) string {
    if a == "" {
        return b
    }
        return a + ":" + b
}))

Для этого примера я также изменил имена переменных, я думаю, что E для «Элемент» более читаем, чем T для «Тип».

Как я уже сказал, благодаря тому, что дженерики выглядят по-другому, основной код Go становится видимым. Вы знаете, на что смотрите, поток управления очевиден, нет двусмысленности и т. д.

Это также просто отлично с большим выводом типа:

var it Iterator::string = stack.Iter()

it = Filter(it, func(s string) bool {
    return s == "foo" || s == "beta" || s == "delta"
})

it = Map::<string, string>(it, func(s string) string {
    return s + ":1"
})

it = Distinct(it)

println(Reduce(it, "", func(a, b string) string {
    if a == "" {
        return b
    }
        return a + ":" + b
}))

@toolbox Извиняюсь, мы говорили мимо друг друга :)

кто-то, кто плохо знаком с программированием отдельного стека для каждого типа, например StackString, StackInt,... намного проще программировать, чем стек (T)

Я бы очень удивился, если бы это было так. Никто не безошибочен, и первая ошибка, которая прокрадывается даже в простой фрагмент кода, покажет, насколько ошибочным является это утверждение в долгосрочной перспективе.

Смысл моего примера состоял в том, чтобы проиллюстрировать использование параметрических функций и их инстанцирование с конкретными типами, что является сутью этого обсуждения, а не того, была ли хороша примерная реализация Stack .

Смысл моего примера состоял в том, чтобы проиллюстрировать использование параметрических функций и их инстанцирование с конкретными типами, что является сутью этого обсуждения, а не в том, хороша ли демонстрационная реализация стека.

Я не думаю, что @gertcuykens хотел сбить вашу реализацию стека, похоже, он чувствовал, что синтаксис дженериков незнаком и труден для понимания.

Однако в случае включения этих типовых параметров можно сделать некоторые замечания:
(а)...(б)...(в)...(г)...

Я вижу все ваши пункты, ценю ваш анализ, и они не ошибаются. Вы правы в том, что в большинстве случаев, внимательно изучив код, вы можете определить, что он делает. Я не думаю, что это опровергает отчеты разработчиков Go, которые говорят, что синтаксис сбивает с толку, двусмыслен или требует больше времени для чтения, даже если они в конечном итоге могут его прочитать.

В целом синтаксис находится в зловещей долине. Код делает что-то другое, но он выглядит достаточно похожим на существующие конструкции, поэтому ваши ожидания не оправдываются, а удобство просмотра падает. Вы также не можете установить новые ожидания, потому что (соответственно) эти элементы необязательны как в целом, так и по частям.

Для этих более конкретных патологических случаев @infogulch хорошо сформулировал это:

Я не думаю, что запихивать всю когнитивную сложность в темные уголки языка только потому, что они редко используются, — тоже хорошая идея. Даже если они настолько редки, что большинство людей обычно не сталкиваются с ними, они все же иногда сталкиваются с ними, и если какой-то код имеет запутанный поток управления, если это не «большинство» кода, у меня остается неприятный привкус во рту.

Я думаю, что на данный момент мы достигли насыщения артикуляции в этом конкретном фрагменте темы. Неважно, сколько мы об этом говорим, лакмусовой бумажкой будет то, насколько быстро и насколько хорошо разработчики Go смогут его изучить, прочитать и написать.

(И да, прежде чем это будет указано, бремя должно быть на авторе библиотеки, а не на разработчике клиента, но я не думаю, что нам нужен эффект повышения, когда общие библиотеки непонятны для человека с улицы. Я также не Я не хочу, чтобы Go превратился в Универсальный Джамбори, но отчасти я верю, что упущения в дизайне ограничат распространение .)

У нас есть игровая площадка, и мы можем делать форки для другого синтаксиса , и это просто фантастика. Может быть, нам нужно еще больше инструментов!

Люди оставили отзыв . Я уверен, что необходимо больше обратной связи, и, возможно, нам нужны более совершенные или оптимизированные системы обратной связи.

@toolbox Как вы думаете, возможно ли проанализировать код, если вы всегда опускаете <> и type вот так? Может быть, требуется более строгое предложение в том, что можно сделать, но, может быть, это стоит компромисса?

type Stack::E []E

func (s Stack::E) Peek() E {
    return s[len(s)-1]
}

func (s *Stack::E) Pop() {
    *s = (*s)[:len(*s)-1]
}

func (s *Stack::E) Push(value E) {
    *s = append(*s, value)
}

type StackIterator::E struct{
    stack Stack::E
    current int
}

func (s *Stack::E) Iter() Iterator::E {
    it := StackIterator::E{stack: *s, current: len(*s)}

    return &it
}

func (i *StackIterator::E) Next() (bool) { 
    i.current--

    if i.current < 0 { 
        return false
    }

    return true
}

func (i *StackIterator::E) Value() E { 
    if i.current < 0 {
        var zero E
        return zero
    }

    return i.stack[i.current]
}

// ...

var it Iterator::string = stack.Iter()

it = Filter::string(it, func(s string) bool {
    return s == "foo" || s == "beta" || s == "delta"
})

it = Map::string, string (it, func(s string) string {
    return s + ":1"
})

it = Distinct::string(it)

println(Reduce(it, "", func(a, b string) string {
    if a == "" {
        return b
    }
        return a + ":" + b
}))

Я не знаю почему, но это Map::string, string (... кажется странным. Похоже, что это создает 2 токена: Map::string и вызов функции string .

Кроме того, несмотря на то, что это не используется в Go, использование «Идентификатора :: Идентификатора» может создать неправильное впечатление у новых пользователей, думая, что существует класс/пространство имен Filter с string Функция

Как вы думаете, возможно ли проанализировать код, если вы всегда опускаете <> и вводите вот так? Может быть, требуется более строгое предложение в том, что можно сделать, но, может быть, это стоит компромисса?

Нет, я так не думаю. Я согласен с @urandom в том, что символ пробела без чего-либо окружающего делает его похожим на два токена. Мне также лично нравится объем Контрактов, и я не заинтересован в изменении его возможностей.

Кроме того, несмотря на то, что это не используется в Go, использование «Identifier::Identifier» может создать неправильное впечатление у новых пользователей, которые думают, что в нем есть класс/пространство имен Filter со строковой функцией. Повторное использование токенов из других широко распространенных языков для чего-то совершенно другого вызовет много путаницы.

На самом деле я не использовал язык с :: , но я видел его. Может быть, ! лучше, потому что он будет соответствовать D, хотя я считаю, что :: выглядит лучше визуально.

Если мы пойдем по этому пути, может возникнуть много дискуссий о том, какие именно символы использовать. Вот попытка сузить то, что мы ищем:

  • Что-то кроме голых identifier() , чтобы это не выглядело как вызов функции.
  • Что-то, что может заключать в себе несколько параметров типа, чтобы визуально объединять их так, как это делают круглые скобки.
  • Что-то, что выглядит связанным с идентификатором, поэтому выглядит как единица.
  • Что-то, что не является двусмысленным для синтаксического анализатора.
  • Что-то, что не конфликтует с другой концепцией, которая имеет сильное мнение разработчиков.
  • Если возможно, что-то, что повлияет на определения, а также на использование дженериков, чтобы их также стало легче читать.

Там много чего может подойти.

  • identifier!(a, b) ( детская площадка )
  • identifier@(a, b)
  • identifier#(a, b)
  • identifier$(a, b)
  • identifier<:a, b:>
  • identifier.<a, b> это как утверждение типа!
  • identifier:<a, b>
  • и Т. Д.

У кого-нибудь есть идеи, как еще больше сузить набор потенциалов?

Просто короткое замечание, что мы рассмотрели все эти идеи, и мы также рассмотрели такие идеи, как

func F(T : a, b T) { }
func G() { F(int : 1, 2) }

Но опять же, доказательство пудинга в еде. Абстрактные обсуждения в отсутствие кода заслуживают внимания, но не приводят к окончательным выводам.

(Не уверен, что об этом говорилось раньше). Я вижу, что в тех случаях, когда мы получаем структуру, мы не сможем «расширить» существующий API для обработки универсальных типов без нарушения существующего кода вызова.

Например, учитывая эту необобщенную функцию

func Repeat(v, n int) []int {
    var r []int
    for i := n; i > 0; i-- {
        r = append(r, v)
    }
    return r
}

Repeat(4, 4)

Мы можем сделать его универсальным, не нарушая обратной совместимости.

func Repeat(type T)(v T, n int) []T {
    var r []T
    for i := n; i > 0; i-- {
        r = append(r, v)
    }
    return r
}

Repeat("a", 5)

Но если мы хотим сделать то же самое с функцией, которая получает общий struct

type XY struct {
    X, Y int
}

func RangeRepeat(arr []XY) []int {
    var r []int
    for _, n := range arr {
        for i := n.Y; i > 0; i-- {
            r = append(r, n.X)
        }
    }
    return r
}

RangeRepeat([]XY{{1, 1}, {2, 2}, {3, 3}})

похоже, код вызова нужно обновить

type XY(type T) struct {
    X T
    Y int
}

func RangeRepeat(type T)(arr []XY(T)) []T {
    var r []T
    for _, n := range arr {
        for i := n.Y; i > 0; i-- {
            r = append(r, n.X)
        }
    }
    return r
}

// error: cannot use generic type XY(type T any) without instantiation
// RangeRepeat([]XY{{1, 1}, {2, 2}, {3, 3}}) // error in old code
RangeRepeat([](XY(int)){{1, 1}, {2, 2}, {3, 3}}) // API changed
// RangeRepeat([]XY{{"1", 1}, {"2", 2}, {"3", 3}}) // error
RangeRepeat([](XY(string)){{"1", 1}, {"2", 2}, {"3", 3}}) // ok

Было бы здорово иметь возможность также получать типы из структур.

@ianlancetaylor

В проекте контракта упоминается, что methods may not take additional type arguments . Однако о замене контракта на отдельные методы речи не идет. Такая функция была бы очень полезна для реализации интерфейсов в зависимости от того, к какому контракту привязан параметрический тип.

Вы обсуждали такую ​​возможность?

Еще вопрос по проекту контракта. Будут ли дизъюнкции типов ограничены встроенными типами? Если нет, можно ли использовать параметризованные типы, особенно интерфейсы в списке дизъюнкции?

Что-то типа

type Getter(T) interface {
    Get() T
}

contract(G, T) {
    G Getter(T)
}

было бы весьма полезно не только для того, чтобы избежать дублирования набора методов из интерфейса в контракт, но и для создания экземпляра параметризованного типа в случае сбоя вывода типа, и у вас нет доступа к конкретному типу (например, он не экспортируется)

@ianlancetaylor Я не уверен, обсуждалось ли это раньше, но что касается синтаксиса аргументов типа для функции, возможно ли объединить список аргументов со списком аргументов типа? Итак, для примера с графиком вместо

var g = graph.New(*Vertex, *FromTo)([]*Vertex{ ... })

ты бы использовал

var g = graph.New(*Vertex, *FromTo, []*Vertex{ ... })

По сути, первые K аргументов списка аргументов соответствуют списку аргументов типа длины K. Остальная часть списка аргументов соответствует обычным аргументам функции. Преимущество этого заключается в отражении синтаксиса

make(Type, size)

который принимает тип в качестве первого аргумента.

Это упростило бы грамматику, но требует информации о типе, чтобы знать, где заканчиваются аргументы типа и начинаются обычные аргументы.

@ smasher164 В нескольких комментариях он сказал, что они рассмотрели это (что означает, что они отбросили его, хотя мне любопытно, почему).

func F(T : a, b T) { }
func G() { F(int : 1, 2) }

Это то, что вы предлагаете, но с двоеточием для разделения двух типов аргументов. Лично мне умеренно нравится, хотя картина неполная; как насчет объявления типа, методов, создания экземпляров и т. д.

Я хочу вернуться к тому, что @Inuart сказал:

Мы можем сделать его универсальным, не нарушая обратной совместимости.

Будет ли команда Go рассматривать изменение стандартной библиотеки таким образом, чтобы соответствовать гарантии совместимости с Go 1? Например, что если strings.Repeat(s string, count int) string заменить на Repeat(type S stringlike)(s S, count int) S ? Вы также можете добавить комментарий //Deprecated к bytes.Repeat , но оставить его для использования в устаревшем коде. Это то, что команда Go рассмотрит?

Редактировать: чтобы было ясно, я имею в виду, будет ли это рассматриваться в Go1Compat в целом? Не обращайте внимания на конкретный пример, если он вам не нравится.

@carlmjohnson Нет. Этот код сломается: f := strings.Repeat , поскольку на полиморфные функции нельзя ссылаться без предварительного создания их экземпляров.

Исходя из этого, я думаю, что объединение аргументов-типов и аргументов-значений было бы ошибкой, поскольку это препятствует естественному синтаксису для ссылки на конкретизированную версию функции. Было бы более естественно, если бы в go уже было каррирование, но его нет. Выглядит странно, что foo(int, 42) и foo(int) являются выражениями, причем оба имеют очень разные типы.

@urandom Да, мы обсудили возможность добавления дополнительных ограничений на параметры типа отдельного метода. Это приведет к тому, что набор методов параметризованного типа будет меняться в зависимости от аргументов типа. Это может быть полезно или может сбивать с толку, но одно кажется бесспорным: мы можем добавить его позже, ничего не нарушая. Поэтому мы отложили эту идею на потом. Спасибо, что подняли это.

Что именно может быть указано в списке разрешенных типов, не так ясно, как могло бы быть. Думаю, у нас есть еще над чем поработать. Обратите внимание, что, по крайней мере, в текущем проекте проекта перечисление типа интерфейса в списке типов в настоящее время означает, что аргумент типа может быть этим типом интерфейса. Это не означает, что аргумент типа может быть типом, реализующим этот тип интерфейса. Я думаю, что в настоящее время неясно, может ли это быть экземпляром параметризованного типа. Но это хороший вопрос.

@smasher164 @toolbox Случаи, которые следует учитывать при рассмотрении объединения параметров типа и обычных параметров в одном списке, — это то, как их разделить (если они разделены) и как обрабатывать случай, когда нет обычных параметров (предположительно, мы можем исключить случай отсутствия параметров типа). Например, если нет обычных параметров, как отличить создание экземпляра функции без ее вызова и создание экземпляра функции и ее вызов? Хотя очевидно, что последний случай является более распространенным, для людей разумно хотеть иметь возможность писать первый случай.

Если бы параметры типа были помещены в те же круглые скобки, что и обычные параметры, то @griesemer сказал в #36177 (его второй пост), что ему очень понравилось использование точки с запятой, а не двоеточия в качестве разделителя, потому что (в результате автоматической вставки точки с запятой) позволяло красиво распределять параметры по нескольким строкам.

Лично мне также нравится использование вертикальных черточек ( |..| ) для выделения параметров типа, поскольку вы иногда видите, что они используются в других языках (Ruby, Crystal и т. д.) для выделения блока параметров. Итак, у нас были бы такие вещи, как:

func F(|T| a, b T) { }
func G() { F(|int| 1, 2) }

Преимущества включают в себя:

  • Они обеспечивают приятное визуальное различие (по крайней мере, на мой взгляд) между типом и обычными параметрами.
  • Вам не нужно будет использовать ключевое слово type .
  • Отсутствие обычных параметров не является проблемой.
  • Символ вертикальной черты, конечно же, входит в набор ASCII и поэтому должен быть доступен на большинстве клавиатур.

Возможно, вы даже сможете использовать его вне круглых скобок, но, предположительно, у вас возникнут те же трудности синтаксического анализа, что и с <...> или [...] , поскольку его можно ошибочно принять за побитовый оператор «или», хотя, возможно, трудности были бы менее острыми.

Я не понимаю, как вертикальные полосы помогают в случае отсутствия обычных параметров. Я не понимаю, как можно отличить создание экземпляра функции от вызова функции.

Одним из способов различения этих двух случаев было бы требование ключевого слова type , если вы создавали экземпляр функции, но не вызывали ее, что, как вы сказали ранее, является более распространенным случаем.

Я согласен, что это может сработать, но это кажется очень тонким. Я не думаю, что читателю будет очевидно, что происходит.

Я думаю, что в Go мы должны стремиться к большему, чем просто возможность что-то сделать. Нам нужно стремиться к подходам, которые являются простыми, интуитивно понятными и хорошо сочетаются с остальной частью языка. Человек, читающий код, должен легко понять, что происходит. Конечно, мы не всегда можем достичь этих целей, но мы должны делать все, что в наших силах.

@ianlancetaylor, помимо обсуждения синтаксиса, который интересен сам по себе, мне интересно, можем ли мы, как сообщество, сделать что-нибудь, чтобы помочь вам и команде в этом вопросе.

Например, я понимаю, что вы хотели бы, чтобы больше кода было написано в стиле предложения, чтобы лучше оценить предложение, как синтаксически, так и иначе? И/или другие вещи?

@toolbox Да. Мы работаем над инструментом, который упростит эту задачу, но он еще не готов. Очень скоро сейчас.

Можно еще что-нибудь об инструменте? Разрешит ли это выполнение кода?

Является ли эта проблема предпочтительным местом для отзывов о дженериках? Он кажется более активным, чем вики. Есть одно наблюдение: в предложении много аспектов, но проблема с GitHub сворачивает обсуждение в линейный формат.

Синтаксис F(T:) / G() { F(T:)} мне кажется вполне приемлемым. Я не думаю, что создание экземпляра, похожего на вызов функции, будет интуитивно понятным для неопытных читателей.

Я не совсем понимаю, в чем проблема обратной совместимости. Я думаю, что в проекте есть ограничение на объявление контракта, кроме как на верхнем уровне. Возможно, стоит взвесить (и измерить), сколько кода действительно сломается, если это будет разрешено. Насколько я понимаю, это только код, в котором используется ключевое слово contract , что кажется небольшим количеством кода (который можно было бы поддерживать в любом случае, указав go1 в начале старых файлов). Сравните это с десятилетиями большей мощности для программистов. В целом кажется довольно простым защитить старый код с помощью таких механизмов, особенно при широком использовании знаменитых инструментов Go.

Далее, что касается этого ограничения, я подозреваю, что запрет на объявление методов в телах функций является причиной того, что интерфейсы не используются больше - они намного более громоздки, чем передача отдельных функций. Трудно сказать, будет ли ограничение верхнего уровня контрактов столь же раздражающим, как и ограничение методов — скорее всего, нет — но, пожалуйста, не используйте ограничение методов в качестве прецедента. Для меня это недостаток языка.

Я также хотел бы увидеть примеры того, как контракты могут помочь сократить многословие на if err != nil и, что более важно, где их будет недостаточно. Возможно ли что-то вроде F() (X, error) {return IfError(foo(), func(i, j int) X { return X(i*j}), Identity )} ?

Мне также интересно, ожидает ли команда go, что сигнатуры неявных функций будут ощущаться как отсутствующая функция, как только станут доступны Map, Filter и друзья. Нужно ли это учитывать при добавлении в язык новых функций неявной типизации для контрактов? Или можно добавить позже? Или это никогда не будет частью языка?

Жду возможности опробовать предложение. Извините за столько тем.

Лично я довольно скептически отношусь к тому, что многие люди хотели бы писать методы внутри тел функций. Сегодня очень редко можно определить типы внутри тел функций; объявление методов будет еще реже. Тем не менее, см. # 25860 (не относится к дженерикам).

Я не вижу, как дженерики помогают с обработкой ошибок (сама по себе это очень многословная тема). Я не понимаю вашего примера, извините.

Более короткий синтаксис функционального литерала, также не связанный с дженериками, — #21498.

Когда я писал прошлой ночью, я не понимал, что можно играть с черновиком.
выполнение (!!). Вау, здорово наконец-то писать более абстрактный код. У меня нет проблем с синтаксисом черновика.

Продолжая дискуссию выше...


Одна из причин, по которой люди не пишут типы в телах функций, заключается в том, что они
не могу написать методы для них. Это ограничение может улавливать тип внутри
блок, в котором он был определен, так как его нельзя сжато преобразовать в
интерфейс для использования в другом месте. Java позволяет анонимным классам удовлетворять свою версию
интерфейсов, и они используются изрядно.

Мы можем обсудить интерфейс в #25860. Я бы сказал, что в эпоху
контрактов, методы станут более важными, поэтому я предлагаю ошибиться в
сторона расширения прав и возможностей местных типов и людей, которые любят писать замыкания, а не
ослабляя их.

(И повторяю, пожалуйста, не используйте строгую совместимость с go1 [против виртуальной
совместимость 99,999%, насколько я понимаю] как фактор при принятии решения об этом
особенность.)


Что касается обработки ошибок, я подозревал, что дженерики могут позволить абстрагироваться.
общие шаблоны для работы с возвращаемыми кортежами (T1, T2, ..., error) . Я не
иметь в виду что-нибудь подробное. Что-то вроде type ErrPair(type T) struct{T T; Err Error} может быть полезно для объединения действий, например, Promise в
Ява/типскрипт. Возможно, кто-то продумывал это подробнее. Попытка
написание вспомогательной библиотеки и кода, который использует библиотеку, возможно, стоит посмотреть
at, если вы ищете реальное использование.

Немного поэкспериментировав, я пришел к следующему. Я хотел бы попробовать это
метод на большом примере, чтобы увидеть, действительно ли помогает использование ErrPair(T) .

type result struct {min, max point}

// with a generic ErrPair type and generic function errMap2 (like Java's Optional#map() function).
func minMax2(msg *inputTimeSeries) (result, error) {
    return errMap2(
        MakeErrPair(time.Parse(layout, msg.start)).withMessage("bad start"),
        MakeErrPair(time.Parse(layout, msg.end)).withMessage("bad end"),
        func(start, end time.Time) (result, error) {
            min, max := argminmax(msg.inputPoints, func(p inputPoint) float64 {
                return float64(p.value)
            })
            mkPoint := func(ip inputPoint) point {
                return point{interpTime(start, end, ip.interp).Format(layout), ip.value}
            }
            return result{mkPoint(*min), mkPoint(*max)}, nil
        }).tuple()
}

// without generics, lots of if err != nil 
func minMax(msg *inputTimeSeries) (result, error) { 
    start, err := time.Parse(layout, msg.start)
    if err != nil {
        return result{}, fmt.Errorf("bad start: %w", err)
    }
    end, err := time.Parse(layout, msg.end)
    if err != nil {
        return result{}, fmt.Errorf("bad end: %w", err)
    }
    min, max := argminmax(msg.inputPoints, func(p inputPoint) float64 {
        return float64(p.value)
    })
    mkPoint := func(ip inputPoint) point {
        return point{interpTime(start, end, ip.interp).Format(layout), ip.value}
    }
    return result{mkPoint(*min), mkPoint(*max)}, nil
}

// Most languages look more like this.
func minMaxWithThrowing(msg *inputTimeSeries) result {
    start := time.Parse(layout, msg.start)) // might throw
    end := time.Parse(layout, msg.end)) // might throw
    min, max := argminmax(msg.inputPoints, func(p inputPoint) float64 {
        return float64(p.value)
    })
    mkPoint := func(ip inputPoint) point {
        return point{interpTime(start, end, ip.interp).Format(layout), ip.value}
    }
    return result{mkPoint(*min), mkPoint(*max)}
}

(полный пример кода доступен здесь )


Для общих экспериментов я попытался написать пакет S-Expression.
здесь .
Я испытал некоторую панику в экспериментальной реализации, пытаясь
работать с составными типами, такими как Form([]*Form(T)) . Я могу предоставить больше отзывов
после работы над этим, если это будет полезно.

Я также не совсем понял, как написать примитивную функцию type -> string:

contract PrimitiveType(T) {
    T bool, int, int8, int16, int32, int64, string, uint, uint8, uint16, uint32, uint64, float32, float64, complex64, complex128
    // string(T) is not a contract
}

func primitiveString(type T PrimitiveType(T))(t T) string  {
    // I'm not sure if this is an artifact of the experimental implementation or not.
    return string(t) // error: `cannot convert t (variable of type T) to string`
}

Фактическая функция, которую я пытался написать, была следующей:

// basicFormAdapter implements FormAdapter() for the primitive types.
type basicFormAdapter(type T PrimitiveType) struct{}


func (a *basicFormAdapter(T)) Format(e T, fc *FormatContext) error {
    //This doesn't work: fc.Print(string(e)) -- cannot convert e (variable of type T) to string
    // This also doesn't work: cannot type switch on non-interface value e (type int)
    // switch ee := e.(type) {
    // case int: fc.Print(string(ee))
    // default: fc.Print(fmt.Sprintf("!!! unsupported type %v", e))
    // }
    // IMO, the proposal to allow switching on T is most natural:
    // switch T.(type) {
    //  case int: fc.Print(string(e))
    //  default: fc.Print(fmt.Sprintf("!!! unsupported type %v", e))
    // }

    // This can't be the only way, right?
    rv := reflect.ValueOf(e)
    switch rv.Kind() {
    case reflect.Bool: fc.Print(fmt.Sprintf("%v", e))
    case reflect.Int:fc.Print(fmt.Sprintf("%v", e))
    case reflect.Int8: fc.Print(fmt.Sprintf("int8:%v", e))
    case reflect.Int16: fc.Print(fmt.Sprintf("int16:%v", e))
    case reflect.Int32: fc.Print(fmt.Sprintf("int32:%v", e))
    case reflect.Int64: fc.Print(fmt.Sprintf("int64:%v", e))
    case reflect.Uint: fc.Print(fmt.Sprintf("uint:%v", e))
    case reflect.Uint8: fc.Print(fmt.Sprintf("uint8:%v", e))
    case reflect.Uint16: fc.Print(fmt.Sprintf("uint16:%v", e))
    case reflect.Uint32: fc.Print(fmt.Sprintf("uint32:%v", e))
    case reflect.Uint64: fc.Print(fmt.Sprintf("uint64:%v", e))
    case reflect.Uintptr: fc.Print(fmt.Sprintf("uintptr:%v", e))
    case reflect.Float32: fc.Print(fmt.Sprintf("float32:%v", e))
    case reflect.Float64: fc.Print(fmt.Sprintf("float64:%v", e))
    case reflect.Complex64: fc.Print(fmt.Sprintf("(complex64 %f %f)", real(rv.Complex()), imag(rv.Complex())))
    case reflect.Complex128:
         fc.Print(fmt.Sprintf("(complex128 %f %f)", real(rv.Complex()), imag(rv.Complex())))
    case reflect.String:
        fc.Print(fmt.Sprintf("%q", rv.String()))
    }
    return nil
}

Я также попытался создать тип типа «Результат».

type Result(type T) struct {
    Value T
    Err error
}

func NewResult(type T)(value T, err error) Result(T) {
    return Result(T){
        Value: value,
        Err: err,
    }
}

func then(type T, R)(r Result(T), f func(T) R) Result(R) {
    if r.Err != nil {
        return Result(R){Err: r.Err}
    }

    v := f(r.Value)
    return  Result(R){
        Value: v,
        Err: nil,
    }
}

func thenTry(type T, R)(r Result(T), f func(T)(R, error)) Result(R) {
    if r.Err != nil {
        return Result(R){Err: r.Err}
    }

    v, err := f(r.Value)
    return  Result(R){
        Value: v,
        Err: err,
    }
}

например

    r := NewResult(GetInput())
    r2 := thenTry(r, UppercaseAndErr)
    r3 := thenTry(r2, strconv.Atoi)
    r4 := then(r3, Add5)
    if r4.Err != nil {
        // handle err
    }
    return r4.Value, nil

В идеале функции then должны быть методами типа Result.

Кроме того, пример абсолютной разницы в черновике, похоже, не компилируется.
Я думаю следующее:

func (a ComplexAbs(T)) Abs() T {
    r := float64(real(a))
    i := float64(imag(a))
    d := math.Sqrt(r * r + i * i)
    return T(complex(d, 0))
}

должно быть:

func (a ComplexAbs(T)) Abs() ComplexAbs(T) {
    r := float64(real(a))
    i := float64(imag(a))
    d := math.Sqrt(r * r + i * i)
    return ComplexAbs(T)(complex(d, 0))
}

Меня немного беспокоит возможность использовать несколько contract для привязки одного параметра типа.

В Scala принято определять такую ​​функцию, как:

def compute[A: PointLike: HasTime: IsWGS](points: Vector[A]): Map[Int, A] = ???

PointLike , HasTime и IsWGS — это небольшие contract (Scala называет их type class ).

В Rust тоже есть похожий механизм:

fn f<F: A + B>(a F) {}

И мы можем использовать анонимный интерфейс при определении функции.

type I1 interface {
    A()
}
type I2 interface {
    B()
}
func f(a interface{
    I1
    I2
})

ИМО, анонимный интерфейс — это плохая практика, поскольку interface — это реальный тип , вызывающему объекту этой функции может потребоваться объявить переменную с этим типом. Но contract просто ограничение на параметр типа, вызывающий всегда играет с каким-то реальным типом или просто с другим параметром типа, я думаю, что безопасно разрешать анонимный контракт в определении функции.

Для разработчиков библиотек неудобно определять новый contract , если комбинация некоторых контрактов используется только в нескольких местах, это испортит кодовую базу. Пользователям библиотек необходимо копаться в определениях, чтобы знать реальные требования к ним. Если пользователь определяет много функций для вызова функции в библиотеке, они могут определить именованный контракт для простоты использования, и они могут даже добавить больше контракта к этому новому контракту, если им нужно, потому что это допустимо.

contract C1(T) {
    T A()
}
contract C2(T) {
    T B()
}
contract C3(T) {
    T C()
}

contract PART(T) {
    C1(T)
    C2(T)
}

contract ALL(T) {
    C1(T)
    C2(T)
    C3(T)
}

func f1(type A PART) (a A) {}

func f2(type A ALL) (a A) {
    f1(a)
}

Я пробовал их в черновом компиляторе, все они не могут быть проверены.

func f(type A C1, C2)(x A)

func f1(type A contract C(A1) {
    C1(A)
    C2(A)
}) (x A)

func f2(type A ((type A1) interface {
    I1(A1)
    I2(A1)
})(A)) (x A)

Согласно примечаниям в CL

Параметр типа, ограниченный несколькими контрактами, не будет привязан к правильному типу.

Я думаю, что этот странный фрагмент действителен после решения этой проблемы.

func f1(type A C1, _ C2(A)) (x A)

Вот некоторые из моих мыслей:

  • Если мы рассматриваем contract как тип параметра типа, type a A <=> var a A , мы можем добавить синтаксический сахар, такой как type a { A1(a); A2(a) } , чтобы определить анонимный контракт быстро.
  • В противном случае мы можем рассматривать последнюю часть списка типов как список требований, type a, b, A1(a), A2(a), A3(a, b) , этот стиль аналогичен использованию interface для ограничения параметров типа.

@bobotu В Go принято создавать функциональные возможности с помощью встраивания. Кажется естественным составлять контракты так же, как вы делаете это со структурами или интерфейсами.

@azunymous Лично я не знаю, как я отношусь ко всему сообществу Go, переходящему с множественных возвратов на Result , хотя кажется, что предложение Contracts позволит это в некоторой степени. Команда Go, кажется, избегает языковых изменений, которые ставят под угрозу «ощущение» языка, с чем я согласен, но это похоже на одно из таких изменений.

Просто мысль; Интересно, есть ли какие-нибудь решения по этому вопросу.

@toolbox Я не думаю, что на самом деле возможно использовать что-то вроде одного типа Result за пределами случая, когда вы просто передаете значения, если только у вас нет массы общих Result s и функции каждой комбинации счетчиков параметров и возвращаемых типов. Либо с большим количеством пронумерованных функций, либо с использованием замыканий вы потеряете читабельность.

Я думаю, что более вероятно, что вы увидите что-то эквивалентное errWriter , где бы вы использовали что-то подобное время от времени, когда оно подходит, названное в соответствии с вариантом использования.

Лично я не знаю, как я отношусь к тому, что все сообщество Go переходит от многократного возврата к результату.

Я не думаю, что это произойдет. Как сказал @azunymous , многие функции имеют несколько типов возврата и ошибку, но результат не может содержать все эти другие возвращаемые значения одновременно. Параметрический полиморфизм — не единственная функция, необходимая для выполнения чего-то подобного; вам также понадобятся кортежи и деструктуризация.

Спасибо! Как я уже сказал, не то, чтобы я глубоко задумался, но приятно знать, что мое беспокойство было неуместным.

@toolbox Я не собираюсь вводить какой-то новый синтаксис, ключевой проблемой здесь является отсутствие возможности использовать анонимный контракт, как анонимный интерфейс.

В черновом компиляторе написать что-то подобное кажется невозможным. Мы можем использовать анонимный интерфейс в определении функции, но мы не можем сделать то же самое для контракта даже в подробном стиле.

func f1(type A, B, C, D contract {
    C1(A)
    C2(A, B)
    C3(A, C)
}) (a A, b B, c C, d D)

// Or a more verbose style

func f2(type A, B, C, D (contract (_A, _B, _C) {
    C1(_A)
    C2(_A, _B)
    C3(_A, _C)
})(A, B, C)) (a A, b B, c C, d D)

ИМО, это естественное расширение существующего синтаксиса. Это по-прежнему контракт в конце списка параметров типа, и мы по-прежнему используем встраивание для создания функциональности. Если Go может предоставить немного сахара для автоматической генерации параметров типа контракта, как в первом фрагменте, код будет легче читать и писать.

func fff(type A C1(A), B C2(B, A), C C3(B, C, A)) (a A, b B, c C)

// is more verbose than

func fff(type A, B, C contract {
    C1(A)
    C2(B, A)
    C3(B, C, A)
}) (a A, b B, c C)

Я сталкиваюсь с некоторыми проблемами, когда пытаюсь реализовать ленивый итератор без вызова динамического метода, как итератор в Rust.

Я хочу определить простой Iterator

contract Iterator(T, E) {
    T Next() (E, bool)
}

Поскольку в Go нет понятия type member , мне нужно объявить E в качестве параметра типа ввода.

Функция для сбора результатов

func Collect(type I, E Iterator) (input I) []E {
    var results []E
    for {
        e, ok := input.Next()
        if !ok {
            return results
        }
        results = append(results, e)
    }
}

Функция для сопоставления элементов

contract MapIO(I, E, O, R) {
    Iterator(I, E)
    Iterator(O, R)
}

func Map(type I, E, O, R MapIO) (input I, f func (e E) R) O {
    return &lazyIterator(I, E, R){
        parent: input,
        f:      f,
    }
}

У меня тут две проблемы:

  1. Я не могу вернуть lazyIterator здесь, компилятор говорит cannot convert &(lazyIterator(I, E, R) literal) (value of type *lazyIterator(I, E, R)) to O .
  2. Мне нужно объявить новый контракт с именем MapIO , для которого нужно 4 строки, а для Map нужно всего 6 строк. Пользователям сложно читать код.

Предположим, Map можно проверить на тип, надеюсь, я смогу написать что-то вроде

type staticIterator(type E) struct {
    elem []E
}

func (it *(staticIterator(E))) Next() (E, bool) { panic("todo") }

func main() {
    inpuit := &staticIterator{
        elem: []int{1, 2, 3, 4},
    }
    mapped := Map(input, func (i int) float32 { return float32(i + 1) })
    fmt.Printf("%v\n", Collect(mapped))
}

К сожалению, компилятор жалуется, что не может вывести типы. Он перестает жаловаться на это после того, как я изменяю код на

func main() {
    input := &staticIterator(int){
        elem: []int{1, 2, 3, 4},
    }
    mapped := Map(*staticIterator(int), int, *lazyIterator(*staticIterator(int), int, float32), float32)(input, func (i int) float32 { return float32(i + 1) })
    result := Collect(*lazyIterator(*staticIterator(int), int, float32), float32)(mapped)
    fmt.Printf("%v\n", result)
}

Код очень сложно читать и писать, и слишком много повторяющихся подсказок типа.

Кстати, компилятор будет паниковать:

panic: interface conversion: ast.Expr is *ast.ParenExpr, not *ast.CallExpr

goroutine 1 [running]:
go/go2go.(*translator).instantiateTypeDecl(0xc000251950, 0x0, 0xc0001af860, 0xc0001a5dd0, 0xc00018ac90, 0x1, 0x1, 0xc00018bca0, 0x1, 0x1, ...)
        /home/tuzi/go-tip/src/go/go2go/instantiate.go:191 +0xd49
go/go2go.(*translator).translateTypeInstantiation(0xc000251950, 0xc000189380)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:671 +0x3f3
go/go2go.(*translator).translateExpr(0xc000251950, 0xc000189380)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:518 +0x501
go/go2go.(*translator).translateExpr(0xc000251950, 0xc0001af990)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:496 +0xe3
go/go2go.(*translator).translateExpr(0xc000251950, 0xc00018ace0)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:524 +0x1c3
go/go2go.(*translator).translateExprList(0xc000251950, 0xc00018ace0, 0x1, 0x1)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:593 +0x45
go/go2go.(*translator).translateStmt(0xc000251950, 0xc000189840)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:419 +0x26a
go/go2go.(*translator).translateBlockStmt(0xc000251950, 0xc00018d830)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:380 +0x52
go/go2go.(*translator).translateFuncDecl(0xc000251950, 0xc0001c0390)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:373 +0xbc
go/go2go.(*translator).translate(0xc000251950, 0xc0001b0400)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:301 +0x35c
go/go2go.rewriteAST(0xc000188280, 0xc000188240, 0x0, 0x0, 0xc0001f6280, 0xc0001b0400, 0x1, 0xc000195360, 0xc0001f6280)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:122 +0x101
go/go2go.RewriteBuffer(0xc000188240, 0x7ffe07d6c027, 0xa, 0xc0001ec000, 0x4fe, 0x6fe, 0x0, 0xc00011ed58, 0x40d288, 0x30, ...)
        /home/tuzi/go-tip/src/go/go2go/go2go.go:132 +0x2c6
main.translateFile(0xc000188240, 0x7ffe07d6c027, 0xa)
        /home/tuzi/go-tip/src/cmd/go2go/translate.go:26 +0xa9
main.main()
        /home/tuzi/go-tip/src/cmd/go2go/main.go:64 +0x434

Я также считаю, что невозможно определить функцию, которая работает с возвратом Iterator определенного типа.

type User struct {}

func UpdateUsers(type A Iterator(A, User)) (it A) bool { 
    // Access `User`'s field.
}

// And I found this may be possible

contract checkInts(A, B) {
    Iterator(A, B)
    B int
}

func CheckInts(type A, B checkInts) (it A) bool { panic("todo") }

Второй фрагмент может работать в некоторых сценариях, но его трудно понять, а неиспользуемый тип B кажется странным.

Действительно, мы можем использовать интерфейс для выполнения этой задачи.

type Iterator(type E) interface {
    Next() (E, bool)
}

Я просто пытаюсь понять, насколько выразителен дизайн Go.

Кстати, код Rust, на который я ссылаюсь,

fn main() {
    let input = vec![1, 2, 3, 4];
    let mapped = input.iter().map(|x| x * 3);
    let result = f(mapped);
    println!("{:?}", result.collect::<Vec<_>>());
}

fn f<I: Iterator<Item = i32>>(it: I) -> impl Iterator<Item = f32> {
    it.map(|i| i as f32 * 2.0)
}

// The definition of `map` in stdlib is
pub struct Map<I, F> {
    iter: I,
    f: F,
}

fn map<B, F: FnMut(Self::Item) -> B>(self, f: F) -> Map<Self, F>

Вот сводка для https://github.com/golang/go/issues/15292#issuecomment -633233479.

  1. Нам может понадобиться что-то, чтобы выразить existential type за func Collect(type I, E Iterator) (input I) []E

    • Фактический тип универсального квантифицированного параметра E не может быть выведен, поскольку он появился только в возвращаемом списке. Из-за отсутствия type member , чтобы сделать E экзистенциальным по умолчанию, я думаю, что мы можем столкнуться с этой проблемой во многих местах.

    • Может быть, мы можем использовать простейший existential type , такой как подстановочный знак Java ? , для разрешения вывода типа func Consume(type I, E Iterator) (input I) . Мы можем использовать _ вместо E , func Consume(type I Iterator(I, _)) (input I) .

    • Но это все еще не может помочь в проблеме вывода типа для Collect , я не знаю, сложно ли вывести E , но Rust, похоже, может это сделать.

    • Или мы можем использовать _ в качестве заполнителя для типов, которые может вывести компилятор, и заполнить отсутствующие типы вручную, например Collect(_, float32) (...) , чтобы выполнить сбор на итераторе float32.

  1. Из-за отсутствия возможности вернуть existential type у нас также есть проблемы с такими вещами, как func Map(type I, E, O, R MapIO) (input I, f func (e E) R) O

    • Rust поддерживает это, используя impl Iterator<E> . Если Go может предоставить что-то подобное, мы можем вернуть новый итератор без упаковки, это может быть полезно для некоторого критичного к производительности кода.

    • Или мы можем просто вернуть упакованный объект, так Rust решает эту проблему до того, как он поддерживает existential type в позиции возврата. Но вопрос в отношении между contract и interface , возможно, нам нужно определить некоторые правила преобразования и позволить компилятору преобразовать их автоматически. В противном случае нам может понадобиться определить contract и interface с идентичными методами для этого случая.

    • В противном случае мы можем использовать CPS только для перемещения параметра типа из позиции возврата в список ввода. например, func Map(type I, E, O, R MapIO) (input I, f func (e E) R, f1 func (outout O)) . Но на практике это бесполезно, просто потому, что мы должны писать фактический тип O , когда мы передаем функцию в Map .

Я только что немного подхватил эту дискуссию, и мне кажется довольно ясным, что синтаксические трудности с параметрами типов остаются серьезной трудностью в проекте предложения. Существует способ полностью избежать параметров типа и реализовать большую часть функциональности дженериков: #32863 -- может быть, сейчас самое время рассмотреть эту альтернативу в свете дальнейшего обсуждения? Если бы был какой-то шанс, что что-то подобное этому дизайну будет принято, я был бы рад попытаться изменить игровую площадку веб-сборки, чтобы ее можно было протестировать.

Я чувствую, что в настоящее время основное внимание уделяется правильности семантики текущего предложения, независимо от синтаксиса, потому что семантику очень трудно изменить.

Я только что увидел статью о Featherweight Go , опубликованную на Arxiv, которая является результатом сотрудничества команды Go и нескольких экспертов по теории типов. Похоже, есть еще запланированные бумаги в этом ключе.

В дополнение к моему предыдущему комментарию, Фил Уодлер из Haskell и один из авторов статьи запланировал выступление на «Полулегком весе» в понедельник, 8 июня, в 7:00 по тихоокеанскому времени / 10:00 по восточному поясному времени: http://chalmersfp.org/ . ссылка на ютуб

@rcoreilly Я думаю, что мы узнаем, являются ли «синтаксические трудности» серьезной проблемой, только тогда, когда у людей будет больше опыта написания и, что более важно, чтения кода, написанного в соответствии с черновиком дизайна. Мы работаем над тем, чтобы люди могли это попробовать.

В отсутствие этого я думаю, что синтаксис — это просто то, что люди видят первым и комментируют в первую очередь. Это может быть серьезной проблемой, а может и нет. Мы еще не знаем.

В дополнение к моему предыдущему комментарию, Фил Уодлер из Haskell и один из авторов газеты запланировал выступление на "Featherweight Go" в понедельник.

Доклад Фила Уодлера был очень доступным и интересным. Меня раздражал, казалось бы, бессмысленный часовой лимит времени, который мешал ему перейти к мономорфизации.

Примечательно, что Пайк попросил Уодлера зайти; очевидно, они знают друг друга по Bell Labs. Для меня у Haskell совсем другой набор ценностей и парадигм, и интересно посмотреть, как его (создатель? главный дизайнер?) думает о Go и дженериках в Go.

Само предложение имеет синтаксис, очень близкий к контрактам, но опускает сами контракты, используя только параметры типа и интерфейсы. Ключевым отличием, которое вызывается, является возможность взять универсальный тип и определить для него методы, которые имеют более конкретные ограничения, чем сам тип.

Судя по всему, команда Go работает над этим или уже имеет прототип! Это будет интересно. Между тем, как это будет выглядеть?

package graph

type Node(type e) interface{
    Edges() []e
}

type Edge(type n) interface{
    Nodes() (from n, to n)
}

type Graph(type n Node(e), e Edge(n)) struct { ... }
func New(type n Node(e), e Edge(n))(nodes []n) *Graph(n, e) { ... }
func (g *Graph(type n Node(e), e Edge(n))) ShortestPath(from, to n) []e { ... }

Имею ли я это право? Я так думаю. Если я... не плохо, на самом деле. Не совсем решает проблему заикания в скобках, но кажется, что это как-то улучшилось. Какая-то безымянная суматоха во мне утихла.

Как насчет примера стека от @urandom ? (Псевдоним interface{} до Any и использование определенного вывода типа.)

package main

type Any interface{}

type Stack(type t Any) []t

func (s Stack(type t Any)) Peek() t {
    return s[len(s)-1]
}

func (s *Stack(type t Any)) Pop() {
    *s = (*s)[:len(*s)-1]
}

func (s *Stack(type t Any)) Push(value t) {
    *s = append(*s, value)
}

type StackIterator(type t Any) struct{
    stack Stack(t)
    current int
}

func (s *Stack(type t Any)) Iter() *StackIterator(t) {
    it := StackIterator(t){stack: *s, current: len(*s)}

    return &it
}

func (i *StackIterator(type t Any)) Next() (bool) { 
    i.current--

    if i.current < 0 { 
        return false
    }

    return true
}

func (i *StackIterator(type t Any)) Value() t {
    if i.current < 0 {
        var zero t
        return zero
    }

    return i.stack[i.current]
}

type Iterator(type t Any) interface {
    Next() bool
    Value() t
}

func Map(type t Any, u Any)(it Iterator(t), mapF func(t) u) Iterator(u) {
    return mapIt(t, u){it, mapF}
}

type mapIt(type t Any, u Any) struct {
    parent Iterator(t)
    mapF func(t) u
}

func (i mapIt(type t Any, u Any)) Next() bool {
    return i.parent.Next()
}

func (i mapIt(type t Any, u Any)) Value() u {
    return i.mapF(i.parent.Value())
}

func Filter(type t Any)(it Iterator(t), predicate func(t) bool) Iterator(t) {
    return filter(t){it, predicate}
}

type filter(type t Any) struct {
    parent Iterator(t)
    predicateF func(t) bool
}

func (i filter(type t Any)) Next() bool {
    if !i.parent.Next() {
        return false
    }

    n := true
    for n && !i.predicateF(i.parent.Value()) {
        n = i.parent.Next()
    }

    return n
}

func (i filter(type t Any)) Value() t {
    return i.parent.Value()
}

func Distinct(type t comparable)(it Iterator(t)) Iterator(t) {
    return distinct(t){it, map[t]struct{}{}}
}

type distinct(type t comparable) struct {
    parent Iterator(t)
    set map[t]struct{}
}

func (i distinct(type t Any)) Next() bool {
    if !i.parent.Next() {
        return false
    }

    n := true
    for n {
        _, ok := i.set[i.parent.Value()]
        if !ok {
            i.set[i.parent.Value()] = struct{}{}
            break
        }
        n = i.parent.Next()
    }


    return n
}

func (i distinct(type t Any)) Value() t {
    return i.parent.Value()
}

func ToSlice(type t Any)(it Iterator(t)) []t {
    var res []t

    for it.Next() {
        res = append(res, it.Value())
    }

    return res
}

func ToSet(type t comparable)(it Iterator(t)) map[t]struct{} {
    var res map[t]struct{}

    for it.Next() {
        res[it.Value()] = struct{}{}
    }

    return res
}

func Reduce(type t Any)(it Iterator(t), id t, acc func(a, b t) t) t {
    for it.Next() {
        id = acc(id, it.Value())
    }

    return id
}

func main() {
    var stack Stack(string)
    stack.Push("foo")
    stack.Push("bar")
    stack.Pop()
    stack.Push("alpha")
    stack.Push("beta")
    stack.Push("foo")
    stack.Push("gamma")
    stack.Push("beta")
    stack.Push("delta")


    var it Iterator(string) = stack.Iter()

    it = Filter(string)(it, func(s string) bool {
        return s == "foo" || s == "beta" || s == "delta"
    })

    it = Map(string, string)(it, func(s string) string {
        return s + ":1"
    })

    it = Distinct(string)(it)

    println(Reduce(it, "", func(a, b string) string {
        if a == "" {
            return b
        }
        return a + ":" + b
    }))


}

Что-то в этом роде, я полагаю. Я понимаю, что на самом деле в этом коде нет контрактов, поэтому это не очень хорошее представление того, как это обрабатывается в стиле FGG, но я могу решить это через мгновение.

Впечатления:

  • Мне нравится, когда стиль параметров типа в методах соответствует стилю объявлений типа. Т.е. говоря "тип" и явно указывая типы, ("type" param paramType, param paramType...) вместо (param, param) . Это делает его визуально согласованным, поэтому код становится более читаемым.
  • Мне нравится, когда параметры типа пишутся в нижнем регистре. Однобуквенные переменные в Go указывают на чрезвычайно локальное использование, но заглавные буквы означают, что они экспортируются, и они кажутся противоречивыми, когда вместе взятые. Нижний регистр выглядит лучше, поскольку параметры типа привязаны к функции/типу.

Ладно, а контракты?

Что ж, мне нравится, что Stringer нетронуты; у вас не будет интерфейса $#$3 Stringer #$ и контракта Stringer .

type Stringer interface {
    String() string
}

func Stringify(type t Stringer)(s []t) (ret []string) {
    for _, v := range s {
        ret = append(ret, v.String())
    }
    return ret
}

У нас также есть пример viaStrings :

type ToString interface {
    Set(string)
}

type FromString interface {
    String() string
}

func SetViaStrings(type to ToString, from FromString)(s []from) []to {
    r := make([]to, len(s))
    for i, v := range s {
        r[i].Set(v.String())
    }
    return r
}

Интересный. На самом деле я не уверен на 100%, что контракт дал нам в этом случае. Возможно, частью этого было правило, согласно которому функция может иметь несколько параметров типа, но только один контракт.

Равно освещается в статье/разговоре:

contract equal(T) {
    T Equal(T) bool
}

// becomes

type equal(type t equal(t)) interface{
    Equal(t) bool
}

И так далее. Я довольно занят семантикой. Параметры типа — это интерфейсы, поэтому те же правила реализации интерфейса применяются к тому, что можно использовать в качестве параметра типа. Это просто не "упаковано" во время выполнения - если вы явно не передадите ему интерфейс, я полагаю, что вы свободны.

Самая важная вещь, которую я отмечаю как не охваченную, — это замена способности контрактов указывать ряд примитивных типов. Что ж, я уверен, что стратегия для этого и многих других вещей появится :

8 - ЗАКЛЮЧЕНИЕ

Это начало истории, а не конец. В дальнейшей работе мы планируем рассмотреть другие методы реализации, помимо мономорфизации, и, в частности, рассмотреть реализацию, основанную на передаче представлений типов во время выполнения, аналогичную той, которая используется для дженериков .NET. Смешанный подход, который иногда использует мономорфизацию и передачу представлений во время выполнения, может быть лучшим, опять же похожим на тот, который используется для дженериков .NET.

Полулегкий Go ограничен крошечным подмножеством Go. Мы планируем модель других важных функций, таких как присваивания, массивы, слайсы и пакеты, которые мы назовем Легчайший вес Go; и модель инновационного механизма параллелизма Go, основанного на «горутинах» и передаче сообщений, которую мы назовем Cruiserweight Go.

Полулегкий го выглядит великолепно для меня. Отличная идея привлечь специалистов по теории типов. Это больше похоже на то, что я отстаивал выше в этой теме.

Приятно слышать, что специалисты по теории типов активно работают над этим!

Это даже похоже (за исключением немного другого синтаксиса) на мое старое предложение «контракты — это интерфейсы» https://github.com/cosmos72/gomacro/blob/master/doc/generics-cti.md

@тулбокс
Разрешая методы с ограничениями, отличными от фактического типа (а также с другими типами в целом), FGG открывает довольно много возможностей, которые были невозможны с текущим проектом контрактов. Например, с FGG можно определить как Iterator, так и ReversibleIterator, а промежуточный и конечный итераторы (map, filter reduce) должны поддерживать оба (например, с Next() и NextFromBack() для обратимых) , в зависимости от родительского итератора.

Я думаю, важно иметь в виду, что FGG не является окончательным местом, где закончатся дженерики в Go. Это один взгляд на них, со стороны. И он явно игнорирует множество вещей, которые в конечном итоге усложняют конечный продукт. Кроме того, я не читал газету, только смотрел выступление. Имея это в виду: насколько я могу судить, есть два важных способа, которыми FGG добавляет выразительности в проект контракта:

  1. Он позволяет добавлять в методы новые параметры-типы (как показано в примере «Список и карты» в докладе). AFAICT это позволило бы реализовать Functor (на самом деле, это его пример List, если я не ошибаюсь), Monad и их друзей. Я не думаю, что эти конкретные типы интересны для Gopher, но для этого есть интересные варианты использования (например, Go-порт Flume или аналогичные концепции, вероятно, выиграют). Лично я чувствую, что это положительное изменение, хотя я еще не понимаю, каковы последствия для размышлений и тому подобного. Я действительно чувствую, что объявления методов, использующие это, начинают становиться трудными для чтения, особенно если параметры типа универсального типа также должны быть перечислены в получателе.
  2. Это позволяет параметрам типа иметь более строгие ограничения для методов универсальных типов, чем для самого типа. Как упоминалось другими, это позволяет вам иметь один и тот же общий тип, реализующий разные методы, в зависимости от того, с какими типами он был создан. Лично я не уверен, что это хорошее изменение. Кажется, рецепт для путаницы, чтобы Map(int, T) закончились методами, которых нет у Map(string, T) . По крайней мере, компилятор должен предоставлять отличные сообщения об ошибках, если что-то подобное происходит. Между тем, польза кажется сравнительно небольшой, особенно с учетом того, что мотивирующий фактор из разговора (отдельная компиляция) не очень актуален для Go: поскольку методы должны быть объявлены в том же пакете, что и их тип получателя, и учитывая, что пакеты являются единицей компиляции, вы не можете расширить тип отдельно. Я знаю, что разговор о компиляции — это скорее конкретный способ говорить о более абстрактном преимуществе, но все же я не думаю, что это преимущество сильно помогает Go.

Я с нетерпением жду следующих шагов, в любом случае :)

Я думаю, важно иметь в виду, что FGG не является окончательным местом, где закончатся дженерики в Go.

@Merovius , почему ты так говоришь?

@арл
FG — это скорее исследовательская работа о том, что _можно_ сделать. Никто прямо не сказал, что именно так полиморфизм будет работать в Go в будущем. Несмотря на то, что 2 основных разработчика Go указаны в документе как авторы, это не означает, что это будет реализовано в Go.

Я думаю, важно иметь в виду, что FGG не является окончательным местом, где закончатся дженерики в Go. Это один взгляд на них, со стороны. И он явно игнорирует множество вещей, которые в конечном итоге усложняют конечный продукт.

Да, очень хороший момент.

Также отмечу, что Вадлер работает как часть команды, и получившийся продукт основан на предложении Contracts и очень близок к нему, что является результатом многолетней работы основных разработчиков.

Разрешая методы с ограничениями, отличными от фактического типа (а также с другими типами в целом), FGG открывает довольно много возможностей, которые были невозможны с текущим проектом контрактов. ...

@urandom Мне любопытно, как выглядит этот пример Iterator; не могли бы вы собрать что-нибудь вместе?

Отдельно меня интересует, что могут делать дженерики помимо карт, фильтров и функциональных вещей, и еще более любопытно, какую пользу они могут принести такому проекту, как k8s. (Не то, чтобы на этом этапе они пошли бы на рефакторинг, но я случайно слышал, что отсутствие дженериков потребовало некоторой причудливой работы, я думаю, с Custom Resources? Кто-то, более знакомый с проектом, может меня поправить.)

Я действительно чувствую, что объявления методов, использующие это, начинают становиться трудными для чтения, особенно если параметры типа универсального типа также должны быть перечислены в получателе.

Возможно, gofmt может как-то помочь? Может быть, нам нужно перейти на несколько линий. Возможно, стоит поиграться.

Как упоминалось другими, это позволяет вам иметь один и тот же общий тип, реализующий разные методы, в зависимости от того, с какими типами он был создан.

Я понимаю, что ты говоришь @Merovius

Уодлер назвал это отличием, и это позволяет ему решить его проблему с выражением, но вы хорошо заметили, что своего рода герметичные пакеты Go, похоже, ограничивают то, что вы можете / должны делать с этим. Можете ли вы придумать какой-либо реальный случай, когда вы хотели бы это сделать?

Как упоминалось другими, это позволяет вам иметь один и тот же общий тип, реализующий разные методы, в зависимости от того, с какими типами он был создан.

Я понимаю, что ты говоришь @Merovius

Уодлер назвал это отличием, и это позволяет ему решить его проблему с выражением, но вы хорошо заметили, что своего рода герметичные пакеты Go, похоже, ограничивают то, что вы можете / должны делать с этим. Можете ли вы придумать какой-либо реальный случай, когда вы хотели бы это сделать?

По иронии судьбы, моей первой мыслью было, что его можно использовать для решения некоторых проблем, описанных в этой статье: https://blog.merovius.de/2017/07/30/the-trouble-with-Optional-interfaces.html .

@ящик для инструментов

Отдельно меня интересует, что могут делать дженерики помимо карт и фильтров и функциональных вещей,

FWIW, следует уточнить, что это своего рода продажа "карт и фильтров и функциональных вещей" короче. Например, я лично не хочу использовать map и filter вместо встроенных структур данных в моем коде (я предпочитаю циклы for). Но это также может означать

  1. Предоставление общего доступа к любой сторонней структуре данных. т. е map и filter можно заставить работать с деревьями обобщений, или с отсортированными картами, или… также. Таким образом, вы можете поменять местами то, что отображено, для большей мощности. И что более важно
  2. Вы можете изменить способ отображения. Например, вы можете создать версию Compose , которая может создавать несколько горутин для каждой функции и запускать их одновременно, используя каналы. Это упростит запуск параллельных конвейеров обработки данных и автоматическое масштабирование узкого места, при этом потребуется только написать func(A) B s. Или вы можете поместить те же функции в структуру, которая запускает тысячи копий программы в кластере, планируя пакеты данных между ними (это то, на что я ссылался, когда ссылался на Flume выше).

Таким образом, хотя возможность писать Map и Filter и Reduce может показаться скучной на первый взгляд, те же методы открывают некоторые действительно захватывающие возможности для упрощения масштабируемых вычислений.

@КрисХайнс

По иронии судьбы, моей первой мыслью было, что его можно использовать для решения некоторых проблем, описанных в этой статье: https://blog.merovius.de/2017/07/30/the-trouble-with-Optional-interfaces.html .

Это интересная мысль, и кажется, что так и должно быть. Но пока не понимаю как. Если вы возьмете пример ResponseWriter , кажется, что это может позволить вам написать общие, типобезопасные оболочки с различными методами в зависимости от того, что поддерживает обернутая ResponseWriter . Но даже если вы можете использовать разные границы для разных методов, вам все равно придется их записывать. Таким образом, хотя это может сделать ситуацию безопасной для типов в том смысле, что вы не добавляете методы, которые не поддерживаете, вам все равно нужно перечислить все методы, которые вы могли бы поддерживать, поэтому промежуточное ПО может по-прежнему маскировать некоторые необязательные интерфейсы. просто не зная о них. Между тем, вы также можете (даже без этой функции) сделать

type Middleware (type RW http.ResponseWriter) struct {
    RW
}

и перезапишите выборочные методы, которые вам нужны, и продвигайте все остальные методы RW . Так что вам даже не придется писать обертки и прозрачно получать даже те методы, о которых вы не знали.

Итак, если предположить, что у нас есть продвинутые методы для параметров типов, встроенных в универсальные структуры (а я надеюсь, что так и есть), проблемы кажутся лучше решаемыми этим методом.

Я думаю, что конкретное решение для http.ResponseWriter похоже на errors.Is/As . Не нужно менять язык, достаточно добавить библиотеку для создания стандартного метода упаковки ResponseWriter и способа запроса, может ли какой-либо из ResponseWriter в цепочке обрабатываться, например, wPush. Я скептически отношусь к тому, что дженерики подходят для чего-то подобного, потому что весь смысл в том, чтобы иметь выбор между дополнительными интерфейсами во время выполнения, например, Push доступен только в http2, а не в том случае, если я раскручиваю локальный сервер разработки http1.

Просматривая Github, я не думаю, что когда-либо создавал проблему для этой идеи, поэтому, возможно, я сделаю это сейчас.

Редактировать: #39558.

@тулбокс
Я предполагаю, что это будет выглядеть примерно так, вместе с его внутренним кодом мономорфизации:

package iter

type Any interface{}

type Iterator(type T Any) interface {
    Next() bool
    Value() T
}

type ReversibleIterator(type T Any) interface {
    Iterator(T)
    NextBack() bool
}

type mapIt(type I Iterator(T), T Any, U Any) struct {
    parent I
    mapF func(T) U
}

func (i mapIt(type I Iterator(T))) Next() bool {
    return i.parent.Next()
}

func (i mapIt(type I Iterator(T), T Any, U Any)) Value() U { 
    return i.mapF(i.parent.Value())
}

func (i mapIt(type I ReversibleIterator(T))) NextBack() bool { 
    return i.parent.NextBack()
}

// Monomorphisation
type mapIt<OnlyForward, int, float64> struct {
    parent OnlyForward,
    mapF func(int) float64
}

func (i mapIt<OnlyForward, int, float64>) Next() bool {
    return i.parent.Next()
}

func (i mapIt<OnlyForward, int, float64>) Value() float64 {
    return i.mapF(i.parent.Value())
}

type mapIt<Slice, int, string> struct {
    parent Slice,
    mapF func(int) string
}

func (i mapIt<Slice, int, string>) Next() bool {
    return i.parent.Next()
}

func (i mapIt<Slice, int, string>) Value() string {
    return i.mapF(i.parent.Value())
}

func (i mapIt<Slice, int, string>) NextBack() bool {
    return i.parent.NextBack()
}



Я предполагаю, что это будет выглядеть примерно так, вместе с его внутренним кодом мономорфизации:

FWIW, вот мой твит несколько лет назад, в котором я исследую, как итераторы могут работать в Go с дженериками. Если вы сделаете глобальную замену <T> на (type T) , вы получите что-то недалекое от текущего предложения: https://twitter.com/rogpeppe/status/425035488425037824

FWIW, следует уточнить, что это своего рода продажа "карт и фильтров и функциональных вещей" короче. Например, я лично не хочу сопоставлять и фильтровать встроенные структуры данных в своем коде (я предпочитаю циклы for). Но это также может означать...

Я понимаю вашу точку зрения и не возражаю, и да, мы выиграем от того, что охватывают ваши примеры.
Но мне все еще интересно, как это повлияет на что-то вроде k8s или другую кодовую базу с «универсальными» типами данных, где виды выполняемых действий не являются картами или фильтрами или, по крайней мере, выходят за рамки этого. Интересно, насколько эффективны контракты или FGG для повышения безопасности типов и производительности в такого рода контекстах.

Хотите знать, может ли кто-нибудь указать кодовую базу, надеюсь, более простую, чем k8s, которая подходит под эту категорию?

@urandom вау . Таким образом, если вы создаете экземпляр mapIt с parent , который реализует ReversibleIterator , тогда mapIt имеет метод NextBack() , а если нет, то он не т. Я правильно читаю?

Если подумать, кажется, что это полезно с точки зрения библиотеки. У вас есть некоторые общие типы структур, которые довольно открыты (параметры типа Any ), и у них есть много методов, ограниченных различными интерфейсами. Итак, когда вы используете библиотеку в своем собственном коде, тип, который вы встраиваете в структуру, дает вам возможность вызывать определенный набор методов, поэтому вы получаете определенный набор функций библиотеки. Что представляет собой этот набор функций, выясняется во время компиляции на основе методов, которые есть у вашего типа.

... Это немного похоже на то, что @ChrisHines поднял в том, что вы вроде как можете написать код, который имеет более или меньшую функциональность в зависимости от того, что реализует ваш тип, но опять же, это действительно вопрос увеличения или уменьшения доступного набора методов, не поведение одного метода, так что да, я не вижу, как в этом помогает угонщик http2.

Во всяком случае, очень интересно.

Не то чтобы я бы сделал это, но я полагаю, что это было бы возможно:

type OverrideX interface {
    GetX() int
}

type OverrideY interface {
    GetY() int
}

type Inheritor(type child Any) struct {
    Parent
    c child
}

func (i Inheritor(type child OverrideX)) GetX() int {
    return i.c.GetX()
}

func (i Inheritor(type child OverrideY)) GetY() int {
    return i.c.GetY()
}

type Parent struct {
    x, y int
}

func (p Parent) GetX() int {
    return p.x
}

func (p Parent) GetY() int {
    return p.y
}

type Child struct {
    x int
}

func (c Child) GetX() int {
    return c.x
}

func main() {
    i := Inheritor(Child){Parent{5, 6}, Child{3}}
    x, y := i.GetX(), i.GetY() // 3, 6
}

Опять же, в основном это шутка, но я думаю, что хорошо исследовать пределы возможного.

Редактировать: Хм, показывает, как у вас могут быть разные наборы методов в зависимости от параметра типа, но дает точно такой же эффект, как простое встраивание Parent в Child . Опять глупый пример ;)

Я не большой поклонник методов, которые можно вызывать только для определенного типа. Учитывая пример @toolbox , вероятно, было бы сложно тестировать из-за того, что некоторые методы можно вызывать только для определенного дочернего элемента - тестер, вероятно, пропустит какой-то случай. Также довольно неясно, какие методы доступны, и требование, чтобы IDE предоставляла предложения, — это не то, что должен требовать Go. Однако вы можете реализовать это, используя только тип, заданный структурой, выполнив утверждение типа в методе.

func (i Inheritor(type child Any)) GetX() int {
    if c, ok := i.c.(OverrideX); ok {
        return c.GetX()
    }
    return i.Parent.GetX()
}

func (i Inheritor(type child Any)) GetY() int {
    if c, ok := i.c.(OverrideY); ok {
        return c.GetY()
    }
    return i.Parent.GetY()
} 

Этот код также типобезопасен, понятен, прост в тестировании и, вероятно, работает идентично оригиналу без путаницы.

@TotallyGamerJet
Этот конкретный пример безопасен по типам, однако другие нет, и потребуются паники во время выполнения с несовместимыми типами.

Кроме того, я не уверен, как тестер мог пропустить какие-либо случаи, учитывая, что они, скорее всего, были теми, кто в первую очередь написал общий код. Кроме того, вопрос о том, ясно ли это, немного субъективен, хотя для вывода определенно не требуется IDE. Имейте в виду, что это не перегрузка функции, метод может быть либо вызван, либо нет, так что нельзя случайно пропустить какой-то случай. Любой может увидеть, что этот метод существует для определенного типа, и ему может потребоваться прочитать его еще раз, чтобы понять, какой тип требуется, но это все.

@urandom Я не обязательно имел в виду, что в этом конкретном примере кто-то пропустит дело - он очень короткий. Я имел в виду, что когда у вас есть множество методов, вызываемых только для определенных типов. Так что я не использую подтипы (как я люблю это называть). Можно даже решить «проблему выражений» без использования утверждений типов или подтипов. Вот как:

type Any interface {}

type Evaler(type t Any) interface {
    Eval() t
}

type Num struct {
    value int
}

func (n Num) Eval() int {
    return n.value
}

type Plus(type a Evaler(type t Any)) struct {
    left a
    right a
}

func (p Plus(type a Evaler(type t Any)) Eval() t {
    return p.left.Eval() + p.right.Eval()
}

func (p Plus(type a Evaler(type t Any)) String() string {
    return fmt.Sprintf("(%s+%s)", p.left, p.right)
}

type Expr interface {
    Evaler
    fmt.Stringer
}

func main() {
    var e Expr = Plus(Num){Num{1}, Num{2}}
    var v int = e.Eval() // 3
    var s string = e.String() // "(1+2)"
}

Любое неправильное использование метода Eval должно быть выявлено во время компиляции, поскольку не разрешено вызывать Eval в Plus с типом, который не реализует сложение. Хотя возможно неправильное использование String() (возможно, добавление структур), хорошее тестирование должно выявлять такие случаи. И Go обычно предпочитает простоту «правильности». Единственное, что достигается с помощью подтипов, — это больше путаницы в документах и ​​​​при использовании. Если вы можете привести пример, который требует подтипирования, я мог бы быть более склонен думать, что это хорошая идея, но в настоящее время я не убежден.
РЕДАКТИРОВАТЬ: исправлена ​​ошибка и улучшено

@TotallyGamerJet в вашем примере метод String должен вызывать String рекурсивно, а не Eval

@TotallyGamerJet в вашем примере метод String должен вызывать String рекурсивно, а не Eval

@волшебный
Я не совсем понимаю, что вы имеете ввиду. Тип структуры Plus — это Evaler, который не гарантирует, что fmt.Stringer будет удовлетворен. Вызов String() на обоих Evaler потребует утверждения типа и, следовательно, не будет безопасным для типов.

@TotallyGamerJet
К сожалению, это идея метода String. Он должен рекурсивно вызывать любые методы String для своих членов, иначе в этом нет смысла. Но вы уже видите, что потребуется утверждение типа и паника, если вы не можете гарантировать, что для метода типа Plug требуется тип a , который имеет метод String.

@urandom
Ты прав! Удивительно, но Sprintf сделает это за вас. Таким образом, вы можете просто отправить как левое, так и правое поля. Хотя все еще может возникнуть паника, если типы в Plus не реализуют Stringer, но меня это устраивает, потому что можно избежать паники, используя глагол %v для вывода структуры (он вызовет String( ) если доступно). Я думаю, что это решение ясно, и любые другие неопределенности должны быть задокументированы в коде. Так что я до сих пор не уверен, зачем нужны подтипы.

@TotallyGamerJet
Лично я до сих пор не понимаю, какие проблемы могут возникнуть, если разрешено иметь методы с различными ограничениями. Метод все еще там, и код четко описывает, какие аргументы (и получатель, в особом случае) требуются.
Подобно тому, как наличие метода, принимающего аргумент string , или получателя MyType , является ясно читаемым и недвусмысленным, так же будет и следующее определение:

func (rec MyType(type T SomeInterface(T)) Foo() T

Требования четко обозначены в самой подписи. IE это MyType(type T SomeInterface(T)) и ничего больше.

Изменение https://golang.org/cl/238003 упоминает эту проблему: design: add go2draft-type-parameters.md

Изменение https://golang.org/cl/238241 упоминает эту проблему: content: add generics-next-step article

Рождество рано!

  • Я вижу, что много усилий было потрачено на то, чтобы сделать дизайн-документ доступным, это видно, это здорово и очень ценно.
  • Эта итерация, на мой взгляд, является серьезным улучшением, и я вижу, что она реализована как есть.
  • Согласен практически со всеми рассуждениями и логикой.
  • Например, если вы указываете ограничение для одного параметра типа, вы должны сделать это для всех.
  • Сопоставимые звучат хорошо.
  • Списки типов в интерфейсах — это неплохо; согласен, это лучше, чем операторные методы, но, на мой взгляд, это, вероятно, самая большая область для дальнейшего обсуждения.
  • Вывод типа (по-прежнему) великолепен.
  • Вывод для ограничений с параметризованным типом с одним аргументом кажется умом, а не ясностью.
  • Мне нравится «Мы не утверждаем, что это просто» в примере с графиком. Это нормально.
  • (type *T constraint) выглядит как хорошее решение проблемы с указателем.
  • Полностью согласен с изменением func(x(T)) .
  • Я думаю, мы хотим, чтобы вывод типа для составных литералов с места в карьер? 😄

Спасибо команде Go! 🎉

https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md#comparable-типы-в-ограничениях

Я считаю, что сравнимый больше похож на встроенный тип, чем на интерфейс. Я считаю, что это небольшая ошибка в проекте предложения.

type ComparableHasher interface {
    comparable
    Hash() uintptr
}

нужно быть

type ComparableHasher interface {
    type comparable
    Hash() uintptr
}

Игровая площадка также, кажется, указывает на то, что она должна быть type comparable
https://go2goplay.golang.org/p/mhrl0xYsMyj

РЕДАКТИРОВАТЬ: Ян Лэнс Тейлор и Роберт Гриземер исправляют инструмент go2go (была небольшая ошибка в переводчике go2go, а не в черновике. Черновик дизайна был правильным)

Были мысли о том, чтобы позволить людям писать свои собственные универсальные хеш-таблицы и тому подобное? ISTM, который в настоящее время очень ограничен (особенно по сравнению со встроенной картой). По сути, встроенная карта имеет comparable в качестве ключевого ограничения, но, конечно, == и != недостаточно для реализации хэш-таблицы. Интерфейс типа ComparableHasher лишь перекладывает ответственность за написание хэш-функции на вызывающую сторону, он не отвечает на вопрос, как она будет выглядеть на самом деле (также вызывающая сторона, вероятно, не должна нести за это ответственность; писать хорошие хеш-функции сложно). Наконец, использование указателей в качестве ключей может быть принципиально невозможным — преобразование указателя в uintptr для использования в качестве индекса может привести к тому, что сборщик мусора переместит указатель и, следовательно, ведро изменится (исключая эту проблему, выставляя предварительно объявленный func hash(type T comparable)(v T) uintptr может быть - возможно, не идеальным - решением).

Я вполне могу принять «это не совсем возможно» в качестве ответа, мне просто любопытно узнать, думали ли вы об этом :)

@gertcuykens Я внес исправление в инструмент go2go, чтобы обрабатывать comparable , как предполагалось.

@Merovius Мы ожидаем, что люди, которые пишут общую хеш-таблицу, предоставят свою собственную хеш-функцию и, возможно, свою собственную функцию сравнения. При написании собственной хэш-функции может пригодиться пакет https://golang.org/pkg/hash/maphash/ . Вы правы в том, что хэш значения указателя должен зависеть от значения, на которое указывает этот указатель; это не может зависеть от значения указателя, преобразованного в uintptr .

Не уверен, что это ограничение текущей реализации инструмента, но попытка вернуть общий тип, ограниченный интерфейсом, возвращает ошибку:
https://go2goplay.golang.org/p/KYRFL-vrcUF

Вчера я реализовал реальный вариант использования дженериков . Это общая абстракция конвейера, которая позволяет независимо масштабировать этапы конвейера и поддерживает отмену и обработку ошибок (она не работает на игровой площадке, поскольку зависит от errgroup , но запуск ее с помощью инструмента go2go кажется Работа). Некоторые наблюдения:

  • Это было довольно весело. Наличие функционирующего средства проверки типов действительно очень помогло при итерации дизайна, переводя недостатки дизайна в ошибки типов. Конечный результат составляет ~100 LOC, включая комментарии. Так что, в целом, опыт написания универсального кода приятен, ИМО.
  • Этот вариант использования, по крайней мере, просто гладко работает с выводом типа, никаких явных экземпляров не требуется. Я думаю, что это хорошее предзнаменование для дизайна вывода.
  • Я думаю, что этот пример выиграет от возможности иметь методы с дополнительными параметрами типа. Необходимость в функции верхнего уровня для Compose означает, что построение конвейера происходит в обратном порядке — необходимо создать последние этапы конвейера, чтобы передать их функциям, создающим более ранние этапы. Если бы методы могли иметь параметры типа, вы могли бы иметь Stage конкретный тип и делать func (s *Stage(A, B)) Compose(type C)(n int, f func(B) C) *Stage(A, C) . И строить трубопровод будет в том же порядке, в каком он проложен (см. комментарий на игровой площадке). Конечно, в существующем черновике может быть и более элегантный API, которого я не вижу — трудно доказать обратное. Мне было бы интересно увидеть рабочий пример этого.

В целом, мне нравится новый черновик, FWIW :) IMO: удаление контрактов — это улучшение, как и новый способ указания необходимых операторов через списки типов.

[править: исправлена ​​ошибка в моем коде, из-за которой мог произойти тупик, если произошел сбой на этапе конвейера. Параллелизм — это сложно]

Вопрос к ветке инструментов: будет ли она идти в ногу с последним релизом (т.е. v1.15, v1.15.1,...)?

@urandom : обратите внимание, что значение, которое вы возвращаете в своем коде, имеет тип Foo (T). Каждый
такая реализация типа создает новый определенный тип, в данном случае Foo(T).
(Конечно, если у вас есть несколько Foo(T) в коде, они все одинаковые.
определенный тип).

Но тип результата вашей функции — V, который является параметром типа. Примечание
что параметр типа ограничен интерфейсом Valuer, но
_не_ интерфейс (или даже тот интерфейс). V — это параметр типа, который
новый тип типа, о котором мы знаем вещи, описанные его ограничением.
В отношении присваиваемости он действует как определенный тип с именем V.

Итак, вы пытаетесь присвоить значение типа Foo(T) переменной типа V.
(который не является ни Foo(T), ни Valuer(T), он имеет только свойства, описанные
Оценщик(Т)). Таким образом, задание не выполняется.

(Кроме того, мы все еще совершенствуем наше понимание параметров типа
и, в конечном счете, нам нужно описать это достаточно точно, чтобы мы могли написать
спец. Но имейте в виду, что каждый параметр типа фактически является новым
об определенном типе мы знаем ровно столько, сколько указывает его ограничение типа.)

Возможно, вы хотели написать это: https://go2goplay.golang.org/p/8Hz6eWSn8Ek?

@Inuart Если под веткой инструментов вы подразумеваете ветку dev.go2go: это прототип, он был создан с учетом целесообразности и в экспериментальных целях. Мы действительно хотим, чтобы люди играли с ним и пытались писать код, но не стоит полагаться на транслятор для производственного программного обеспечения. Многое может измениться (даже синтаксис, если нужно). Мы собираемся исправлять ошибки и корректировать дизайн по мере того, как узнаем из отзывов. Идти в ногу с последними выпусками Go кажется менее важным.

Вчера я реализовал реальный вариант использования дженериков. Это общая абстракция конвейера, которая позволяет независимо масштабировать этапы конвейера и поддерживает отмену и обработку ошибок (она не запускается на игровой площадке, потому что зависит от группы ошибок, но запуск ее с помощью инструмента go2go работает).

Мне нравится пример. Я только что прочитал его полностью, и то, что меня больше всего сбило с толку (даже не стоило объяснять), не имело ничего общего с задействованными дженериками. Я думаю, что ту же конструкцию без дженериков было бы не намного легче понять. Это также определенно одна из тех вещей, которые вы хотите написать один раз, с тестами, и потом не придется возиться с ними снова.

Одна вещь, которая могла бы улучшить читабельность и обзор, — это если бы инструмент Go имел способ отображения мономорфной версии универсального кода, чтобы вы могли видеть, как все обернулось. Может быть неосуществимым, отчасти потому, что функции могут даже не быть мономорфными в окончательной реализации компилятора, но я думаю, что это было бы ценно, если бы это вообще было достижимо.

Я думаю, что этот пример выиграет от возможности иметь методы с дополнительными параметрами типа.

Я также видел этот комментарий на вашей игровой площадке; определенно синтаксис альтернативного вызова кажется более читабельным и простым. Не могли бы вы объяснить это более подробно? Едва обернувшись вокруг вашего примера кода, у меня возникли проблемы с прыжком :)

Итак, вы пытаетесь присвоить значение типа Foo(T) переменной типа V.
(который не является ни Foo(T), ни Valuer(T), он имеет только свойства, описанные
Оценщик(Т)). Таким образом, задание не выполняется.

Отличное объяснение.

... В противном случае грустно видеть, что пост HN был захвачен толпой Rust. Было бы неплохо получить больше отзывов от Gophers по этому предложению.

Два вопроса к команде Go:

  • Вы хотите использовать эту проблему github для вопросов / ошибок с прототипами дженериков или есть другой форум, который вы бы предпочли?
  • Я играл со списками типов : Пример: https://go2goplay.golang.org/p/AAwSof_wT6t

Есть ли разница между этими двумя или это ошибка в игровой площадке go2? Первый компилируется, второй выдает ошибку

type Addable interface {
    type int, float64
}

func Add(type T Addable)(a, b T) T {
  return a + b
}
type Addable interface {
    type int, float64, string
}

func Add(type T Addable)(a, b T) T {
  return a + b
}

Ошибка с: invalid operation: operator + not defined for a (variable of type T)

Что ж, это был самый неожиданный и приятный сюрприз. В какой-то момент я надеялся найти способ действительно попробовать это, но я не ожидал этого в ближайшее время.

Первым делом нашел баг: https://go2goplay.golang.org/p/1r0NQnJE-NZ

Во-вторых, я создал пример итератора и был немного удивлен, обнаружив, что вывод типа не работает. Я могу просто заставить его возвращать тип интерфейса напрямую, но я не думал, что он не сможет вывести его, поскольку вся необходимая информация о типе поступает через аргумент.

Изменить: Кроме того, как уже говорили несколько человек, я думаю, что добавление новых типов во время объявлений методов было бы весьма полезным. Что касается реализации интерфейса, вы можете либо просто не разрешать реализацию интерфейса, разрешать реализацию только в том случае, если интерфейс также вызывает там дженерики ( type Example interface { Method(type T someConstraint)(v T) bool } ), либо, возможно, вы могли бы реализовать интерфейс, если _любое_ возможно его вариант реализует интерфейс, а затем его вызов будет ограничен тем, что хочет интерфейс, если он вызывается через интерфейс. Например,

```иди
тип Интерфейсный интерфейс {
Получить (строка) строка
}

тип Пример (тип T) struct {
v Т
}

// Это будет работать только потому, что Interface.Get более специфичен, чем Example.Get.
func (e Example(T)) Get(type R)(v R) T {
вернуть fmt.Sprintf("%v: %v", v, ev)
}

func DoSomething(между интерфейсом) {
// В основе лежит Example(string), а Example(string).Get(string) предполагается, потому что это требуется.
fmt.Println(inter.Get("пример"))
}

основная функция () {
// Разрешено, потому что Пример(строка).Получить(строка) возможен.
DoSomething(Пример(строка){v: "Пример."})
}

@DeedleFake Первое, о чем вы сообщаете, это не ошибка. Вам нужно будет написать https://go2goplay.golang.org/p/qo3hnviiN4k в данный момент. Это зафиксировано в эскизном проекте. В списке параметров запись a(b) интерпретируется как a (b) ( a типа в скобках b ) для обратной совместимости. Мы можем изменить это в будущем.

Пример Iterator интересен — на первый взгляд он выглядит как ошибка. Пожалуйста, зарегистрируйте ошибку (инструкции в блоге) и назначьте ее мне. Спасибо.

@Kashomon Сообщение в блоге (https://blog.golang.org/generics-next-step) предлагает список рассылки для обсуждения и подачи отдельных вопросов об ошибках. Спасибо.

Думаю проблема с + уже решена.

@тулбокс

Одна вещь, которая могла бы улучшить читабельность и обзор, — это если бы инструмент Go имел способ отображения мономорфной версии универсального кода, чтобы вы могли видеть, как все обернулось. Может быть неосуществимым, отчасти потому, что функции могут даже не быть мономорфными в окончательной реализации компилятора, но я думаю, что это было бы ценно, если бы это вообще было достижимо.

Инструмент go2go может сделать это. Вместо go tool go2go run x.go2 напишите go tool go2go translate x.go2 . Это создаст файл x.go с переведенным кодом.

Тем не менее, я должен сказать, что это довольно сложно читать. Не невозможно, но непросто.

@griesemer

Я понимаю, что возвращаемый аргумент может быть интерфейсом, но я действительно не понимаю, почему он не может быть самим универсальным типом.

Вы можете, например, использовать тот же общий тип в качестве входного параметра, и это прекрасно работает:
https://go2goplay.golang.org/p/LuDrlT3zLRb
Это работает, потому что тип уже создан?

@urandom написал:

Я понимаю, что возвращаемый аргумент может быть интерфейсом, но я действительно не понимаю, почему он не может быть самим универсальным типом.

Теоретически это возможно, но не имеет смысла делать тип возвращаемого значения универсальным, если тип возвращаемого значения не является универсальным, поскольку он определяется функциональным блоком, т. е. возвращаемым значением.

Обычно универсальные параметры либо полностью определяются кортежем значения параметра, либо типом приложения функции в месте вызова (определяет создание экземпляра универсального возвращаемого типа).

Теоретически вы также можете разрешить параметры универсального типа, которые не определяются кортежем значения параметра и должны быть указаны явно, например:

func f(type S)(i int) int
{
    s S =...
    return 2
}

не знаю, насколько это имеет смысл.

@urandom Я не обязательно имел в виду, что в этом конкретном примере кто-то пропустит дело - он очень короткий. Я имел в виду, что когда у вас есть множество методов, вызываемых только для определенных типов. Так что я не использую подтипы (как я люблю это называть). Можно даже решить «проблему выражений» без использования утверждений типов или подтипов. Вот как:

type Any interface {}

type Evaler(type t Any) interface {
  Eval() t
}

type Num struct {
  value int
}

func (n Num) Eval() int {
  return n.value
}

type Plus(type a Evaler(type t Any)) struct {
  left a
  right a
}

func (p Plus(type a Evaler(type t Any)) Eval() t {
  return p.left.Eval() + p.right.Eval()
}

func (p Plus(type a Evaler(type t Any)) String() string {
  return fmt.Sprintf("(%s+%s)", p.left, p.right)
}

type Expr interface {
  Evaler
  fmt.Stringer
}

func main() {
  var e Expr = Plus(Num){Num{1}, Num{2}}
  var v int = e.Eval() // 3
  var s string = e.String() // "(1+2)"
}

Любое неправильное использование метода Eval должно быть выявлено во время компиляции, поскольку не разрешено вызывать Eval в Plus с типом, который не реализует сложение. Хотя возможно неправильное использование String() (возможно, добавление структур), хорошее тестирование должно выявлять такие случаи. И Go обычно предпочитает простоту «правильности». Единственное, что достигается с помощью подтипов, — это больше путаницы в документах и ​​​​при использовании. Если вы можете привести пример, который требует подтипирования, я мог бы быть более склонен думать, что это хорошая идея, но в настоящее время я не убежден.
РЕДАКТИРОВАТЬ: исправлена ​​ошибка и улучшено

Я не знаю, почему бы не использовать «<>»?

@99yun
Пожалуйста, ознакомьтесь с часто задаваемыми вопросами, включенными в обновленный черновик .

Почему бы не использовать синтаксис F\как С++ и Java?
При разборе кода внутри функции, например, v := F\, в момент просмотра < неясно, видим ли мы экземпляр типа или выражение, использующее оператор <. Решение этого требует эффективного неограниченного просмотра вперед. В целом мы стремимся поддерживать эффективность парсера Go.

@urandom Тело универсальной функции всегда проверяется на тип без создания экземпляра (*); в общем (например, если он экспортируется) мы не можем знать, как он будет создан. При проверке типа он может полагаться только на доступную информацию. Если тип результата является параметром типа, а возвращаемое выражение имеет другой тип, несовместимый с назначением, возврат не может работать. Или, другими словами, если универсальная функция вызывается с (возможно, предполагаемыми) аргументами типа, тело функции не проверяется снова с этими аргументами типа. Он только проверяет, что аргументы типа удовлетворяют ограничениям универсальной функции (после создания экземпляра сигнатуры функции с этими аргументами типа). Надеюсь, это поможет.

(*) Точнее, универсальная функция проверяется на тип, поскольку она создается с собственными параметрами типа; параметры типа являются реальными типами; мы просто знаем о них ровно столько, сколько говорят нам их ограничения.

Пожалуйста, давайте продолжим эту дискуссию в другом месте. Если у вас есть дополнительные вопросы по фрагменту кода, который, по вашему мнению, должен работать, отправьте сообщение о проблеме, чтобы мы могли обсудить его там. Спасибо.

Кажется, нет способа использовать функцию для создания нулевого значения универсальной структуры. Возьмем, к примеру, эту функцию:

func zero(type T)() T {
    var zero T
    return zero
}

Похоже, он работает для основных типов (int, float32 и т. д.). Однако когда у вас есть структура с общим полем, все становится странно. Возьмем, к примеру:

type Opt(type T) struct {
    val T
}

func (o Opt(T)) Do() { /*stuff*/ }

Все вроде хорошо. Однако при выполнении:

opt := zero(Opt(int))
opt.Do() 

он не компилируется, выдавая ошибку: opt.Do undefined (type func() Opt(int) has no field or method Do) Я могу понять, если это невозможно сделать, но странно думать, что это функция, когда предполагается, что int является частью типа Opt. Но что еще более странно, так это то, что это можно сделать:

opt := zero(Opt)      //  But somehow this line compiles
opt(int).Do()         // This will panic

Я не уверен, какая часть является ошибкой, а какая предназначена.
Код: https://go2goplay.golang.org/p/M0VvyEYwbQU

@TotallyGamerJet

Ваша функция zero() не имеет аргументов, поэтому вывод типа не происходит. Вы должны создать экземпляр функции zero , а затем вызвать ее.

opt := zero(Opt(int))()
opt.Do()

https://go2goplay.golang.org/p/N6ip-nm1BP-

@тулбокс
О да. Я думал, что предоставляю тип, но забыл второй набор скобок для фактического вызова функции. Я все еще привыкаю к ​​этим дженерикам.

Я всегда понимал, что отсутствие дженериков в Go было дизайнерским решением, а не недосмотром. Это сделало Go намного проще, и я не могу понять чрезмерную паранойю против простого дублирования копии. В нашей компании мы сделали тонны кода Go и ни разу не нашли ни одного случая, когда мы предпочли бы дженерики.

Для нас это определенно заставит Go чувствовать себя меньше, чем Go, и похоже, что толпа шумихи, наконец, сумела повлиять на развитие Go в неправильном направлении. Они не могли просто оставить Go в его упрощенной красоте, нет, они должны были продолжать жаловаться и жаловаться, пока наконец не добились своего.

Извините, это не предназначено для унижения кого-либо, но именно так начинается разрушение красиво оформленного языка. Что дальше? Если мы будем продолжать что-то менять, как хотелось бы многим, мы получим «C++» или «JavaScript».

Просто оставьте Go там, где это должно было быть!

@ iio7 У меня самый низкий IQ из всех здесь, мое будущее зависит от того, смогу ли я читать код других людей. Ажиотаж начался не только из-за дженериков, но и потому, что новый дизайн не требует изменения языка в текущем предложении, поэтому мы все рады, что есть окно, позволяющее упростить вещи и при этом сохранить некоторые универсальные и функциональные вкусности. Не поймите меня неправильно, я знаю, что в команде всегда будет кто-то, кто пишет код, как ученый-ракетчик, а я, обезьяна, должен понимать это просто так? Таким образом, примеры, которые вы видите сейчас, принадлежат ученым-ракетчикам, и, честно говоря, да, мне требуется некоторое время, чтобы прочитать их, но, в конце концов, путем проб и ошибок я знаю, что они пытаются запрограммировать. Все, что я хочу сказать, это довериться Яну, Роберту и остальным, они еще не закончили с дизайном. Не удивлюсь, если через год или около того появятся инструменты, которые помогут компилятору говорить на идеальном простом обезьяньем языке, независимо от того, насколько сложный ракетный универсальный код вы ему бросите. Лучший отзыв, который вы можете дать, это переписать несколько примеров и указать, если что-то слишком переработано, чтобы они могли убедиться, что компилятор пожалуется на это или будет автоматически переписан чем-то вроде инструмента vet.

Я читал часто задаваемые вопросы о <> , но для такого глупого человека, как я, синтаксическому анализатору сложнее определить, является ли это общим вызовом, если он выглядит так: v := F<T> , а не v := F(T) ? Не сложнее ли это со скобками, поскольку он не будет знать, является ли это вызовом функции с T в качестве обычного аргумента?

Вдобавок ко всему, я думаю, что синтаксический анализатор, конечно, должен быть быстрым, но давайте также не будем забывать, что проще всего читать программисту, что не менее важно для IMO. Легче сразу понять, что делает v := F(T) ? Или проще v := F<T> ? Тоже важно учитывать :)

Не аргументирую ни за, ни против v := F<T> , просто поднимаю некоторые мысли, которые, возможно, стоит рассмотреть.

Это легально Go сегодня :

    f, c, d, e := 1, 2, 3, 4
    a, b := f < c, d > (e)
    fmt.Println(a, b) // true false

Нет смысла обсуждать угловые скобки, если вы не предложите, что с этим делать (отменить совместимость?). Это для всех намерений и целей мертвая проблема. Вероятность того, что угловые скобки будут приняты командой Go, практически нулевая. Пожалуйста, обсудите что-нибудь еще.

Изменить, чтобы добавить: извините, если этот комментарий был слишком кратким. На Reddit и HN много обсуждают угловые скобки, что меня очень расстраивает, потому что проблема обратной совместимости давно известна людям, которым небезразличны дженерики. Я понимаю, почему люди предпочитают угловые скобки, но это невозможно без кардинального изменения.

Спасибо за ваш комментарий @iio7. Всегда есть ненулевой риск того, что что-то выйдет из-под контроля. Вот почему мы соблюдали предельную осторожность на этом пути. Я считаю, что то, что мы имеем сейчас, намного чище и ортогональнее, чем то, что было в прошлом году; и лично я надеюсь, что мы сможем сделать его еще проще, особенно когда речь идет о списках типов — но мы узнаем, когда узнаем больше. (Несколько иронично, но чем более ортогональным и чистым становится дизайн, тем мощнее он будет и тем более сложный код можно будет написать.) Последние слова еще не сказаны. В прошлом году, когда у нас появился первый потенциально жизнеспособный дизайн, реакция многих людей была похожа на вашу: «Мы действительно этого хотим?» Это отличный вопрос, и мы должны попытаться ответить на него как можно лучше.

Наблюдение @gertcuykens также верно — естественно, люди, играющие с прототипом go2go, максимально исследуют его пределы (чего мы и хотим), но в процессе также создают код, который, вероятно, не прошел бы проверку в надлежащем производстве. параметр. К настоящему времени я видел много общего кода, который действительно трудно расшифровать.

Бывают ситуации, когда общий код явно будет выигрышным; Я имею в виду универсальные параллельные алгоритмы, которые позволили бы нам поместить в библиотеку несколько незаметный код. Конечно, существуют различные контейнерные структуры данных и такие вещи, как сортировка и т. д. Вероятно, подавляющему большинству кода вообще не нужны дженерики. В отличие от других языков, где общие функции занимают центральное место во многом, что делается в языке, в Go общие функции — это просто еще один инструмент в наборе инструментов Go; не фундаментальный строительный блок, на котором строится все остальное.

Для сравнения: на заре Go мы все склонны чрезмерно использовать горутины и каналы. Потребовалось время, чтобы узнать, когда они были уместны, а когда нет. Теперь у нас есть несколько более или менее устоявшихся рекомендаций, и мы используем их только тогда, когда это действительно уместно. Я надеюсь, что то же самое произошло бы, если бы у нас были дженерики.

Спасибо.

Из раздела черновика о синтаксисе на основе [T] :

Язык обычно допускает запятую в конце списка, разделенного запятыми, поэтому A[T,] должен быть разрешен, если A является универсальным типом, но обычно не разрешен для выражения индекса. Однако синтаксический анализатор не может знать, является ли A универсальным типом или значением типа среза, массива или карты, поэтому об этой ошибке синтаксического анализа нельзя сообщить, пока проверка типов не будет завершена. Опять же, решаемая, но сложная.

Разве это не может быть довольно легко решено, просто сделав конечную запятую полностью допустимой в выражениях индекса, а затем просто удалив ее с помощью gofmt ?

@DeedleFake Возможно. Это, безусловно, было бы легким выходом; но это также кажется немного уродливым синтаксически. Я не помню всех подробностей, но в более ранней версии была поддержка параметров типа стиля [type T]. См. ветку dev.go2go, коммит 3d4810b5ba, где поддержка удалена. Можно было бы раскопать это снова и исследовать.

Можно ли ограничить длину универсальных аргументов в каждом списке [] максимум одним, чтобы избежать этой проблемы, как и для встроенных универсальных типов:

  • [Н]Т
  • []Т
  • карта[К]Т
  • Чан Т

Обратите внимание, что последние аргументы во встроенных универсальных типах не заключены в [] .
Общий синтаксис объявления выглядит следующим образом: https://github.com/dotaheor/unify-Go-builtin-and-custom-generics#the-generic-declaration-syntax .

@dotaheor Я не совсем понимаю, о чем вы спрашиваете, но явно необходимо поддерживать несколько аргументов типа для универсального типа. Например, https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md#containers .

@ianlancetaylor
Я имею в виду, что каждый параметр типа заключен в [] , поэтому тип в вашей ссылке может быть объявлен как:

type Map[type K][type V] struct

Когда он используется, это выглядит так:

var m Map[string]int

Аргумент типа, не заключенный в [] , указывает на конец использования универсального типа.

Размышляя об упорядочении массивов № 39355 в сочетании с дженериками, я обнаружил, что «сопоставимый» обрабатывается особым образом в текущем черновике дженериков (предположительно из-за невозможности легко перечислить все сопоставимые типы в списке типов) как предварительно объявленное ограничение типа .

Было бы неплохо, если бы проект дженериков был изменен, чтобы также определить «упорядоченный» / «упорядоченный», аналогично тому, как предопределено «сопоставимое». Это связанное обычно используемое отношение для значений одного и того же типа, и это позволит будущим расширениям языка go определять порядок для большего количества типов (массивы, структуры, срезы, типы сумм, проверенные перечисления, ...) без сложности что не все упорядоченные типы будут перечислены в списке типов, таком как «сопоставимый».

Я не предлагаю принять решение о том, что в спецификации языка должно быть упорядочено больше типов, но это изменение дженериков делает его более совместимым с таким изменением (ограничения. Заказанный код не должен быть волшебным компилятором, сгенерированным позже или будет устарел при использовании списка типов). Сортировка пакетов может начинаться с предварительно объявленного ограничения типа «ordered», а позже может «просто» работать, например, с массивами, если они когда-либо изменялись, и без исправления используемого ограничения.

@martisch Я думаю, что это должно произойти только после расширения упорядоченных типов . В настоящее время constraints.Ordered может перечислить все типы (это не работает для comparable из-за того, что указатели, структуры, массивы и т. д. сопоставимы, так что это должно быть волшебно. Но ordered в настоящее время ограничен конечным набором встроенных базовых типов), и пользователи могут на это положиться. Если мы распространим упорядочение на массивы (например), мы все равно сможем добавить новые магические ограничения ordered и встроить их в constraints.Ordered . Это означает, что все пользователи constraints.Ordered автоматически выиграют от нового ограничения. Конечно, пользователи, которые пишут свой собственный явный список типов, не выиграют, но это то же самое, если мы добавим ordered сейчас для пользователей, которые не встраивают этот список.

Итак, ИМО, нет ничего потерянного в том, чтобы откладывать это до тех пор, пока это не станет действительно значимым. Мы не должны добавлять какой-либо возможный набор ограничений в качестве предварительно объявленного идентификатора , тем более любой потенциальный набор ограничений в будущем :)

Если мы распространим упорядочение на массивы (например), мы все равно сможем добавить новые магические ограничения ordered и встроить их в constraints.Ordered .

@Merovius Это хороший момент, о котором я не подумал. Это позволяет последовательно расширять constraints.Ordered в будущем. Если также будет constraints.Comparable , то это хорошо впишется в общую структуру.

@martisch , обратите внимание, что ordered — в отличие от comparable — не является когерентным типом интерфейса, если мы также не определяем (глобальный) общий порядок среди конкретных типов или запрещаем неуниверсальному коду использовать < для переменных типа ordered или запретить использование comparable в качестве общего типа интерфейса времени выполнения.

В противном случае транзитивность «орудий» нарушается. Рассмотрим этот фрагмент программы:

    var x constraints.Ordered = int(0)
    var y constraints.Ordered = string("0")
    fmt.Println(x < y)

Что он должен выводить? (Является ли ответ интуитивным или произвольным?)

@bcmills
А как насчет fun (<)(type T Ordered)(t1 T,t2 T) Bool?

Чтобы сравнить арифметические типы разного вида:

Если любая арифметика S реализует только Ordered(T) для S<:T , то:

//Isn't possible I think
interface SorT(S,T)
{ 
type S,T
}

fun (<)(type R SorT(S,T), S Ordered(R), T Ordered(R))(s S, t T) Bool

должен быть уникальным.

Для полиморфизма во время выполнения вам потребуется, чтобы Ordered был параметризуемым.
Или:
Вы разбиваете Ordered на типы кортежей, а затем переписываете (<) следующим образом:

//but isn't supported that either
fun(<)(type R Ordered)(s R.0,t R.1)

Привет!
У меня есть вопрос.

Есть ли способ сделать ограничение типа, которое передает только общие типы с одним параметром типа?
Что-то, что проходит только Result(T) / Option(T) /etc, но не только T .
Я пытался

type Box(type T) interface {
    Val() (T, bool)
}

но для этого требуется метод Val()

type Box(type T) interface{}

похоже на interface{} , т.е. Any

также пробовал https://go2goplay.golang.org/p/lkbTI7yppmh -> компиляция не удалась

type Box(type T) interface {
       type Box(T)
}

https://go2goplay.golang.org/p/5NsKWNa3E1k -> сбой компиляции

type Box(type T) interface{}

type Generic(type T) interface {
    type Box(T)
}

https://go2goplay.golang.org/p/CKzE2J-YOpD -> не работает

type Box(type T) interface{}

type Generic(type T Box(T)) interface {}

Ожидается ли такое поведение или это просто ошибка проверки типов?

Ограничения @tdakkota применяются к аргументам типа и к полностью созданной форме аргументов типа. Невозможно написать ограничение типа, которое предъявляет какие-либо требования к неэкземплярной форме аргумента типа.

Пожалуйста, ознакомьтесь с часто задаваемыми вопросами, включенными в обновленный черновик .

Почему бы не использовать синтаксис Fкак С++ и Java?
При разборе кода внутри функции, например, v := F, в момент просмотра < неясно, видим ли мы экземпляр типа или выражение, использующее оператор <. Решение этого требует эффективного неограниченного просмотра вперед. В целом мы стремимся поддерживать эффективность парсера Go.

@TotallyGamerJet Как бы то ни было!

Как работать с нулевым значением универсального типа? Без перечисления, как мы можем работать с необязательным значением.
Например: универсальная версия vector и функция с именем First возвращают первый элемент, если его длина > 0, иначе нулевое значение универсального типа.
Как мы напишем такой код? Поскольку мы не знаем, какой тип в векторе, если chan/slice/map , мы можем return (nil, false) , если struct или primitive type как string , int , bool , как с этим бороться?

@leaxoy

var zero T должно хватить

@leaxoy

var zero T должно хватить

Глобальная магическая переменная, такая как nil ?

@leaxoy
var zero T должно хватить

Глобальная магическая переменная, такая как nil ?

Обсуждается предложение по этой теме — см. предложение: Перейти 2: универсальное нулевое значение с выводом типа #35966 .

Он исследует несколько новых альтернативных синтаксисов для выражения (не оператора как var zero T ), которое всегда будет возвращать нулевое значение типа.

Нулевое значение в настоящее время выглядит возможным, но может ли оно занимать место в стеке или куче? Должны ли мы рассмотреть возможность использования enum Option для выполнения этого за один шаг.
В противном случае, если нулевое значение не занимает места, было бы лучше и не нужно добавлять перечисление.

Нулевое значение в настоящее время выглядит возможным, но может ли оно занимать место в стеке или куче?

Исторически сложилось так, что компилятор Go оптимизировал такие случаи. Я не слишком беспокоюсь.

Значение типа по умолчанию можно указать в шаблонах C++. Рассматривалась ли аналогичная конструкция для параметров универсального типа? Потенциально это позволит модифицировать существующие типы без нарушения существующего кода.

Например, рассмотрим существующий тип asn1.ObjectIdentifier , который является []int . Одна проблема с этим типом заключается в том, что он не соответствует спецификации ASN.1, в которой говорится, что каждый sub-oid может быть целым числом произвольной длины (например, *big.Int ). Потенциально ObjectIdentifier можно изменить, чтобы он принимал общий параметр, но это нарушило бы большую часть существующего кода. Если бы существовал способ указать, что int является значением параметра по умолчанию, возможно, это позволило бы модифицировать существующий код.

type SignedInteger interface {
    type int, int32, int64, *big.Int
}
type ObjectIdentifier(type T SignedInteger) []T
// type ObjectIdentifier(type T SignedInteger=int) []T  // `int` would be the default instantiation type.

// New code with generic awareness would compile in go2.
var oid1 ObjectIdentifier(int) = ObjectIdentifier(int){1, 2, 3}

// But existing code would fail to compile:
var oid1 ObjectIdentifier = ObjectIdentifier{1, 2, 3}

Просто для ясности: приведенная выше asn1.ObjectIdentifier — это всего лишь пример. Я не говорю, что использование дженериков — единственный или лучший способ решить проблему соответствия ASN.1.

Кроме того, есть ли планы разрешить параметризуемые конечные границы интерфейса?:

type Ordable(type T, S) interface {
    type S, type T
}

Как поддерживать условие where для параметра типа.
Можем ли мы написать такой код:

type Vector(type T) struct {
    vec []T
}

func (v Vector(T)) Sum() T where T: Summable {
      //
}

func (v Vector(T)) First()  (T, bool) {
     //
}

Метод Sum работает только тогда, когда параметры типа T равны Summable , иначе мы не сможем вызвать Sum в Vector.

Привет @leaxoy

Вы можете просто написать что-то вроде https://go2goplay.golang.org/p/pRznN30Qu8V

type Addable interface {
    type int, uint
}

type SummableVector(type T Addable) Vector(T)

func (v SummableVector(T)) Sum() T {
    var r T
    for _, i := range v.vec {
        r = r + i
    }
    return r
}

Я думаю, что предложение where не похоже на Go, и его будет сложно разобрать, оно должно быть чем-то вроде

type Vector(type T) struct {
    vec []T
}

func (v Vector(T Summable)) Sum() T {
      //
}

func (v Vector(T)) First()  (T, bool) {
     //
}

но это похоже на специализацию метода.

@sebastien-rosset Мы не рассматривали типы по умолчанию для параметров универсального типа. Язык не имеет значений по умолчанию для аргументов функций, и не очевидно, почему дженерики будут другими. На мой взгляд, возможность сделать существующий код совместимым с пакетом, добавляющим дженерики, не является приоритетом. Если пакет переписывается для использования дженериков, можно требовать изменения существующего кода или просто вводить дженерики с новыми именами.

@сигхойя

Кроме того, есть ли какие-либо планы разрешить параметризуемые конечные границы интерфейса?

Извините, я не понимаю вопроса.

Я хотел бы напомнить людям, что сообщение в блоге (https://blog.golang.org/generics-next-step) предполагает, что обсуждение дженериков происходит в списке рассылки golang-nuts, а не в системе отслеживания проблем. Я буду продолжать читать этот выпуск, но в нем около 800 комментариев, и он совершенно громоздкий, если не считать других трудностей системы отслеживания проблем, таких как отсутствие цепочек комментариев. Спасибо.

Обратная связь: я прослушал самый последний подкаст Go Time, и я должен сказать, что объяснение от @griesemer по поводу проблемы с угловыми скобками было впервые, когда я действительно понял это, т.е. что на самом деле означает «неограниченный просмотр вперед в синтаксическом анализаторе». для Го? Большое спасибо за дополнительную информацию там.

Кроме того, я за квадратные скобки. 😄

@ianlancetaylor

сообщение в блоге предполагает, что обсуждение дженериков происходит в списке рассылки golang-nuts, а не в системе отслеживания проблем.

В недавнем сообщении в блоге [1] @ddevault указывает, что для группы Google (где находится этот список рассылки) требуется учетная запись Google. Вам нужен аккаунт для публикации, и, по-видимому, в некоторых группах даже требуется учетная запись для чтения. У меня есть учетная запись Google, поэтому для меня это не проблема (и я также не говорю, что согласен со всем в этом сообщении в блоге), но я согласен с тем, что если мы хотим иметь более справедливое сообщество golang, и если мы хотим избежать эхо-камеры, возможно, было бы лучше не иметь такого рода требований.

Я не знал этого о группах Google, и если есть какое-то исключение для голанг-орехов, то, пожалуйста, примите мои извинения и не обращайте на это внимания. Что бы это ни стоило, я многому научился, прочитав эту ветку, и я также был вполне убежден (после использования golang более шести лет), что дженерики - неправильный подход к языку. Однако это только мое личное мнение, и спасибо за то, что вы предоставили нам язык, который мне очень нравится!

Ваше здоровье!

[1] https://drewdevault.com/2020/08/01/pkg-go-dev-sucks.html

@purpleidea В качестве списка рассылки можно использовать любую группу Google. Вы можете присоединиться и участвовать, не имея учетной записи Google.

@ianlancetaylor

Любую группу Google можно использовать в качестве списка рассылки. Вы можете присоединиться и участвовать, не имея учетной записи Google.

Когда я иду в:

https://groups.google.com/forum/#!forum/golang-орехи

в приватном окне браузера (чтобы скрыть мою учетную запись google, в которую я вошел) и нажмите «новая тема», она перенаправляет меня на страницу входа в google. Как я могу использовать его без учетной записи Google?

@purpleidea Написав электронное письмо на адрес [email protected] . Это список рассылки. Только для веб-интерфейса требуется учетная запись Google. Что кажется справедливым — учитывая, что это список рассылки, вам нужен адрес электронной почты, а группы, очевидно, могут отправлять письма только из учетной записи gmail.

Я думаю, что большинство людей не понимают, что такое список рассылки.

В любом случае вы также можете использовать любое общедоступное зеркало списка рассылки, например https://www.mail-archive.com/[email protected]/

Все это здорово, но от этого не становится легче, когда люди ссылаются на
темы в группах Google (что случается часто). это невероятно
раздражает попытка найти сообщение по идентификатору в URL-адресе.

-Сэм

Вс, 2 августа 2020 г., в 19:24, Ахмед В. написал:
>
>

Я думаю, что большинство людей не понимают, что такое список рассылки.

В любом случае, вы также можете использовать любое публичное зеркало списка рассылки, например
https://www.mail-archive.com/[email protected]/

— Вы получаете это, потому что подписаны на эту тему.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/golang/go/issues/15292#issuecomment-667738419 или
отписаться
https://github.com/notifications/unsubscribe-auth/AAD5EPNQTEUF5SPT6GMM4JLR6XYUBANCNFSM4CA35RXQ
.

--
Сэм Уайтд

Это действительно не место для обсуждения.

Есть новости по этому поводу? 🤔

@Imperatorn были, просто они здесь не обсуждались. Было решено, что квадратные скобки [ ] будут выбранным синтаксисом, а слово «тип» не потребуется при написании универсальных типов/функций. Существует также новый псевдоним «любой» для пустого интерфейса.

Последний проект дженериков находится здесь .
Смотрите также этот комментарий re: обсуждения на эту тему. Спасибо.

Я хотел бы напомнить людям, что сообщение в блоге (https://blog.golang.org/generics-next-step) предполагает, что обсуждение дженериков происходит в списке рассылки golang-nuts, а не в системе отслеживания проблем. Я буду продолжать читать этот выпуск, но в нем около 800 комментариев, и он совершенно громоздкий, если не считать других трудностей системы отслеживания проблем, таких как отсутствие цепочек комментариев. Спасибо.

В связи с этим, хотя я уважаю то, что команда Go хотела бы убрать такие обсуждения из проблемы по практическим причинам, похоже, что на GitHub есть много членов сообщества, которые не на голанг-орехах. Интересно, подойдет ли новая функция «Обсуждения» на GitHub? 🤔 Судя по всему, есть резьба.

@toolbox Аргумент можно привести и в другом направлении - есть люди, у которых нет учетной записи github (и они отказываются ее получить). Вам также не нужно быть подписанным на golang-nuts , чтобы публиковать сообщения и участвовать в них.

@Merovius Одна из функций, которые мне очень нравятся в проблемах GitHub, заключается в том, что я могу подписаться на уведомления только о проблемах, которые меня интересуют. Я не знаю, как это сделать с группами Google?

Я уверен, что есть веские причины предпочесть тот или иной. Безусловно, может возникнуть дискуссия о том, каким должен быть предпочтительный форум. Однако, опять же, я не думаю, что обсуждение должно быть здесь. Эта проблема и так достаточно шумная.

@toolbox Аргумент можно привести и в другом направлении - есть люди, у которых нет учетной записи github (и они отказываются ее получить). Вам также не нужно быть подписанным на golang-nuts, чтобы публиковать сообщения и участвовать там.

Я понимаю, что вы говорите, и это правда, но вы не попадаете в цель. Я не говорю, что пользователям golang-nuts нужно предложить перейти на GitHub (как сейчас происходит наоборот). Я говорю, что пользователям GitHub было бы неплохо иметь дискуссионный форум.

Я уверен, что есть веские причины предпочесть тот или иной. Безусловно, может возникнуть дискуссия о том, каким должен быть предпочтительный форум. Однако, опять же, я не думаю, что обсуждение должно быть здесь. Эта проблема и так достаточно шумная.

Я согласен, что это совершенно не по теме данного вопроса, и я извиняюсь за то, что поднял его, но я надеюсь, что вы видите иронию.

@keean @Merovius @toolbox и ребята из будущего.

К вашему сведению: для такого рода обсуждений существует открытый вопрос, см. #37469.

Привет,

Прежде всего, спасибо за Go. Язык вообще блестящий. Одной из самых удивительных вещей в Go для меня была удобочитаемость. Я новичок в этом языке, поэтому я все еще нахожусь на ранних стадиях открытия, но до сих пор он казался невероятно ясным, четким и точным.

Единственный отзыв, который я хотел бы представить, заключается в том, что после моего первоначального просмотра предложения по дженерикам мне было нелегко быстро разобрать [T Constraint] , по крайней мере, не так просто, как набор символов, предназначенный для дженериков. . Я понимаю, что стиль C++ F<T Constraint> невозможен из-за характера парадигмы многократных возвратов go. Любые символы, отличные от ascii, были бы абсолютной рутиной, поэтому я очень благодарен, что вы отказались от этой идеи.

Пожалуйста, рассмотрите возможность использования комбинации символов. Я не уверен, что побитовые операции могут быть неправильно истолкованы или замутить воду синтаксического анализа, но, на мой взгляд, F<<T Constraint>> было бы неплохо. Однако подойдет любая комбинация символов. Хотя это может добавить некоторый первоначальный налог на сканирование глаз, я думаю, что это можно легко исправить с помощью лигатур шрифтов, таких как FireCoda и Iosevka . Не так много можно сделать, чтобы четко и легко различить разницу между Map[T Constraint] и map[string]T .

Я не сомневаюсь, что люди будут тренировать свой ум, чтобы различать два применения [] в зависимости от контекста. Я просто подозреваю, что это усложнит кривую обучения.

Спасибо за замечание. Чтобы не упустить очевидное, но map[T1]T2 и Map[T1 Constraint] можно различить, потому что первое не имеет ограничений, а второе имеет обязательное ограничение.

Синтаксис широко обсуждался на golang-nuts, и я думаю, что он решен. Мы будем рады услышать комментарии, основанные на фактических данных, таких как синтаксический анализ двусмысленностей. Для комментариев, основанных на чувствах и предпочтениях, я думаю, пришло время не соглашаться и соглашаться.

Еще раз спасибо.

@ianlancetaylor Достаточно честно. Я уверен, что вы устали слышать придирки к этому :) Для чего бы это ни стоило, я имел в виду легко различать сканирование.

В любом случае, я с нетерпением жду возможности его использовать. Спасибо.

Общая альтернатива reflect.MakeFunc была бы огромным выигрышем в производительности инструментов Go. Но я не вижу способа разложить тип функции с текущим предложением.

@ Хулио-Гуэрра, я не уверен, что вы подразумеваете под «разложением типа функции». Вы можете в некоторой степени параметризовать типы аргументов и возвращаемых значений: https://go2goplay.golang.org/p/RwU11S4gC59

package main

import (
    "fmt"
)

func Call[In, Out any](f func(In) Out, v In) Out {
    return f(v)
}

func main() {
    triple := func(i int) int {
        return 3 * i
    }
    fmt.Println(Call(triple, 23))
}

Это работает только в том случае, если количество обоих постоянно.

@ Хулио-Гуэрра, я не уверен, что вы подразумеваете под «разложением типа функции». Вы можете в некоторой степени параметризовать типы аргументов и возвращаемых значений: https://go2goplay.golang.org/p/RwU11S4gC59

На самом деле, я имею в виду то, что вы сделали, но в обобщенном виде для любого параметра функции и списка типов возвращаемого значения (аналогично массиву параметров и типов возвращаемого значения для Reflect.MakeFunc). Это позволило бы иметь обобщенные оболочки функций (вместо использования инструментальной генерации кода).

Была ли эта страница полезной?
0 / 5 - 0 рейтинги