Эта статья о том, что будет означать добавление дженериков в Go, и почему я считаю, что нам это следует сделать. Также я коснусь возможного изменения архитектуры языка ради добавления дженериков.
Go вышел 10 ноября 2009-го. Меньше чем через сутки появился первый комментарий про дженерики. В нём также упомянуты исключения, которые мы добавили в язык в виде паники и восстановления (panic and recover) в начале 2010-го.
За три года наблюдений отсутствие дженериков всегда входило список трёх главных проблем, которые необходимо исправить в языке.
Зачем нужны дженерики?
Что означает добавление дженериков и почему мы этого хотим?
Перефразируя Jazayeri и других: программирование с дженериками позволяет представлять функции и структуры данных в виде дженериков, исключая типы.
Что это означает?
Допустим, нам нужно представить элементы слайса в обратном порядке. Это не слишком распространённая задача, но и не такая уж редкая.
Пусть у нас есть слайс целых чисел.
func ReverseInts(s []int) {
first := 0
last := len(s)
for first < last {
s[first], s[last] = s[last], s[first]
first++
last--
}
}
Выглядит просто. Но даже для простой функции вроде этой вам может понадобиться написать несколько тестов. И когда я это сделал, то обнаружил баг. Уверен, многие его уже заметили.
func ReverseInts(s []int) {
first := 0
last := len(s) - 1
for first < last {
s[first], s[last] = s[last], s[first]
first++
last--
}
}
Мы вычитаем 1, в то время как назначаем переменную
last
. Теперь поменяем порядок в слайсе строковых значений.func ReverseStrings(s []string) {
first := 0
last := len(s) - 1
for first < last {
s[first], s[last] = s[last], s[first]
first++
last--
}
}
Если вы сравните
ReverseInts
и ReverseStrings
, то увидите, что функции совершенно одинаковые, за исключением типа параметра. Вряд ли я вас этим удивил.А вот что может удивить новичков в Go, так это отсутствие способа написать простую функцию
Reverse
, работающую со слайсами любых типов.В большинстве других языков вы можете написать такую функцию.
В динамически типизируемых языках вроде Python или JavaScript вы можете просто написать функцию без указания типа элементов. В Go так нельзя, потому что это статически типизируемый язык, он требует прописать конкретный тип слайса и тип его элементов.
Большинство других статически типизируемых языков, вроде С++, Java, Rust или Swift, поддерживают дженерики именно для таких ситуаций.
Современное программирование на Go с дженериками
Так как же люди пишут подобный код на Go?
В этом языке вы можете написать функцию, которая работает с разными типами слайсов с помощью интерфейсного типа (interface type) и определения метода для типов слайсов, которые вы хотите передавать. Так работает функция
sort.Sort
из стандартной библиотеки.Иными словами, интерфейсные типы в Go — это разновидность программирования с дженериками. Они позволяют выявлять общие аспекты разных типов и выражать их в виде методов. Затем можно писать функции, использующие эти интерфейсные типы, и функции будут работать с любыми типами, реализованными в этих методах.
Но этот подход не соответствует нашим желаниям. Нужно самостоятельно писать методы в интерфейсах. Довольно странно определять именованный тип с парой методов чтобы лишь поменять порядок элементов в слайсе. А методы, которые вы пишете, будут совершенно одинаковыми для всех типов слайсов, так что мы не исключили код, а, в каком-то смысле, перенесли и уплотнили. Хотя интерфейсы — это способ представления дженериков, они не дают нам всего, что нам нужно от дженериков.
Другой способ использования интерфейсов для дженериков, при котором не нужно писать методы, заключается в том, чтобы переложить на язык обязанность определять методы для каких-то типов. Сейчас Go этого не поддерживает, но, к примеру, язык может определять, чтобы каждый тип слайса имел метод
Index
, возвращающий элемент. Но чтобы использовать этот метод на практике, потребуется возвращать пустой тип интерфейса, и тогда мы теряем все преимущества статического типизирования. Не будет способа определять дженерик-функцию, которая берёт два разных слайса с элементами одного типа, или которая берёт карту с элементами одного типа и возвращает слайс с элементами такого же типа. Go статически типизирован потому, что это облегчает написание больших программ. Мы не хотим терять преимущества статического типизирования ради выгод дженериков.Ещё один подход заключается в написании дженерик-функции
Reverse
, использующей пакет reflect, но это было бы слишком странно и работал бы чересчур медленно. Кроме того, потребовалось бы делать явные утверждения типов и отказаться от проверки на статичность типов.Или можно написать генератор кода, который берёт тип и генерирует функцию
Reverse
для слайсов этого типа. Для этого подходят несколько генераторов. Но такое решение прибавляет ещё один этап в каждый пакет, которому понадобится Reverse
, что усложняет сборку из-за необходимости компилирования всех копий, а исправление багов в исходном коде потребует перегенерирования всех экземпляров, некоторые из которых могут уже задействоваться в совершенно других проектах. Все описанные подходы достаточно странные, и я думаю, что большинство программистов, которым нужно менять порядок в слайсах на Go, просто пишут функцию для конкретного типа слайса. Потом им нужно написать тесты для функций, чтобы удостовериться, что они не сделали простых ошибок, вроде той, что я сделал в начале. А потом придётся рутинно прогонять эти тесты.
И когда мы это делаем, это означает, что мы делаем кучу лишней работы просто ради функции, которая выглядит точно так же, за исключением типа элементов. Дело не в том, что это невозможно. Это точно возможно, и программисты на Go так и делают. Просто должен быть способ получше.
Для статически типизированных языков вроде Go этот способ получше называется дженериками. Выше я написал, что программирование с дженериками позволяет представлять функции и структуры данных в виде дженериков исключая типы. Именно это нам и нужно.
Что дженерики могут дать Go
Первое и самое важное, что нам нужно от дженериков в Go, это возможность писать функции вроде
Reverse
и не беспокоиться о типах элементов в слайсах. Мы хотим исключить тип элементов. Также мы хотим писать функцию и тесты однократно, класть их в доступный для Go пакет и вызывать по мере необходимости.А поскольку мы говорим об open source, то будет ещё лучше, если кто-нибудь сможет написать
Reverse
один раз, а мы все будем пользоваться этой реализацией.Здесь мне следует сказать, что термин «дженерики» может обозначать много разного. В этой статье я подразумеваю под «дженериками» то, что описал выше. В частности, я не имею в виду шаблоны, как в С++, которые поддерживают гораздо больше возможностей, чем я перечислил.
Я подробно рассказал о
Reverse
, но есть и много других функций, которые мы могли бы писать как дженерики, например:- Поиск наименьшего или наибольшего элемента в слайсе.
- Поиск среднего или стандартного отклонения в слайсе.
- Вычисление модуля или пересечения в карте.
- Поиск кратчайшего пути в графе с узлами или рёбрами.
- Применение к слайсу или карте функции преобразования, которая возвращает новый слайс или карту.
Эти примеры доступны в большинстве других языков. Фактически, я написал этот список, просто посмотрев на шаблоны из стандартной библиотеки С++.
Есть примеры, характерные для Go с его строгой поддержкой параллелизма.
- Чтение из канала без таймаута.
- Комбинирование двух каналов в один.
- Параллельный вызов списка функций, который возвращает слайс результатов.
- Вызов списка функций с использованием
Context
, возвращающий результат первой завершаемой функции, отменяющий и очищающий дополнительные горутины.
Я много раз видел все эти функции, с разными типами. Их не трудно написать на Go. Но лучше было бы хорошо многократно использовать эффективную и отлаженную реализацию, которая работает для всех типов.
Чтобы не было недопонимания, это всего лишь примеры. Есть много других функций общего назначения, которые будет проще и безопаснее писать с использованием дженериков. Кроме того, как написал выше, это не только функции, а ещё и структуры данных.
В Go встроены две дженерик-структуры данных общего назначения: слайсы и карты. Они могут содержать значения любых типов, с проверкой на статичность типов значений, которые хранятся и извлекаются. Значения хранятся сами по себе, а не как интерфейсные типы. То есть, когда у меня есть
[]int
, слайс содержит сами числа, а не числа, преобразованные в интерфейсный тип.Слайсы и карты — самые полезные дженерик-структуры данных, но они не единственные. Вот другие:
- Наборы.
- Самобалансирующиеся деревья с эффективной вставкой и обходом в порядке сортировки.
- Мальтикарты с многочисленными экземплярами ключа.
- Конкуретные карты хэшей, поддерживающие параллельную вставку и поиск безо всяких блокировок.
Если мы можем писать дженерик-типы, мы можем определять новые структуры данных, вроде тех, что имеют те же преимущества проверки типов, как у слайсов и карт: компилятор может статически проверить типы значений, которые они содержат, и значения могут храниться сами по себе, а не как интерфейсные типы.
Должна быть возможность взять алгоритмы, вроде упомянутых мной выше, и применить их к дженерик-структурам данных.
Все эти примеры должны быть такими же, как в случае с
Reverse
: дженерик-функции и структуры пишутся один раз, помещаются в пакет, а затем многократно используются там, где нужно. Они должны работать как слайсы и карты, в том смысле, что они должны хранить не значения пустых интерфейсных типов, а конкретные типы, которые будут проверяться при компиляции.Вот какую пользу может извлечь Go из дженериков. Дженерики могут стать эффективными кирпичиками, позволяющими делиться кодом и проще создавать программы.
Надеюсь, я смог объяснить, почему имеет смысл их изучить.
Преимущества и цена
Но дженерики не появляются из Big Rock Candy Mountain, края, в котором солнце каждый день сияет над лимонадными источниками. У каждого изменения языка есть своя цена. Несомненно, что добавление дженериков в Go усложнит язык. Как и с любым изменением, нам нужно обсудить максимизацию преимуществ и минимизацию цены.
В Go мы старались уменьшить сложность с помощью независимых, самостоятельных особенностей, которые можно свободно комбинировать друг с другом. Мы снижаем сложность, делая эти особенности простыми, и увеличиваем преимущества этих свойств, обеспечивая свободную комбинируемость. То же самое мы хотим сделать и с дженериками.
Чтобы выражаться конкретнее, приведу несколько рекомендаций, которым мы должны следовать.
Минимизация новых концепций
Нужно добавлять в язык как можно меньше новых концепций. Это означает минимальное количество нового синтаксиса, ключевых слов и других наименований.
Сложность падает на автора дженерик-кода, а не пользователя
Сложность должна максимально перекладываться на программиста, пишущего дженерик-пакет. Мы не хотим, чтобы пользователь пакета волновался по поводу дженериков. Значит, должна быть возможность естественным образом вызывать дженерик-функции, то есть нужно сообщать обо всех ошибках использования дженерик-пакетов так, чтобы их можно было легко понять и исправить. Также должен быть простой способ отладки вызовов в дженерик-коде.
Автор кода и пользователь могут работать независимо
Нужно легко разделять задачи автора дженерик-кода и его пользователей, чтобы они могли развивать код независимо. Один не должен беспокоиться о том, что делает другой, хотя бы не больше, чем беспокоятся автор и пользователь обычной функции из другого пакета. Вроде бы очевидно, но не во всех языках это условие соблюдается в отношении дженериков.
Быстрая сборка и исполнение
Мы хотим как можно больше сократить длительность сборки и исполнения по сравнению с сегодняшней производительностью Go. C дженериками связаны компромиссы между быстрой сборкой и исполнением. Нам нужно как можно больше ускорить оба процесса.
Сохранение ясности и простоты Go
Самое важное заключается в том, что сейчас Go является простым языком. Программы на Go обычно легко понять. В ходе нашего длительного исследования мы большое внимание уделяем поиску возможности добавления дженериков с сохранением ясности и простоты. Нам нужно найти механизмы, которые хорошо укладываются в текущий язык, которые не превратят его во что-то совершенно другое.
Этим рекомендациям нужно следовать при любых реализациях дженериков в Go. Это моё самое важное сообщение сегодня: дженерики могут дать языку значительные преимущества, но их стоит внедрять лишь в том случае, если Go останется самим собой.
Черновик архитектуры
Думаю, это возможно. Чтобы завершить статью, я хочу от обсуждения необходимости внедрения дженериков и требований к ним перейти к обсуждению архитектуры, с помощью которой дженерики можно внедрить в Go.
На Gophercon в этом году Роберт Гриземер (Robert Griesemer) и я опубликовали черновик архитектуры дженериков в Go. По ссылке вы найдёте все подробности, а здесь я коснусь лишь основных моментов.
Так реализована функция
Reverse
.func Reverse (type Element) (s []Element) {
first := 0
last := len(s) - 1
for first < last {
s[first], s[last] = s[last], s[first]
first++
last--
}
}
Тело функции совершенно такое же, изменилась только сигнатура.
Тип элементов слайса исключён. Теперь он называется
Element
и превратился в так называемый параметр типа (type parameter). Вместо того, чтобы быть частью параметра типа слайса, он теперь отдельный, дополнительный параметр типа.Обычно для вызова функции с параметром типа нужно передать аргумент типа, который выглядит как и любой другой аргумент, не считая того, что это тип.
func ReverseAndPrint(s []int) {
Reverse(int)(s)
fmt.Println(s)
}
Это
(int)
, представленный в примере после Reverse
.К счастью, в большинстве случаев, включая этот, компилятор может вывести аргумент типа из типов обычных аргументов, и вам вообще не понадобится упоминать аргумент типа.
Вызов дженерик-функции выглядит как вызов любой другой функции.
func ReverseAndPrint(s []int) {
Reverse(s)
fmt.Println(s)
}
Хотя дженерик-функция
Reverse
чуть сложнее функций ReverseInts
и ReverseStrings
, эта сложность возлагается на автора кода, а не на вызывающего.Контракты
Поскольку Go статически типизирован, нужно обсудить тип параметра типа. Этот метатип говорит компилятору, какого рода аргументы типов разрешены при вызове дженерик-функции, а также какого рода операции может выполнить дженерик-функция со значениями параметра типа.
Функция
Reverse
может работать со слайсами любых типов. Она может только присваивать значения типа Element
, который работает с любыми типами в Go. Для этого вида дженерик-функций, который встречается очень часто, нам не нужно говорить ничего особого о параметре типа.Теперь посмотрим на другую функцию.
func IndexByte (type T Sequence) (s T, b byte) int {
for i := 0; i < len(s); i++ {
if s[i] == b {
return i
}
}
return -1
}
Сейчас пакеты bytes и strings из стандартной библиотеки содержат функцию
IndexByte
. Эта функция возвращает индекс b
в последовательности s
, где s
является строкой или []byte
. Мы могли бы одной этой дженерик-функцией заменить две функции в пакетах bytes и strings. На практике можно этого не делать, но это простой и полезный пример.Теперь нам нужно узнать, как действует параметр
T
— как строка или []byte
. Можно применить к нему len
, индексировать и сравнить результат операции индексирования со значением byte.Для выполнения компиляции самому параметру типа
T
тоже нужен тип. Это будет метатип, но поскольку нам иногда нужно описывать различные взаимосвязанные типы, и поскольку метатип описывает связь между реализацией дженерик-функции и вызывающим её, мы называем тип T
контрактом. В данном случае контракт называется Sequence
. Он идёт после списка параметров типа.Вот как контракт
Sequence
определяется в этом примере.contract Sequence(T) {
T string, []byte
}
Всё просто, потому что и пример простой: параметр типа
T
может быть строкой или []byte
. Контракт может быть новым ключевым словом или специальным идентификатором, который распознаётся в области видимости пакета. Подробности вы можете узнать из документации к черновику архитектуры.Те, кто помнят архитектуру, которую мы представили на Gophercon 2018, заметят, что такой способ написания контракта гораздо проще. Мы получили много отзывов о ранней версии, в которой контракты были гораздо сложнее, и постарались учесть пожелания. Новые контракты гораздо проще писать, читать и понимать.
Контракты позволяют задавать тип параметра типа и/или список методов параметра типа. Также контракты позволяют описывать взаимосвязи между разными параметрами типов.
Контракты с методами
Вот ещё один простой пример функции, использующей метод
String
для возвращения []string
строкового представления всех элементов в s
.func ToStrings (type E Stringer) (s []E) []string {
r := make([]string, len(s))
for i, v := range s {
r[i] = v.String()
}
return r
}
Всё просто: проходим по слайсу, вызываем метод
String
применительно к каждому элементу и возвращаем слайс с получившимся строковыми значениями.Этой функции нужно, чтобы тип элемента реализовывал метод
String
. И контракт Stringer
это гарантирует.contract Stringer(T) {
T String() string
}
Контракт утверждает, что
T
должен реализовывать метод String
.Вы могли заметить, что этот контракт выглядит как интерфейс
fmt.Stringer
, так что нужно указать на то, что аргумент функции ToStrings
не является слайсом fmt.Stringer
. Это слайс какого-то типа элементов, и тип элементов реализует fmt.Stringer
. Представления памяти слайса типа элементов и слайса fmt.Stringer
обычно различаются, и Go не поддерживает прямое преобразование между ними. Так что лучше не полениться и написать, даже если fmt.Stringer
существует.Контракты с несколькими типами
Вот пример контракта с несколькими параметрами типов.
type Graph (type Node, Edge G) struct { ... }
contract G(Node, Edge) {
Node Edges() []Edge
Edge Nodes() (from Node, to Node)
}
func New (type Node, Edge G) (nodes []Node) *Graph(Node, Edge) {
...
}
func (g *Graph(Node, Edge)) ShortestPath(from, to Node) []Edge {
...
}
Здесь описан граф, построенный из узлов и рёбер. Мы не требуем для него определённую структуру данных. Вместо этого мы говорим, что тип
Node
должен содержать метод Edges
, который возвращает список рёбер, соединённых с Node
. А тип Edge
должен иметь метод Nodes
, который возвращает два Nodes
, соединённых Edge
.Я опустил реализацию, но здесь показана сигнатура функции
New
, которая возвращает Graph
, и сигнатура метода ShortestPath
в Graph
.Важный вывод заключается в том, что контракт не обязательно относится к какому-то одному типу. Он может описывать взаимосвязи между двумя и больше типами.
Упорядоченные типы (Ordered types)
В Go удивительно часто жалуются на отсутствие функции
Min
. Ну, или Max
. Причина в том, что полезная функция Min
должна работать только с упорядоченными типами, то есть она должна быть дженериком.Поскольку писать
Min
самостоятельно довольно просто, любая полезная реализация дженериков должна позволять нам добавлять эту функцию в стандартную библиотеку. Вот как она выглядит в нашей архитектуре:func Min (type T Ordered) (a, b T) T {
if a < b {
return a
}
return b
}
Контракт
Ordered
говорит о том, что тип T
должен быть упорядоченным, то есть контракт поддерживает операторы вроде «меньше чем», «больше чем», и так далее.contract Ordered(T) {
T int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64,
string
}
Контракт
Ordered
— это просто список всех упорядоченных типов, определённых языком. Контракт принимает любые типы из списка, или любые именованные типы, в основе которых лежит какой-то тип из списка. По сути, любой тип можно использовать с оператором «меньше чем».Гораздо проще перечислить типы, поддерживающие оператор «меньше чем», чем изобрести новую нотацию, работающую со всеми операторами. Наконец, в Go операторы поддерживаются только встроенными типами.
Тот же подход можно использовать применительно к любому оператору, или, в более общем плане, написать контракт для любой дженерик-функции, которая должна работать со встроенными типами. Это позволяет тому, кто пишет дженерик-функцию, явно указать набор типов, с которыми должна использоваться функция. И это позволяет вызывающему дженерик-функцию ясно видеть, применима ли функция к типам, с которыми используется.
На практике этот контракт, вероятно, попадёт в стандартную библиотеку. И поэтому функция
Min
(которая, вероятно, тоже окажется в стандартной библиотеке) будет выглядеть так. Здесь мы просто ссылаемся на контракт Ordered
, определённый в контрактах пакета.func Min (type T contracts.Ordered) (a, b T) T {
if a < b {
return a
}
return b
}
Дженерик-структуры данных
Наконец, давайте посмотрим на простую дженерик-структуру данных, двоичное дерево. В этом примере дерево содержит функцию сравнения, поэтому к типам элементов не предъявляется никаких требований.
type Tree (type E) struct {
root *node(E)
compare func(E, E) int
}
type node (type E) struct {
val E
left, right *node(E)
}
Так создаётся новое двоичное дерево. Функция сравнения передаётся в функцию
New
.func New (type E) (cmp func(E, E) int) *Tree(E) {
return &Tree(E){compare: cmp}
}
Не экспортированные методы возвращают указатель либо на слот, содержащий
v
, либо на место в дереве, в которое она должна отправиться.func (t *Tree(E)) find(v E) **node(E) {
pn := &t.root
for *pn != nil {
switch cmp := t.compare(v, (*pn).val); {
case cmp < 0:
pn = &(*pn).left
case cmp > 0:
pn = &(*pn).right
default:
return pn
}
}
return pn
}
Подробности в данном случае не имеют значения, особенно в свете того, что я не тестировал этот код. Я лишь пытаюсь показать, на что похоже написание простой дженерик-структуры данных.
Этот код тестирует, содержит ли дерево значение.
func (t *Tree(E)) Contains(v E) bool {
return *t.find(e) != nil
}
This is the code for inserting a new value.
func (t *Tree(E)) Insert(v E) bool {
pn := t.find(v)
if *pn != nil {
return false
}
*pn = &node(E){val: v}
return true
}
Обратите внимание на аргумент типа
E
в типе узла. Так выглядит написание дженерик-структуры данных. Пишется так же, как обычный код на Go, за исключением того, что там и сям разбросаны аргументы типов.Пользоваться деревом просто.
var intTree = tree.New(func(a, b int) int { return a - b })
func InsertAndCheck(v int) {
intTree.Insert(v)
if !intTree.Contains(v) {
log.Fatalf("%d not found after insertion", v)
}
}
Так и должно быть. Писать дженерик-структуру данных немного сложнее, потому что зачастую нужно явно прописывать аргументы типов для поддерживаемых типов. Но максимально возможное использование такой структуры ничем не отличается от использования обычной не-дженерик-структуры данных.
Следующие шаги
Мы работаем над фактическими реализациями, которые позволяют нам экспериментировать с этой архитектурой. Важно иметь возможность испытать её на практике, чтобы быть уверенными в том, что мы можем писать такие программы, какие хотим. Работа движется не так быстро, как мы надеялись, но по мере поступления новой информации об этих реализациях мы будем делиться с вами подробностями.
Роберт Гризмер написал подготовительный CL, который модифицирует пакет go/types. Это позволяет протестировать, может ли код, использующий дженерики и контракты, выполнять проверку типов. Работа ещё не закончена, но для одного пакета эта фича по большей части работает, и мы продолжим разработку.
Нам хотелось бы, чтобы с помощью этой и последующими реализациями люди пытались писать и использовать дженерик-код, смотрели, что происходит. Мы хотим быть уверены, что люди могут писать нужный им код, и что они могут использовать его как ожидается. Конечно, сначала не всё будет работать, и по мере работы мы что-то изменим. И нам гораздо интереснее отзывы о семантике, чем о подробностях синтаксиса.
Хочу поблагодарить всех, кто комментировал предыдущую архитектуру, и всех, кто обсуждал возможный облик дженериков в Go. Мы прочитали все комментарии и очень благодарны за ваш труд. Без него мы не пришли бы к тому, к чему пришли.
Наша цель — создать архитектуру, позволяющую писать рассмотренные мной выше разновидности дженерик-кода, не усложняя язык настолько, чтобы он стал слишком сложен в использовании или уже не ощущался бы как Go. Мы надеемся, что эта архитектура — шаг к цели, и будем продолжать улучшать её, опираясь на свой и ваш опыт в том, что работает, а что нет. Если мы достигнем этой цели, то сможем что-то предложить для будущих версий Go.
Комментарии (209)
snuk182
07.08.2019 14:32+2Огромный вопрос — кому, собственно, Ян все это рассказывает? На планете вообще остался хоть кто-то, связанный с программированием, но не в курсе, что такое и зачем нужны дженерики?
sudochsh
07.08.2019 19:19+4Ну те кто работают с динамической типизацией, могут быть не в курсе.
TheShock
08.08.2019 02:44А ещё те, кто последние 5 лет кричали: «в Go не нужны дженерики, там уже есть типизированные массивы!!»
cy-ernado
08.08.2019 03:05-1Необходимость дженериков была понятна изначально.
Было несколько итераций с большими паузами.
Просто в го не хотят бездумно запиливать новые фичи, было желание сделать правильно и не наступить на грабли. Необходимость дженериков часто преувеличивают, действительно нужны они в довольно редких случаях, поэтому фокус был на другое и с ними не спешили, стараясь отполировать решение.
Кубернетис с докером прекрасно написали без дженериков, да.
TheShock
08.08.2019 03:17Может вам и была, а многим — не была.
cy-ernado
08.08.2019 03:30-1Я скорее не про себя, а про команду го и её мнение на этот счёт, да и про сообщество.
Дженерики нужны, но не настолько, чтобы бежать их пилить сломя голову – позиция, которая была очень давно и которой придерживалось большинство. "Мы не знаем, как их сейчас сделать правильно" и вот это вот всё.
Я как раз 5 лет на го и пишу, так что с моей точки зрения всё происходило примерно так, и данный пропозал – логичное продолжение работы над языком.
Может быть со стороны это действительно воспринимается иначе, особенно если быть предвзятым к этому языку.
4410
08.08.2019 10:29+2«Мы не знаем, как их сейчас сделать правильно» и вот это вот всё.
Почему-то такая логика не помешала сделать ужас с $GOPATH, а потом его починить модулями.
vitvakatu
08.08.2019 11:27+8О да, тот самый Kubernetes, которые фактически написали свои собственные generics, чтобы не страдать. А так конечно необходимость преувеличивают.
cy-ernado
08.08.2019 14:32-1Отсутствие generics не помешало Kubernetes развиться до текущего состояния.
А так конечно необходимость преувеличивают.
Не все пишут проекты уровня Kubernetes, да и проблема обычно решается другим подходом к проектированию, без попыток писать на Go как на C#, Rust или Python.
Я не погружался в то, что сделали в кодовой базе Kubernetes, так что не могу ничего сказать по этому поводу, но предполагаю, что проблему можно было решить иначе. Судя по последним изменениям вk8s.io/apimachinery
, наблюдается движения к более строгой типизации.
Пятилетний опыт коммерческой разработки на Go показал, что дженерики не нужны для большинства решаемых задач (но это не значит, что с ними в некоторых местах было бы удобнее и проще). Но вам, конечно, виднее.
siziyman
08.08.2019 11:29+2Как у человека, который сейчас активно пишет на го, у меня складывается впечатление, что Роб Пайк сотоварищи находились в криогенном сне последние 25 лет.
Иначе то, что они выкатили на-гора в конце нулевых статически типизированный язык без дженериков и с обработкой ошибок, сделанной хуже, чем в, прости господи, Си, объяснить мне кажется сложным.
И да, без них больно и неприятно. Без человеческой обработки ошибок ещё хуже, конечно.snuk182
08.08.2019 11:37Вот как раз с обработкой ошибок, очень субъективно, дела идут прекрасно с точки зрения языковых конструкций. Ровно до того момента, когда надо проверять, не игнорируется ли ошибка (а именно в этом корень всех претензий). Я тогда предлагал в компилятор просто повесить проверку, не игнорируется ли из любой функции возвращаемый параметр типа
Error
. Да, привет утиной типизации, с которой это труднореализуемо. Да, вопрос тонны уже написанного кода при этом открыт.siziyman
08.08.2019 12:03+1Нет-нет-нет, проблема даже не в этом.
Умолчания языка с точки зрения обработки ошибок абсолютно ужасны, потому что у вас ошибки по умолчанию (в стандартной библиотеке в том числе) из каких-то проверяемых хотя бы теоретически значений возвращают только строчку с текстом. А строчка с текстом зависит не то что от ОС, а от локализации, и от кучи чего ещё. У вас нет нормального способа обработать ошибку, потому что вы знаете только "… ну что-то пошло не так". Чтобы содержательно реагировать на ошибку, эту ошибку надо уметь отличать от какой-то другой ошибки.
Файл не найден — одно. К файлу нет доступа — другое. Что-то третье — ну, оно третье.
Они теперь, конечно, пытаются это как-то исправить, но да, есть тонна написанного уже кода, который никто не перепишет, и сконкатенированные четыре сообщения об ошибке с тремя двоеточиями (это когда кто-то четырежды написал if err != nil, из них три раза это обёртки своего кода, и только четвёртый по вложенности вызов содержит, собственно, проблему и её описание, а обработать это нельзя, потому тексты просто склеиваются и логируются) будут сниться вам ещё 10 лет.snuk182
08.08.2019 12:08Поэтому и было предложение контролировать только возвращаемые значения, которые
имеют типкастятся к error + впилить какой-то ключ для легаси-кода, который позволяет понижать ошибки игнорирования к предупреждениям.siziyman
08.08.2019 12:11+1Ну да, только проблему всего существующего кода это уже не решает, действительно, и, кажется, это грустно.
Ключи го не любит, мне кажется.
Вон, даже неиспользуемые импорты и переменные им ни-ни. А в ходе разработки это болезненно — хочу я протестить небольшой кусок кода, закомментировал использование переменной или вызов внешнего пакета исгореллезу убирать импорт, заменять присваивание переменной на _, и тем самым трачу время на удовлетворение синдрома вахтёра компилятора.Denis631
10.08.2019 23:40да ладно вам, убирать неиспользованные импорты не надо. Вроде есть Тулы (не знаю если gofmt это делает), который убирает или пересобирает импорты, так что проблема будет только в переменной
maxxant
08.08.2019 13:47+2> складывается впечатление, что Роб Пайк сотоварищи находились в криогенном сне последние 25 лет.
Моё субьективное после плюсов, совершенно обратное. Затащить фич, вообще не проблема — просто потом боротся с ними тяжко. Более 16 ГБ для линковки, 40 минут билда на многоядерных ксеонах и SSD-ях в CI, разные ccache у разработчиков, эпизодические работы по ускорению билдов. Вообщем я рад что некоторые вещи можно перетащить на go.siziyman
08.08.2019 13:49Ну плюсы — пример ещё более плохого языкового дизайна, но у них хотя бы есть оправдание (легаси, обратная совместимость...), Пайк же сразу начал с плохого.
Ну и да, языковые фичи сами по себе совершенно необязательно транслируются в увеличение времени билда и затрат памяти.
webkumo
08.08.2019 17:40Плюсы собираются по 40 минут и жрут память как не в себя не из-за фич, а из-за "особенностей процесса". Java на тот же объём фич и кода затрачивает на пару порядков меньше времени и на порядок-два меньше памяти. Потому-что при создании языка этот момент хоть чуть-чуть, но продумали. И упростили. И да, те же дженерики — почти бесплатные с точки зрения компиляции и совсем бесплатные в рантайме (впрочем в рантайме их по сути-то и нет).
Denis631
10.08.2019 23:45+1на мой взгляд вы не знаете о чем говорите. Тэмплэйты в плюсах это вообще не дженерики. Да о чем говорить если даже тэмплэйты Turing complete, только variadic templates чего стоят. У джава тот же объем фич. Ну ну
Shadow0fClown
08.08.2019 11:34+2Только вот маленькая ремарка. Им пришлось накостылять свои собственные «дженерики» для решения их задач. А так да. Написано без дженериков.
zuborg
07.08.2019 15:24Все выглядит вполне приятно. Интересно, есть ли какие-то минусы — лично я сходу не нашел.
Sly_tom_cat
07.08.2019 16:31Минусы всплываю не быстро.
Вон сколько времени try обсуждали и таки позже насыпали таких минусов, что тему закрыли.
Тут ведь важно как это еще тот же дебаггер будет отрабатывать и прочие мелочи. ИМХО именно дебаггинг и code coverage в тестах похоронили try.snuk182
07.08.2019 17:22Претензии к
try
всплыли моментально — странные неявные манипуляции с переменной ошибки только ленивый не заметил.Sly_tom_cat
07.08.2019 17:50Основные претензии — да всплыли моментально, но именно накопление всего разнообразия минусов сыграло роль в финале (на мой взгляд).
loltrol
07.08.2019 17:25А мне кажется выстрелит. Т.е. по сути может у них получится что то типа с++ темплейтов, где для каждой отдельной комбинации дженерик аргументов будет за кулисами сгенерировано отдельную функцию(т.е ReverseAndPrint(int)() и ReverseAndPrint(map[string]string) будут двумя разными методами). С дебаггингом тут будет попроще, чем с try.
Vadem
07.08.2019 18:29Ну уже появляются статьи с критикой.
Вот эта например — My Case Against the Contracts Proposal for Go.
Я с ней не согласен, но почитать интересно.snuk182
07.08.2019 18:38Да там такое впечатление, что человек читал спеку наспех по диагонали.
Vadem
07.08.2019 18:47Ну да. Статья так себе, но надеюсь, что за ней последуют и другие.
Важно чтобы пропозал обсуждали и критикивали, иначе есть опасность не заметить серьезных недостатков.
Я считаю, что лучше никаких дженериков чем плохие.
Хотя данный пропозал на первый взгляд выглядид неплохо.
vintage
09.08.2019 07:00Вас не смущает, что Ordered является просто перечислением возможных типов и это считается нормальным?
DmitryKoterov
07.08.2019 21:13+3Такое ощущение, что предлагается использовать скобки () вместо <>, как в других языках, по каким-то религиозным соображениям.
defuz
07.08.2019 23:54+5«Треугольные» кавычки (<>), в отличии от настоящих кавычек ([]{}) являются еще и бинарными операторами сравнения, из-за чего не обязаны быть парными в валидном коде. Это мешает нормальному здорову сну разработчиков парсеров.
Вот вам пример из раста, который хотел выглядеть как C++:
if a as u32 < 5 { ... }
Ответ заботливого компилятораerror: `<` is interpreted as a start of generic arguments for `u32`, not a comparison --> src/main.rs:2:17 | 2 | if a as u32 < 5 {} | -------- ^ --- interpreted as generic arguments | | | | | not interpreted as comparison | help: try comparing the cast value: `(a as u32)`
TheShock
08.08.2019 02:47-3Выглядит, как гниль в спецификации языка.
Какой-то C# даже близко не имеет таких проблем.defuz
08.08.2019 03:16+10Извините, кажется у вас C# прогнил:
Будете учить новый язык? Не на гнилом же писать…TheShock
08.08.2019 03:20-2Эмс… а что этот код вообще значит? Кажется, я неправильно вас понял. Вы хотите писать ничего не значащий код и ждете, чтобы он заработал. Простите, не понимаю ваш пример
defuz
08.08.2019 03:26+5Что не понятного? Так работает:
bool b = (f as F) < a;
А так не работает, потому что парсер решил что я пытаюсь задать параметризацию типа:
bool b = f as F < a;
А так снова заработало:
bool b = f as F > a;
netch80
09.08.2019 08:42Это из спека C# 4-й версии, но более поздние имеют то же самое по сути:
If a sequence of tokens can be parsed (in context) as a simple-name (§7.6.2), member-access (§7.6.4), or pointer-member-access (§18.5.2) ending with a type-argument-list (§4.4.1), the token immediately following the closing > token is examined. If it is one of
( ) ] }:;,.? == != | ^
then the type-argument-list is retained as part of the simple-name, member-access or pointer-member-access and any other possible parse of the sequence of tokens is discarded. Otherwise, the type-argument-list is not considered to be part of the simple-name, member-access or pointer-member-access, even if there is no other possible parse of the sequence of tokens. Note that these rules are not applied when parsing a type-argument-list in a namespace-or-type-name (§3.8). The statement
F(G<A,B>(7));
will, according to this rule, be interpreted as a call to F with one argument, which is a call to a generic method G with two type arguments and one regular argument. The statements
F(G < A, B > 7);
F(G < A, B >> 7);
will each be interpreted as a call to F with two arguments. The statement
x = F < A > +y;
will be interpreted as a less than operator, greater than operator, and unary plus operator, as if the statement had been written x = (F < A) > (+y), instead of as a simple-name with a type-argument-list followed by a binary plus operator. In the statement
x = y is C<T> + z;
the tokens C<T> are interpreted as a namespace-or-type-name with a type-argument-list.
Эта диверсия с <> — она везде имеет похожие последствия в неоднозначности, так что непонятно, зачем было её вообще размножать.
lany
08.08.2019 07:10Ещё не забудьте бинарный сдвиг >>, который должен закрывать вложенный дженерик. В старых плюсах обязательно было пробел ставить в середине.
cy-ernado
08.08.2019 02:56+1Простота парсера и сохранение привычного для го синтаксиса это не религиозные соображения.
Для человека, который достаточно долго пишет на го, параметризация через скобки выглядит привычнее.
Просто так делать "как в других языках" не получится.
taliban
07.08.2019 21:30А вот что может удивить новичков в Go, так это отсутствие способа написать простую функцию Reverse, работающую со слайсами любых типов.
Можно ведь написать функцию которая работает со слайсом любого типа, что вы обманываете новичков?HEKET313
08.08.2019 07:21+1Как? Через interface{} с последующим использованием Reflect? Этот способ в статье описан. Если вы про другой, то можно пример?
blind_oracle
09.08.2019 13:50Нельзя. []interface{} это отдельный тип и к, допустим, []int он никак не кастится.
taliban
09.08.2019 14:47-3так в примере то было пофигу какой тип, там просто происходила работа со слайсом «чего-то»
blind_oracle
09.08.2019 16:36+1И что? Как юзать такую функцию?
Если сделать func(s []interface{}) то передать в нее []int нельзя.
Если сделать func(s interface{}) то внутри нужно делать reflect или type switch для каждого поддерживаемого типа.TheShock
09.08.2019 16:39Можно использовать функцию
CastIntArrayToInterfaceArray
, а потом, на том результате, который вернулся — использовать функциюCastInterfaceArrayToIntArray
. Я, кстати, видел такое в реальном коде на Гоуblind_oracle
09.08.2019 16:45Ужос какой. Уже кровь из глаз пошла :)
Слава богу у меня в реальных проектах нужда в дженериках не настолько сильно возникала… Да, я писал пару ReverseXXX, но не более того.
taliban
09.08.2019 16:43Да, я затупил, постоянно забываю про слайс интерфейсов ибо не использую такое, согласен, нельзя.
Gorthauer87
07.08.2019 23:29+2Вот почему нельзя было интерфейсы переиспользовать вместо контрактов? По сути они об одном и том же, разница лишь в диспетчеризации.
В Rust трейты можно и как динамические интерфейсы использовать и как контракты в генерик методах. В swift протоколы, как я понимаю аналогично работают.
А тут новая сущность вводится.tendium
08.08.2019 00:16+1В отличие от дженериков, которые будут определяться в compile-time, интерфейсы будут определяться в run-time. А из этого следует:
— интерфейсы приходят не бесплатно
— интерфейсы могут быть источником неявных ошибок
И как бонус: в IDE в функцию, в сигнатуре которой есть интерфейс(ы), можно передать что угодно, ошибка не высветится.taliban
08.08.2019 00:59-1вообще-то высветится, подобие интерфейса проверяется на этапе компиляции
пруф: play.golang.org/p/USNSdEWJTm5
Но тем не менее контракты довольно крутая штука, было бы прикольно как типы их использовать вне концепции дженериковtendium
08.08.2019 01:45Ну и какое отношение имеет это к дженерикам? Речь-то об этом:
func dummy(q interface{}) { // здесь детект типа, и если тип не поддерживается, то вызываем panic() }
Ясно, что ни о каком compile-time речи не идёт. Тип будет определяться в рантайме. И подсветить неподдерживаемый тип IDE не сможет (если только кто-то, не дай бог, не додумается вводить в go аннотации в комментариях, а-ля PHP).
При этом опять же очевидно, что если код слабо покрыт тестами, то не исключено, что где-то в функцию может быть передано значение с неподдерживаемым типом. И всё, привет panic.taliban
08.08.2019 01:58-1зачем же пустой интерфейс использовать? их же не для того добавили. В го просто контракты как типы и без дженериков уже была бы крутой фичей. Дженерики по большей степени не нужны.
TheShock
08.08.2019 02:50+1Дженерики по большей степени не нужны
Ну и как в Go написать, к примеру, map/filter/reduce?taliban
08.08.2019 10:53Через анонимную функцию, внезапно
siziyman
08.08.2019 11:32Правильно, давайте заставлять программистов писать в 2 раза больше бойлерплейта и периодически класть болт на type safety в статически типизированном языке. :)
taliban
08.08.2019 11:47+1Чтож там у вас за бойлерплейт такой? или у вас содержимое мап функций всегда повторяется? Чета я не верю, в большинстве случаев даже сигнатура не схожа. Да и в принципе, написать лишних 20 символов это очень большая нагрузка на бедных программистов. Вы уже написали только что кучу бойлерплейта, ну что, сильно устали? Вы наверно так устали что следующий камент будет через час минимум. А вы знали что мап/редьюс можно вообще без функций писать?
Если вам так нужны дженерики, почему не выбираете язык где они есть c#/f#/rust/typescript? Зачем вы мучаетесь с го делая тонны лишнего бойлерплейта?siziyman
08.08.2019 12:04Там, где выбор за мной, я действительно выбираю не го и рекомендую не го. Но, внезапно, компания, где я рядовой сотрудник, под меня не подстраивается (и устраивался я туда не го-девелопером, да, так вышло).
snuk182
08.08.2019 13:30Вы уже написали только что кучу бойлерплейта, ну что, сильно устали?
Дело же не в усталости, дело в вручную написанном (в худшем случае скопипащенном неизвестно откуда) коде. Больше ручного кода — больше шансов на механическую ошибку. При этом нельзя перегибать палку, как в скриптовых языках, толерантных к приведениям типов — теряется контроль за данными в рантайме.
taliban
08.08.2019 13:59-1ну я как бы стараюсь не использовать interface{} в своем коде, если использовать интерфейсы так по назначению, сделать нормальный метод с которым будем работать, сделать функцию которая его принимает, и с ним работает, и дальше добавляйте метод любому типу с которым работаете чтоб функция могла с ним работать. Неужели это так сложно? Не будет никаких ошибок, код будет гибкий и универсальный. Вы не будете привязаны ни к какому типу, и на этапе компиляции будут проверяться все ошибки.
У меня нет функций (может я не правильно пишу) которые должны работать с разными типами, неужели сигнатура и содержимое двух фильтрующих функций для разных обьектов будут совпадать?
Да, можно вспомнить старую забавную гифку где пишут функцию для работы с интом которая превращается в 8 из-за разных типов, но это частный случай, в большинстве случаев нет такого кода, практически всегда вы знаете точно с каким кодом работаете и можете их явно указать. Я за все время работы с го (а это не один месяц) только один раз столкнулся с функцией которая должна была работать аж с 2 типами, ибо функционал был похож.
Denis631
10.08.2019 23:52Если вам так нужны дженерики, почему не выбираете язык где они есть c#/f#/rust/typescript? Зачем вы мучаетесь с го делая тонны лишнего бойлерплейта?
потому что это требует рынок, не потому что мы этого хотим (на F# или Rust нету проектов, работы как на Go)
TheShock
08.08.2019 15:12Через анонимную функцию, внезапно
Да? Расскажите мне, пожалуйста, как это сделать. Только так, чтобы:
1. Я мог написать функцию под любой массив. Чтобы не нужно было под каждый тип копипастить код
2. В filter и map возвращался массив того же типа, что я передал
3. Чтобы анонимные функции принимали только тот аргумент, который я передал. Чтобы нельзя было передать функцию, которая принимает строку в качестве аргумента вместе с массивом интомtaliban
08.08.2019 15:24Ухты, а напишите мне функцию с дженериком чтоб:
[{a:1}, {a:2}, {a:3}]
[{b:1}, {b:2}, {b:3}]
[1,2,3]
эти 3 массива можно было фильтровать или мапом пройтись. Вы же под любой массив хотите?
И как вы собираетесь работать с универсальной мап функцией, когда работаете с разными данными?
Вы ведь понимаете что анонимная функция — функция которую вы только что написали? Мап это не обязательно метод, это еще может быть hoc функция, просто код без функции.
Или это очень большая проблема написать лишних 20 символов чтоб типы указать?
Или это большая проблема указать интерфейс который реализует явно методы для перебора или фильтров?
Вы не в курсе как работать с интерфейсами в го? Не всегда нужен тип, а иногда лучше когда его нет.
Приведите мне пример массивов с которыми вам нужно работать, а не просто набор миллиона типов для того чтоб меня озадачить, и я дам вам реализацию.TheShock
08.08.2019 15:38И как вы собираетесь работать с универсальной мап функцией, когда работаете с разными данными?
Элементарно на любом языке с дженериками.
Вы ведь понимаете что анонимная функция — функция которую вы только что написали? Мап это не обязательно метод, это еще может быть hoc функция, просто код без функции.
Да, понимаю.
Или это очень большая проблема написать лишних 20 символов чтоб типы указать?
1. Да, проблема
2. В Гоу вам 20 символов не поможет, надо под каждый тип создавать свою функцию
Или это большая проблема указать интерфейс который реализует явно методы для перебора или фильтров?
Это не поможет написать универсальные фильтр и мап.
Вы не в курсе как работать с интерфейсами в го? Не всегда нужен тип, а иногда лучше когда его нет.
В курсе, я писал на нем полгода, потому знаю, какое он говно.
Приведите мне пример массивов с которыми вам нужно работать, а не просто набор миллиона типов для того чтоб меня озадачить, и я дам вам реализацию.
Ваши два массива меня устраивают. А лучше давайте 5 разных массивов.
[{a:1}, {a:2}, {a:3}] [{b:1}, {b:2}, {b:3}] [{c:1}, {c:2}, {c:3}] [{d:1}, {d:2}, {d:3}] [{e:1}, {e:2}, {e:3}] [{f:1}, {f:2}, {f:3}]
Ухты, а напишите мне функцию с дженериком чтоб:
[{a:1}, {a:2}, {a:3}]
[{b:1}, {b:2}, {b:3}]
[1,2,3]
Запросто, на любом адекватном языке. Так будет на TS:
type A = { a: number }; type B = { b: number }; var aArr = [{a:1}, {a:2}, {a:3}] as A[]; var bArr = [{b:1}, {b:2}, {b:3}] as B[]; function myMap<TIn, TOut> (arr: TIn[], cb: (item: TIn) => TOut): TOut[] { var result: TOut[] = []; for (var it of arr) { result.push( cb(it) ); } return result; } function myFilter<T> (arr: T[], cb: (item: T) => boolean): T[] { var result: T[] = []; for (var it of arr) { if (cb(it)) { result.push( it ); } } return result; } console.log( myMap(aArr, it => it.a) ); // [ 1, 2, 3 ] console.log( myMap(bArr, it => it.b) ); // [ 1, 2, 3 ] console.log( myFilter(aArr, it => it.a == 2) ); // [ { a: 2 } ] console.log( myFilter(bArr, it => it.b == 2) ); // [ { b: 2 } ]
Могу такой же написать, к примеру, на C#TheShock
08.08.2019 15:49Ну и возвращается не только корректные данные, но и тип правильный, который далее можно использовать:
taliban
08.08.2019 15:51И вот теперь главный вопрос: а почему ж вы пытаетесь все это затащить в го? Вы ж пишете на ТС или С#?
в го вот этот кусок it => it.a == 2 уйдет в реализацию интерфейса для типа с которым работаешь. Интерфейс с одним методом который фильтрует по полю.
Я ща прям предвижу ответ «но мне же надо 100500 разных типов так фильтровать» или «но мне же надо 100500 разных фильтров». И сразу отвечу, нет, это вы ща синтетику пытаетесь пропихнуть чтоб показать как в го это сильно сложней делается. И я не говорю что в го это супер элементарно и просто. Другое дело что в реальном коде я такого почему-то не встречал, странно правда? Или у вас реально приходят 100500 разных данных в фильтр и вы используете 100500 разных фильтрах на этих данных? Я даже на тс до такого не дохожу.
Не надо делать из го шарпы, пишите на шарпе. в большинстве случаев вы такой код пишете только на хабре чтоб что-то кому-то доказать.
И в итоге нет, 20 символов не проблема, и вам не нужно «для всех» типов писать эти 20 символов, не надо глобализацию устраивать, вы прекрасно знаете что «для всех» ничего не нужно делать.TheShock
08.08.2019 15:52Так покажите пример.
taliban
08.08.2019 15:56-2Это вы ждете что я напишу для каждого типа реализацию и вы скажете «ага, я же говорил, а теперь еще тип добавьте и будет копипаст», да, так и будет. и?
TheShock
08.08.2019 15:58Значит, универсальные map и filter написать нельзя?
taliban
08.08.2019 16:02«it.a == 2» — вот эта строка будет копироваться для каждого типа с которым я работаю. Можно ли считать функцию которая принимает что-то попадающее под сигнатуру и работает с ним вне зависимости от типа универсальной?
TheShock
08.08.2019 16:03Я вас не понимаю, вы можете написать пример кода? Вы хотите в качестве фильтра пользовать строку?
taliban
08.08.2019 17:07Нет, не строку, я хотел отдать туда массив интерфейсов.
Но дело взяло неожиданный поворот.
Беру свои слова, ибо я в очередной раз вспомнил про одну особенность в го, которую я успешно про*бал когда с вами общался.
В идеале все должно было выглядеть так: play.golang.org/p/yG5SP2NaHXR но дело в том что го работает с массивами интерфейсов через одно место, и в итоге то что я говорил не будет работать. :)TheShock
08.08.2019 17:11Ну от идеала очень далеко. Вам необходимо в таком случае дублировать код не только для разных типов, но и для разных колбеков, по которым вы делаете фильтр. По сути вы вообще не написали функцию filter, а каждый раз придётся писать её полностью с нуля.
taliban
09.08.2019 16:04Сколько у вас в текущем проекте функций filter? Я согласен с дублированием и уже взял свои слова назад. Мне просто интересно на сколько часто вам нужна такая функция. Я просто ща подумал что за последние 4 года мне такая функция понадобилась от силы раз 5, это я беру в учет 4 последние проекта. Мап/редьюс и того меньше. И все эти 4 раза работали с парочкой типов данных от силы.
Может я со своей колокольни по незнанию так сопротивляюсь, а в других местах они часто используются.TheShock
09.08.2019 16:31У меня в проекте дженерики используются очень часто. Много вещей в гоу стали бы лучше, если бы были дженерики. Да, везде можно закопипастить +3 строки, +2 строки. Этой всей копипастой в итоге код на Гоу и выглядит является таким говнокодистым.
Ну вот, к примеру, JSON:
var bird Bird json.Unmarshal( []byte(birdJson), &bird )
vs
bird := json.Unmarshal<Bird>( []byte(birdJson) )
Ну или, допустим, пойдем на уровень выше с этим JSON. Мы хотим, чтобы наш Rest API фреймворк сам занимался обработкой ошибок некорретного JSON. С Дженериками можно было бы написать что-то такое:
fw.Get<IdJson>("/book", func (json IdJson) { return Render(GetBook(json.id)); }) fw.Post<BookJson>("/book", func (json BookJson) { SaveBook(json) return Ok(); })
Без дженериков такой-же код выглядит как-то так:
fw.Get("/book", func (c fw.Context) { var idJson IdJson error := json.Unmarshal( []byte(c.body), &idJson ) if error { return RenderError(error); } return Render(GetBook(idJson.id)); }) fw.Post("/book", func (c fw.Context) { var bookJson BookJson error := json.Unmarshal( []byte(c.body), &bookJson ) if error { return RenderError(error); } SaveBook(bookJson) return Ok(); })
Тот же код, но теперь тьма ненужной копипасты в каждом эндпоинте и куча мест для ошибки.taliban
09.08.2019 16:56в первом примере вы забыли что анмаршал ошибку возвращает и это важный момент, пример мне не нравится, разница в пару символов не влияет на «это же короче писать», а читабельность не улучшается, ибо текущий вариант очень понятный сходу.
А второй вариант уже успешно работает, такое можно сделать (да, рефлексия или кодогенерация увы, знаю что у всех аллергия на эти слова).
Второй вариант лучше, но вот проблема, вам ведь контекст нужен будет все равно? А если данные не жсон? Опять же пример ради примера, просто потому что вы работаете конкретно с жсон и лично вам было бы удобней так.
Да, в го хватает копипасты, кое где дженерики ее уберут, но этих мест будет очень мало. Меня больше пугает что их начнут использовать везде, там где надо, там где не надо или просто потому что могут, и это модно стильно молодежно. Новая фича и ее обязательно нужно сунуть везде где ни попадя. И в итоге боли будет больше чем пользы.anjensan
09.08.2019 17:10+1Меня больше пугает что
ихего начнут использовать везде, там где надо, там где не надо или просто потому что могут, и это модно стильно молодежно.Это вы щас историю успеха Go описали?
Не переживайте, наговнокодить можно и сейчас, было бы желание. Врядли ситация кардинально поменяется.
taliban
09.08.2019 17:26-2Какой остроумный ответ, у вас такое хорошее чувство юмора, я оценил. Подскажите где не стоит использовать го, чтоб я случайно не полез куда не нужно?
TheShock
09.08.2019 18:55вам ведь контекст нужен будет все равно
С Дженериками то? Совсем не проблема!
fw.Get("/book", func (context JsonContext<IdJson>) { return Render(GetBook(context.json.id)); })
А если данные не жсон?
Это же фреймворк. Ну будет тогда у нас XmlContext. Как авторы фреймворка решат — так и будет. А сейчас у них вообще возможности такой нету.
да, рефлексия
Да? И как при помощи рефлексии это сделать, покажите мне. Быдлокодогенерацией — да, можно, конечно.
просто потому что вы работаете конкретно с жсон и лично вам было бы удобней так.
Ну конкретно я работаю с JSON. А кто-то другой работает конкретно с XML. Третий работает с пейджингом, который может разбивать на страницы, а дженериком указывает, какой именно тип является ячейкой этой страницы. Как результат — код на Гоу вызывает аллергию у всех людей, у которых есть аллергия на быдлокод.
И в итоге боли будет больше чем пользы.
А разве это не идеология Гоу? «Боли больше чем пользы!»
anjensan
09.08.2019 15:48Простите, но ваше "в идеале" вяглядит очень "не очень".
Про то ведь и речь, что по нормальному с джерериками все могло бы выглядеть наподобие https://play.golang.org/p/jJLl_CIs_1R
Никаких левых структур, никакихsInt
, нужная логика в одном месте (в анонимной функции).
PS: особо хорош способ возвращения слайса
to
из методаfilter
;)taliban
09.08.2019 15:58А еще можно просто цикл написать, там всего 3 строки, вместо того чтоб делать функцию которая понадобится от силы 3 раза, добавить ее в какой-то пакет непонятный (привет «utils») про который успешно забудет кто-то в большом проекте и возможно будет две одинаковые функции которые делают одно и то же 3 раза в лучшем случае.
Мое «в идеале» выглядит так же как и бесполезная функция «ради дженериков», но ваш код хоть работать будет в отличии от моего )anjensan
09.08.2019 16:13Ну это если функция фильтрует слайсы, то она
"бесполезная". Ну гляньте на примеры из статьи https://blog.golang.org/pipelines, например функциюmerge
. Как же весло, когда нельзя такую функцию добавить в непонятный пакет, а нужно копипастить по большому проекту!
Мне кажется ваша логика сломалась: с дженериками будет копипаста из-за того, что люди забудут об существующей реализации — это плохо, но без дженериков будет еще больше копипасты — это типо хорошо.
taliban
09.08.2019 16:17Так говоришь как буд-то твой проект только и делает что фильтрует то да се, а затем делает мап/редьюс и после этого добавляет в массив результат который снова надо фильтровать и поехали заново по кругу. Я выше написал и лично я не могу вспомнить где мне реально понадобилась функция которая заменяет 3 строки кода. В го обработка ошибок только занимает 3 строки кода и делается через каждые 5 минут, это никого не волнует, а написать пару раз фильтр на 3 строки за проект это ужасно много и надо обязательно вынести в функцию в пакет, и сделать универсальной ради пары раз использований.
anjensan
09.08.2019 16:29это никого не волнует
Это не волнует фанатиков и дураков. Так то народ постоянно стонет и ругается на обработку ошибок в Го (точнее на ее отсутствие). Блин, этож самая популярная тема похейтить сей яызк :D
Я выше написал и лично я не могу вспомнить где мне реально понадобилась функция которая заменяет 3 строки кода.
Ну во-первых, тут три строки. Там три строки… Потихоньку набегает. А во-вторых — а зачем так привязываться к
filter
? Ведь можно поговорить о более удачных примерах из оригинального пропозала
Там приводят
func Keys(m map[K]V) []K func Uniq(<-chan T) <-chan T func Merge(chans ...<-chan T) <-chan T func SortSlice(data []T, less func(x, y T) bool)
Неужели ни разу не понадобилась фунция
Keys
?
а написать пару раз фильтр на 3 строки за проект
А, понял. Проекты просто махонькие. Ну тогда норм.
taliban
09.08.2019 16:35-1Вот пропозал с ошибками отменили а дженерики добавить собираются.
Нет, мне ни разу не понадобилась функция keys. Я редко работаю с мапами, в основном структуры. Я стараюсь писать код так чтоб не было неоднозначности, без пустых интерфейсов и милиона разных типов, если мне надо работать с int и int64 я выберу int64 для обоих вариантов итд. Да, я понимаю что дженерики кое где упростят жизнь, я пришел в го из TypeScript так что о дженериках не по наслышке знаю, но в го мне они, внезапно, перестали быть нужны. Ибо в 99% я точно знаю с чем работаю, либо у меня интерфейс который делает только то что умеет и больше мне от него ничего не нужно.
зы: нет, не махонькие, к слову.
tendium
09.08.2019 17:10го работает с массивами интерфейсов через одно место
С массивами или слайсами?
tendium
08.08.2019 08:47А для чего были добавлены интерфейсы? Нет, понятно, что есть еще и юз.кейс а-ля fmt.Printf, но кто вам сказал, что нельзя использовать интерфейсы для эмуляции дженериков?
Вот, кстати, пример из стандартной библиотеки:
type ValueConverter interface { // ConvertValue converts a value to a driver Value. ConvertValue(v interface{}) (Value, error) }
Тоже с интерфейсом в качестве параметра.taliban
08.08.2019 11:34Я ведь не говорил что нельзя использовать интерфейсы. Только вот есть ли у вас хоть одна функция которая должна принимать на вход «абсолютно любой тип»? если нет, а я уверен что нет, тогда не надо приводить в пример такое.
В го в большинстве случаев вы используете типы явно, если вам нужно что-то универсальное, вы можете написать функцию с интерфейсом для универсальной работы (не пустым интерфейсом а нормальным), и затем после работы этой фцнкции привести свои данные назад в нужный тип.
Вы ведь точно знаете с чем работаете, или нет?
Или вы из тех людей которые используют дженерики где можно и где нельзя просто потому что «я могу»?snuk182
08.08.2019 11:52+1Вы ведь точно знаете с чем работаете, или нет?
Вы писали что-то сложнее простой утилитки когда-нибудь?
Не знать конечный тип даже в более строго типизированных языках — сплошь и рядом. От откровенной херни спасают только контракты (дженерики, интерфейсы). В Go в данный момент это карнавал непредсказуемости.taliban
08.08.2019 14:05А вы когда нибудь писали код который работает с интерфейсами вместо типов? Внезапно в приложении сложней простой утилитки нужно отвязываться от типов и полагаться на интерфейсы, тогда и большинство проблем исчезнет само собой. Вам просто не важно будет с каким типом вы работаете. Люди в ентерпрайз сегменте так делают с незапамятных времен, специально чтоб делать приложения лучше. И внезапно это стало плохо.
snuk182
08.08.2019 14:13Внезапно в приложении сложней простой утилитки нужно отвязываться от типов и полагаться на интерфейсы, тогда и большинство проблем исчезнет само собой.
Вы противопоставляете динамическую диспетчеризацию статической, в то время как они должны сосуществовать. Люди в энтерпрайз сегменте уже досыта наелись ошибками в проде, которых можно было бы избежать, переложив на подгонку типов при компиляции то, что делалось в рантайме. Как пример, боль при переходе с Java 1.4 на 1.5 была невыносимой, но даже крупный и ленивый энтерпрайз рано или поздно в это ввязывался.
taliban
08.08.2019 14:20А причем тут рантайм? все ошибки проверяются на этапе компиляции при работе с интерфейсами. interface{} — это не работа с интерфейсами, это отстой. Нормальный интерфейс реализует действие. И вы можете принять только те типы которые это действие могу делать. Это нормальная ситуация, это минимум ошибок, это простой и понятный код и это прекрасное разделение реализации от вида. Лисков об этом много говорила.
Я прекрасно понимаю что от типов отказаться полностью нельзя и в большинстве случаев (я выше писал) вы работаете с типами которые 100% вы знаете. Но в остальных случаях интерфейсы (нормальные а не пустые) заменяют дженерики практически полностью. В моем случае я лишь единожды столкнулся с ситуацией где мне нужны были дженерики, это очень маленький процент всего кода и в математике такие величины игнорируются, в тот раз мне было плохо, но я пережил.snuk182
08.08.2019 14:27Рантайм будет причем, когда упадет на проде. И если вы знаете, что использовать interface{} не самая лучшая идея, то тот, кто будет ваш код использовать, может этого не знать.
tendium
08.08.2019 17:56+1Только вот есть ли у вас хоть одна функция которая должна принимать на вход «абсолютно любой тип»
В том-то и дело, что я НЕ хочу принимать абсолютно любой тип. Но в отсутствие дженериков можно сделать только так, а затем в рантайме проверить, что прислали.
И, кстати, в статье приводится отличный пример на счет Min/Max. Сейчас это сделано с тем, что math.Max() принимает float64. Т.е. все остальные типы вас вынуждают приводить к float64. Надо ли объяснять, что это не эффективно? Типа, если вам нужно эффективно, то пишите свою функцию Max? А всё из-за отсутствия дженериков…
Gorthauer87
08.08.2019 08:20Так если подставить интерфейс в том месте, где подставляется контракт, то его можно будет в compile time посчитать.
Да и в конце концов, обобщения все равно ломают совместимость, можно было бы и интерфейсы доработать.tendium
08.08.2019 08:59Так если подставить интерфейс в том месте, где подставляется контракт, то его можно будет в compile time посчитать.
И как это сделать?
обобщения все равно ломают совместимость
А как ломается совместимость? Какой старый код не будет работать?Gorthauer87
08.08.2019 09:13А посмотреть как в других языках эту проблему решили? Просто пишешь, что у тебя аргумент это обобщенный тип, реализующий определенные интерфейсы. В предложении тоже самое сказано про контракты, в этом смысле они просто дублируют интерфейсы.
Я уже молчу про то, что например для сравнения в контракте нужно исчерпывающий список типов писать, а как добавить новые типы в контракт сравнения не понятно.
В этой новости не было про обратную совместимость, но я помню было обсуждение про go 2.0 и там вроде бы решили, что можно ее немного и сломатьqrKot
08.08.2019 10:49А посмотреть как в других языках эту проблему решили?
По разному. Где-то шаблонами в compile-time (C++, бесплатные абстракции и все вот это), где-то резолвом в рантайме (привет Java и type-erasure).
«Нельзя просто так вот взять и перенести в свой язык фичу из другого языка». Ну или можно, но это PHP получится.
В предложении тоже самое сказано про контракты, в этом смысле они просто дублируют интерфейсы.
В этом смысле дублируют, в других — нет. Просто контракт и интерфейс — разные сущности. Интерфейс в рантайме существует, а контракт — сущность сугубо compile-time'овая, хинт для компилятора, а потому в рантайме бесплатная.
Я уже молчу про то, что например для сравнения в контракте нужно исчерпывающий список типов писать
Ну, например, не обязательно. Это просто один из примеров того, что можно сделать. В данном конкретном примере объясняется, что перечислить список скалярных типов языка проще, чем изобретать костыль для определения, есть ли определенный над типом оператор, не больше и не меньше.
В других случаях вы имеете синтаксис, схожий с интерфейсами, в котором вам нужно просто тупо в лоб перечислить список операций/полей, которые нужны типу, чтобы он пролазил в вашу конструкцию.
Я уже молчу про то, что например для сравнения в контракте нужно исчерпывающий список типов писать,
Вы невнимательно прочли. Даже в случае скалярных типов сказано, что этот список будет в стандартной библиотеке, его писать не нужно будет, можно будет просто использовать.
а как добавить новые типы в контракт сравнения не понятно.
Через запятую же)))
В этой новости не было про обратную совместимость, но я помню было обсуждение про go 2.0 и там вроде бы решили, что можно ее немного и сломать
Не было потому, что не поломали, очевидно. Ваш старый код без контрактов и дженериков будет без проблем собираться самым свежим компилятором, т.к. уже имеющиеся конструкции не изменились.
Поломать собирались обработкой ошибок, но ее откатили.Gorthauer87
08.08.2019 13:53В этом смысле дублируют, в других — нет. Просто контракт и интерфейс — разные сущности. Интерфейс в рантайме существует, а контракт — сущность сугубо compile-time'овая, хинт для компилятора, а потому в рантайме бесплатная.
Ну грубо говоря и контракт и интерфейс требуют от типа некоторых свойств, читай наличия методов, разница, как я понимаю, лишь в том, что интерфейс неявно реализуется для типов (утиная типизация и все дела), а в контракте ты пишешь для каких типов он поддерживается.
Но никто не мешает ввести и для контрактов такую же утиную типизацию, да и в целом может лучше просто отказаться от утиной типизации в интерфейсах и реализовывать их явно? Или наоборот, оставить интерфейсы, но пометить их как deprecated, а контракты использовать и для статической и динамической диспетчеризации?
Через запятую же)))
Ну хорошо, у меня есть свой тип, я хочу для него добавить поддержку контракта из чужого пакета, я смогу это сделать? Или скажем иначе, я могу определить контракт Sequence для своего типа?
siziyman
08.08.2019 13:57Конечно же от утиной типизации надо отказываться, это ужасное решение, но сейчас это делать, боюсь, поздно и несподручно.
tendium
08.08.2019 18:25+1Когда вы говорите об интерфейсах вместо контрактов, то как это соотносится со скалярными типами? Они-то никакие интерфейсы не реализуют… Или вы предлагаете, как в Ruby считать, что [почти] всё — объект? Да и как быть со слайсами? Скажем, []uint64 и []int64. Как их объединить одним интерфейсом? Писать wrapper?
Gorthauer87
08.08.2019 22:55А что мешает скалярному типу реализовывать интерфейс сравнения на большее меньше?
qrKot
08.08.2019 10:39Так если подставить интерфейс в том месте, где подставляется контракт, то его можно будет в compile time посчитать.
Как раз наоборот же. Интерфейс не гарантирует возможность посчитать в compile-time. interface{}, т.е. пустой интерфейс — это как раз сущность рантайма, его в compile-time «посчитать» нельзя. Плюс приведения интерфейсов и т.д. выполняются в рантайме.
Контракт же гарантировано считается в compile-time, в том и задумка.vintage
09.08.2019 07:23Только вот зачем вводить новую сущность, вместо того, чтобы починить существующую? И после этого они говорят про минимализм и ортогональность.
snuk182
08.08.2019 11:57В Rust трейты можно и как динамические интерфейсы использовать и как контракты в генерик методах.
И это была настолько неудачная идея, что от нее спасались как могли.
defuz
08.08.2019 12:16Не могли бы вы как-то подкрепить свое мнение? Вы дали ссылку на stackoverflow где человек не совсем понимает что он делает и на RFC с предложением улучшения синтаксиса.
snuk182
08.08.2019 12:32В RFC есть отдельный раздел "мотивация" с причинами введения аж целого резервированного слова для решения проблемы разделения типажей на статические и динамические контракты. Если мало — есть обсуждения.
То, что человек не понимает, что он делает, и является основной проблемой — неразличимость статических и динамических контрактов, вследствие чего возникает закономерное желание их повзаимозаменять. Основную проблему введение ключевого слова не решило, но ограничило область такой взаимозаменяемости, попутно заставляя разработчика лезть в документацию. Как приятная побочка, можно теперь подсказывать компилятору о новых местах, потенциально пригодных для оптимизации обоих видов полиморфизма.
defuz
08.08.2019 12:53+1RFC мотивиран улучшением наглядности, разделение на static и dynamic dispatch никак не изменилось.
То, что человек не понимает, что он делает, и является основной проблемой
Именно, и смею предположить что виноват здесь не язык, а недостаток понимания.
Из приведенного вами вопроса на stackoverflow. Человек хочет хранить поле типа MyTrait в своей структуре. Вроде бы понятная потребность, вот только как это должно работать? Тип структуры должен быть параметризирован типом и во время компиляции она должна мономорфизироваться для любого типа T реализующего MyTrait? Или на самом деле мы хотим в структуре хранить виртуальную таблицу методов этого типажа? Проблема именно в том что человек не до конца понимает что он в действительности хочет.snuk182
08.08.2019 13:03разделение на static и dynamic dispatch никак не изменилось
Я именно это и сказал — главная проблема не решена. Непонимание ее — просто отправная точка для хоть каких-то попыток ее решить или смягчить. Потому в итоге расплодилось синтаксиса на
Trait
(легаси),dyn Trait
(чисто динамика),impl Trait
(чисто статика). Попробую поискать обсуждение планов на типажи, поделюсь если найду (сходу не находится) — вроде как там проскакивала депрекация общего типажа.defuz
08.08.2019 13:08Все верно, bare синтаксис (Trait) уже сегодня можно отключить через deny(bare_trait_objects), в будущем его сделают недоступным.
Bare синтаксис существовал когда impl Trait еще не реализовали, а значит и не было с чем его путать. Когда это сделали, решили провести унификацию: impl Trait vs dyn Trait. Но старый синтаксис пока оставили, чтобы не нарушать обратную совместимость.snuk182
08.08.2019 13:16Вот да, когда вводили эдишены, было довольно долгое время непонятно, зачем с ними морочиться. А потом, когда начали вылезать вот такие архитектурные ошибки, идея эдишенов как способов мягко их отсечь оказалась весьма здравой.
defuz
08.08.2019 13:28Это не ошибки архитектуры, а противостояние развития языка с обеспечением его стабильности. Когда ввели dyn Trait я вообще не понял сразу что произошло: добавлено новое ключевое слово и новый более многословный синтаксис для того что уже и так работало. Но его сознательно побыстрее пропихнули в язык, еще до того как полноценно реализовали impl Trait, чтобы был как можно больший запас времени для переезда на новый синтаксис.
snuk182
08.08.2019 13:45Где это открыто называется ошибкой архитектуры, я в официальных источниках пока не нашел, только на SO. Но скорость, с которой по
dyn/impl
принимались решения, косвенно говорит в пользу этой версии.
maxxant
08.08.2019 12:39+1в черновике описано почему отклонили:
github.com/golang/proposal/blob/master/design/go2draft-contracts.md#why-not-use-interfaces-instead-of-contracts
Вообщем там список из семи часто спрашиваемых отклоненных идей «why not»
recompileme
07.08.2019 23:52+5AloneCoder Почему статья помечена как перевод? Оригинал длинней раз в 10. Вы выкинули весь негатив и сомнения по поводу дженериков, оставив только их «плюсы». Увеличится время исполнения или компиляции. Усложнится чтение и понимание кода. Дженерики начнут пихать всюду. Очень много нововведений в пропорсал. Очень мал юзкейс применения дженериков. Не пропорциональное усложнение языка. Я против в данном виде. Если не согласны — прочтите оригинал для начала: github.com/golang/proposal/blob/master/design/go2draft-contracts.md
Siemargl
08.08.2019 01:20+1Это перевод статьи (ткнуть надо в «Автор оригинала...») под заголовком, а не драфта.
KirEv
08.08.2019 02:19+3Когда я начал Интересоваться golang, больше всего влекла идея простоты и выразительноси, спустя где-то год повернулась возможность написать рест апи на го, до этого — ни строчки кода…
Сейчас более 2х лет свыше 90% написанного кода мной — на Go.
Последний год все чаще приходиться работать с чужим кодом, и все чаще меня бросает в ужас/ступор/холодный пот…
Изобилие интерфейсов, попытки скрестить паттерны, натянуть ооп и тд. и т.п., разбавля все это дело рефлексией, множеством абстракция, тучой готовых либ и решей (из которых малая часть нужна конкретной задачи проекта).
Как можно настолько похерить идею лаконичности и выразительности?
А вы говорите дженертков не хватает…
В го приходят многие из интерпритируемых языков, и многим из многих вообще мало известно о природе типов…
А потом нам с ихним кодом работать.
Простите, накипело…
Отнедавна участвую в проекте, пред.разрабы форкнулись от опенсорс, накрутили поверху своих ''фич'', другой разраб после них начал добавлять новые ''фичи'' и заодно учить Go… и это подобие с кучей сторонних либ, миллионом интерфейсов, странными реализациями сервисовнутртлибной имплементацией (map[string] func(interface{} )interface{} ) и т.п… сейчас стонет у меня на столе…
Пожалуйста, не нужно дженертков…
Пригорело, простите…HEKET313
08.08.2019 08:35+1То, что вы видели много говнокода на Go, вообще никак не значит, что языку не нужно развитие в сторону дженериков. Это не нормально, когда ты вынужден копипастить (или генерить) кучу одинаковых функций с разными типами аргументов, либо использовать рефлексию, чтобы выразить что-то общее. Это не нормально, когда перед каждой функцией, которая принимает []interface{}, а у тебя []string или []int, нужно гонять цикл по этому массиву и просто перегонять значения из одного типа в другой.
И еще касательно плохого кода. Я около года всего пишу на Go, но за это время о каких-то конкретных правилах и best-practice я слышал очень поверхностно. Я не видел, чтобы кто-то использовал какой-то фреймворк, который располагает к определенному стилю написания кода. Максимум, что я встречал в проектах — это куча линтеров. А касательно принципов — кучку высказываний аля «Возвращайте структуры, принимайте интерфейсы» по поводу которых написана тонна статей с обоснованием, но в итоге потом оказывается, что это правило не всегда актуально и не работает в куче случаев. Поэтому нет ничего странного, что многие, приходя в язык, стараются натянуть сову на глобус, не у всех хватает смелости признать, что его прошлый опыт не релевантен и нужно писать по-другому, но интересно, что при этом у новичков часто нет какого-то относительно «правильного» примера.
И, кстати, в данном случае вернемся обратно к дженерикам, люди потому и мутят кучу абстракций с interface{} и рефлексией, потому что у них нет удобного способа выразить то, что им нужно в данный момент. Язык как будто запирает тебя в рамках строгой типизации, но при этом не предлагает ничего взамен для более абстрактных вещей кроме пресловутых interface{} и рефлексии.
И да, если вы знаете какой-то свод правил или серию статей о «правильном» коде на Go, я буду признателен, если вы мне их посоветуете.
Color
08.08.2019 11:42+3Вот интересно, кстати. Многие говорят про го-вэй и про то, что не нужно тянуть подходы из других языков.
Это понятно, но можете привести какие-то примеры хорошего го-вейного продуктивного кода (не стандартные либы), который прям вот 100% отражает принципы го?
Сам уже не первый год пишу на го, но как дело доходит до серьезной логики, все равно получаются классы, фабрики, DI и интерфейсы (ну хоть без наследования).Alpapla
09.08.2019 19:42Я долго искал качественный код на Go, думал начать на нем писать из материальных соображений. Задавал вопросы в форумах, искал репозитории, статьи и пр. Так и не нашел.
Потом оказалось, что понятие о говнокоде у гоферов и всех остальных кардинально отличаются. Вот код главного контрибьютора языка Go.
Нарушены буквально все принципы того же Clean Code.TheShock
10.08.2019 00:38
Это просто квинтэссенция всего Гоу какая-то.'6' <= f[1][0] && f[1][0] <= '8'
Sarymian
08.08.2019 05:32-7Начал изучать Go не так давно…
И постоянно как 6-ти классник над «многочленом» хихикаю над:
Пусть у нас есть
слайсспайс целых чисел.
Ни чего не могу с собой поделать.
Deterok
08.08.2019 09:01+2Писал код на Go примерно года 2 с небольшим и хочу сказать что без Generic'ов очень плохо.
Source
09.08.2019 00:11+1А что вас заставляет писать на Go? Вроде же вполне естественно с накоплением опыта желать выражать свои мысли на более выразительном языке, а не на менее выразительном…
Мне до сих пор иногда кажется, что кто-нибудь из Google выскочит с большим плакатом РОЗЫГРЫШ.Deterok
09.08.2019 22:23+1Я сразу хочу уточнить, что go неплохой язык. У него есть своя ниша и он в ней неплохо уживается. Как любой другой язык он является инструментом и многге завист от того где, с какими целями и как его использует. Что заставляло писать на нем? Во-первых любопытство, мне интересно изучать новые технологии и языки. Во-вторых неплохая зп. В трктьих сфера: blockchain, финансы, микросервисы.
Source
11.08.2019 00:40-1Для любопытства вопросов нет, я тоже Go ради любопытства изучил и даже 1 сервис на нём написал. Но постоянно писать на таком урезанном и во многом непоследовательном языке — хз… Зачем лишать себя удовольствия от программирования?
Если только ради сильного интереса к конкретной сфере, где он получил распространение. Этот аргумент действительно выглядит убедительно :-)
Denis631
11.08.2019 00:09+1Заставляет писать на Gо тот факт, что все проекты CNCF (Cloud Native Computing Foundation) написаны на нем. Так что хочешь не хочешь, а надо писать на нем (например для контрибуции)
Misiam
08.08.2019 09:39+1программирование с дженериками позволяет представлять функции и структуры данных в виде дженериков, исключая типы.
Нельзя объяснять термин используя тот же самый терминкроме термина «рекурсия».
Программирование с дженериками позволяет акцентировать внимание на алгоритме, абстрагируясь от конкретного типа.
dbelka
08.08.2019 10:22+1Вообщем, люди разделились на два лагеря:
— одни перешли в go из типизированных языков(где есть дженерики). Они считают, что дженерики нужны.
— вторые пришли в go из языков без строгой типизации(где дженериков никогда и не было). Они считают, что дженерики не нужны.
Fight!tendium
08.08.2019 18:08А зачем в языках без строгой типизации дженерики? Нет, ну вот я знаю, что для PHP есть RFC с предложением дженериков. Но только там это появилось от того, что php начал двигаться в сторону строгой типизации.
dbelka
08.08.2019 18:22Всё верно, я о том кто к чему больше привык просто.
tendium
08.08.2019 18:29Так нет, речь о том, что, скажем, универсальную функцию max можно сделать как в языке со строгой оптимизацией и без дженериков, так и на языке со строгой типизацией и дженериками. А вот на языке со строгой типизацией без дженериков универсальную функцию сделать можно, но больно.
gtetrust
08.08.2019 11:34+1Для реверса, вполне можно написать такую функцию:
func Reverse(s []interface{}) { first := 0 last := len(s) - 1 for first < last { s[first], s[last] = s[last], s[first] first++ last-- } }
Или это не так?)HEKET313
08.08.2019 11:51Так, но перед каждым использованием ее будете гонять цикл для преобразования []string или []int или любого другого массива в []interface{}. А потом обратно
tendium
08.08.2019 18:09Либо же надо сделать входной параметр просто interface{}, но тогда придется в функции использовать рефлексию и проверять, слайс ли пришел. Что лучше? Не знаю. И я не об эффективности…
TheShock
08.08.2019 18:53Для реверса, вполне можно написать такую функцию:
Что, если нам надо написать функцию, которая возвращает новый массив, а не изменяет старый — как вы это сделаете?
Или это не так?)
mOlind
08.08.2019 11:52Для меня golang прекрасен именно отсутствием генериков и гибкими интерфейсами. Эти два подхода замечательно заменяют друг друга. Ты хочешь обязать какой-то объект обладать какими-то свойствами. Опиши это в интерфейсе и передавай в функцию свой интерфейс. Все. Не надо шаблон. Функция будет работать одинаково для всех типов, которые подходят под интерфейс. Я пишу часто на C++ и в моей жизни хватает шаблонов. Пожалуй их даже слишком много и Golang как глоток свежего воздуха с подохдом интерфейсов.
Если реализовывать два подохда одновременно. Будет каша и путаница.siziyman
08.08.2019 12:07Так не надо шаблонов, надо дженерики.
А «гибкие интерфейсы» потом вам светят несуразными паниками в рантайме (и хорошо, если девелоперском рантайме, а не в продакшне).mOlind
08.08.2019 12:30Погодите, а откуда паника появится? Если на этапе компилации уже будет все проверено, чтобы входные типы умели делать то, что требует интерфейс.
snuk182
08.08.2019 12:52Если на этапе компилации уже будет все проверено, чтобы входные типы умели делать то, что требует интерфейс.
Проверено — на основании чего?
mOlind
08.08.2019 13:33+1На основании функций, которые тип реализует, а интерфейс требует?
package main import "fmt" import "math" type geometry interface { area() float64 perim() float64 } type circle struct { radius float64 } func (c circle) area() float64 { return math.Pi * c.radius * c.radius } // func (c circle) perim() float64 { // return 2 * math.Pi * c.radius // } func measure(g geometry) { fmt.Println(g) fmt.Println(g.area()) fmt.Println(g.perim()) } func main() { c := circle{radius: 5} measure(c) }
Компилятор возвращает ошибку:
test.go:29:9: cannot use c (type circle) as type geometry in argument to measure:
circle does not implement geometry (missing perim method)
Научите, как сделать панику в рантайме.snuk182
08.08.2019 14:06Банально.
Вы можете возразить — так здесь же все очевидно, не надо кастить что угодно к чему попало! В том и дело, что на сложных проектах совсем не очевидно, откуда что пришло и к чему приводится. Вот похожие касты возникают, когда на рантайм перекладывают обязанность компилятора — если в функции ожидаетсяsquare
и ничего больше, дляcircle
этой функции вообще не должно существовать. Да, можно воткнуть проверку типа рефлексией, но это процессорное время и лишние несколько строк кода.mOlind
08.08.2019 16:12Может быть я не работал на сложных проектах на Go. Но работал на сложных проектах на C++. Где стрелять себе в ногу проще по многим причинам. И если есть возможность не писать хреновый код, его не надо писать. Могу понять студенческие поделки, но в продакшене такого быть не должно. Язык сам дает инструменты, чтобы проверять, что все норм. Почему бы ими не воспользоваться?
Тут у вас есть возможность проверить, успешный ли каст или нет. Добавили проверку — все работает.
В каком случае может возникнуть необходимость делать касты без проверки я не могу представить.tendium
08.08.2019 18:14есть возможность проверить, успешный ли каст или нет
Есть, но это в свою очередь увеличивает количество написанного кода и снижает его читаемость. К тому же теперь вам надо принимать решение в рантайме, что делать, если каст был неуспешный. Что в целом может усложнять архитектуру кода. А вот при наличии дженериков код бы стал лаконичнее, а потенциальные ошибки проверялись бы компилятором сразу.
anjensan
09.08.2019 16:01Ну добавите вы проверку. Сделали такую проверку в рантайме, тип не тот, паники нету… Но что дальше? Напечатать в лог "Unsupported!!" и продолжаете выполнение? Т.е. в вашем понимании это "все работает"?
mOlind
09.08.2019 16:48А в чем собственно проблема? Вы же сейчас прочитали абстрактный кусок кода, сделали какие-то выводы и спрашиваете у меня верны ли эти выводы.
Да я считаю, что конверсию типов надо проверять. Да, все ошибки, которые возвращает api надо проверять. Да, если что-то может пойти не так, надо проверить, все ли хорошо. И если произошло что-то нештатное, действовать по обстоятельствам: отправлять ошибку выше по стеку, обрабатывать, пробовать снова или что-то еще.
Но это все риторика о том как обрабатывать ошибки.
Усложнение концепции языка не защитит от ошибок, а наоборот сделает код сложнее и ошибок станет больше. Несмотря на благие намерения. Стандартные библиотеки go показали, как на нем можно решать прикладные задачи и без дженериков.
serge-phi
09.08.2019 17:59Подозреваю, что похожие касты возникают, когда надо побыстрее накодить костыль (говорю это без осуждения, самому приходится заниматься костыльным программированием из-за нехватки времени). По хорошему надо добавить добавить метод «print» в интерфейс «geometry». Язык тут ни при чем. Вопрос цены изменения кода. Если этот интерфейс реализует не три класса, а три тысячи, то костыль с кастом будет быстрее закодировать. Но этот костыль потом с большой вероятностью воткнется в спину, когда через некоторое время нужно будет добавить три тысячи первый класс, реализующий интерфейс «geometry», а человек, который добавляет новый класс, не в курсе про этот костыль.
snuk182
08.08.2019 12:37Как раз шаблоны есть. Дженерики — не шаблоны, дженерики проверяются компилятором на этапе сборки конечной сущности, а не принимаются им по факту. Это также позволяет навешивать контракты на обобщенный тип-параметр — теперь сборкой занимается компилятор, у которого есть возможность совершать проверки.
Zanak
08.08.2019 13:22Прошу прощения, не соображу, а как тогда будет выглядеть универсальная функция Sort, работающая абсолютно с любым типом, и встроенным, и кастомным?
maxxant
08.08.2019 14:25github.com/golang/proposal/blob/master/design/go2draft-contracts.md#sort
вкратце
var s []*Person // ... sort.SliceFn(s, func(p1, p2 *Person) bool { return p1.Name < p2.Name })
uvelichitel
08.08.2019 13:34Мне синтаксис как то не очень. А разве нельзя просто ввести тип type, то есть разрешить декларации вроде
и получить все плюшки Hindley–Milner не меняя синтакис, а прямо в существующем?type Gene type
siziyman
08.08.2019 13:53То есть чтобы понять, является ли функция обобщённой, надо будет бегать по декларациям типов аргументов и проверять, не занесло ли туда
type type
? Читаемость страдает, как минимум. Каково это с точки зрения парсинга — я тоже не знаю.
Mozzi
08.08.2019 19:51Дженерикам — да, но не любой ценой. А данный пропозал, на мой личный вкус (6 лет коммерческой разработки на нём), тяжеловат, увы (
vba
FYI: Термин "Дженерики" на русский переводится как "Обобщения".
bolk
Чего минусуете-то человека, черти? Именно так и переводится, есть даже термин «обобщённое программирование».
HEKET313
Потому что не нужно переводить на русский интернационалтные термины хорошо понятные любому программисту
bolk
Серьёзно? И часто вы говорите «фанкшн» вместо «функции» или вот «киборд» вместо «клавиатуры»?
«Дженерики» — слово, появившееся от отсутствия образования. Придумали его те, кто учился не универистетах, а в смузишных и не знают, что есть слово «обобщения», придуманное ещё до их рождения.
Color
А «функции» — это исконно русское слово, вестимо?
bolk
А я где-то про исконность говорю? Вроде нет, говорю о том, что слово существует давным-давно, а новое придумывают от необразованности.
Color
Вы путаете литературный перевод и профессиональный сленг/терминологию. Это разные вещи.
Возьмите различные области — юриспруденцию, короблестроение, шахтерское дело, металлургию. Огромное количество терминов называются так, как они называются, хотя имеют перевод на русский язык. Именно потому, что это термины, и люди, оперируя ими, говорят о профессиональных сущностях, а не об общеупотребительных.
В ИТ то же самое.
bolk
А вы думаете айти в двухтысячных появилось что ли? И до него были айтишники, слово «обобщение» уже было, просто в профессию пришло много народу с непрофильным образованием и вместо нормальных терминов стало тащить слова, которые они не знали как называются на русском.
HEKET313
Я прям горю желанием услышать, как же вы произносите фразу «Протестировать релиз на стейджинге и отправить его в продакшн»
bolk
Я говорю о ситуации, где слово в языке есть, а вы пытаетесь меня подтолкнуть к ситуации, когда слова в языке нет или оно неудобное.
Для справки: «стейдижнг» = «отладочная среда» (неудобно), «продакшн» = «продуктовая среда» (неудобно, мы говорим «прод»).
HEKET313
Возможно я просто предвзято отношусь к конкретно этому слову из-за моего окружения, как я уже писал в комменте ниже. Вообще я только недавно узнал, что дженерик — это обобщение, как-то не думал даже его переводить, до этого это для меня было слово означающее именно ту самую передачу типов в класс/функцию как в C#, где я с ними и познакомился. Поэтому для меня до недавнего времени «дженерик» был таким же словом как «тест» или «продакшн», а уж чтобы слышать его в разговорах как «обобщение» — этого я вообще от родясь не слышал.
bolk
Обобщение — старое понятие, представьте, если бы в разговорах с вашими друзьями вы использовали бы «майнус» вместо «минуса», просто не знали бы, что слово «минус» уже существует.
Вот «дженерик» для меня — примерно то же самое.
Pafnutyi
А ещё можно вспомнить «инстансы» и «хенделы», и смузи не забыть в кружечку налить ))) За инстансы ваще бы ноги выдёргивал и вичками бил — бесят до безобразия ;P
vba
Нет уж увольте, хватит засорять русский язык словами которых там отродясь не было, и появляются они там от безграмотности. Если бы вы, хоть одну книгу открыли на русском языке, по данной теме, то ужаснулись бы всем тем обобщениям, кортежам, спискам, ссылочным значениям и другим из ряда вон выходящим терминам использующимся в наших академических кругах. Мы не пигмеи со словарным запасом из 6 слов, не нужно захламлять наш язык всяким мусором, тем более что в русском языке все эти термины уже давно есть.
adictive_max
Нет уж увольте, хватит пытаться вручную форматировать язык, как будто он ваша личная собственность. Язык формируетя сам, так, как удобно его носителям, и они не обязаны спрашивать вашего соизволения.
Кстати, по поводу «отродясь не было»:
термин,кортеж,академический,пигмеи.bolk
Язык формируется из двух течений: переменами и сопротивления переменами. То и другое необходимо. А вы так говорите, как будто первое важно, а второе — плохо.
adictive_max
А вы так говорите, как будто второе важно, а первое — плохо.
bolk
Я вам написал про перемены и сопротивление. Я как раз из сопротивления.
tendium
Так а чего вы на современном русском говорите? Говорите на языке из Слова о полку Игореве, а то, вишь, вокруг холопы распоясались, язык мелодичный поломали.
bolk
Я говорю важны и перемены им, и сопротивление, алло. А вы говорите «с Москвы», «на моём Айфон», «одевать ботинки»?
tendium
А как вы говорите: в Шереметьеве или в Шереметьево?
bolk
«В Шереметьеве», разумеется.
vba
Одно дело вливать в язык редкие слова которых не было до этого в языке, типа шлагбаум, штраф, академия. А другое дело по малограмотности начинать спикать как ньюбайный бушмен, который тайпает на кейбоарде, релизя в продакшен что слайсает лоафы брэда шарповым найфом.
Если это новый русский язык, потому что большинству так удобнее, я на таком суржике обобщатся не намерен, а люди которые этим занимаются, малограмотны и не знают ни родного языка и не познали красоты и лаконичности французского и/или английского.
Мое оценочное суждение.
siziyman
Я тут аккуратно повторюсь: вы всячески порицаете тех, кто «коверкает русский язык», при этом у вас не хватает тучи запятых, и вы стабильно не можете написать «тся/ться» по вполне себе канонiчным правилам русского языка. Это нормально? :)
webkumo
Простите, а как вы произносите "звонит" и имеет ли для вас какое-то значение постановка ударения в данном слове?
ЗЫ бесят порой "борцуны" за чистоту языка, когда сами грамотно писать/говорить не способны.
ЗЗЫ а ещё в добавок к коментарию Сайзыймена — у вас ещё и с литературной корректностью проблемы (не согласованное предложение "Моё оценочное суждение" — пример согласованного "Это — моё оценочное суждение", такое упрощение корректно только в устной разговорной речи). И ё вы не используете, нарушая правила русского языка.
vba
Что-то я такого правила не припомню, если на то пошло то там глагола не хватает. И это как-бы ответ на комментарий.
ё во многих случаях необязательна к использованию. Да и если честно не знаю где на раскладке она есть(QWERTY без кириллицы). Вы историю, почему она стала обязательна к написанию в 1942г, знаете?
За опечатки и ошибки можно упрекнуть кого угодно.
Я не скрываю того факта, что из-за отсутствия многолетней практики письменного русского языка, я допускаю некоторые ошибки. Это все вопрос практики. Я никогда не претендовал на роль хранителя русской орфографии. Опечатками и отсутствием запятых языка не испортить. А вот тупым перениманием импортных терминов, можно и даже очень эффективно превратить, русский язык в подобие англо-суржика.
webkumo
Ну тогда с словарь языка тоже хранить не надо. Нежизнеспособные заимствования — умрут сами. Жизнеспособные — выживут несмотря на десятки "исторически имеющихся" аналогов. Всяческие опечатки, орфографические ошибки — считай новое слово (некоторые слова меняют написание в ходе исторического процесса языка!). Только это новое слово ещё хуже заимствованного. Пунктуация: не справляетесь со сложными предложениями — пишите простые. Вот просто ведь?! Но почему-то вам хочется использовать глубину языка, но наплевать на корректное его оформление. А это compile time error же!
Ё "необязательна" только в типографике (чтоб этим советским упрощателям в аду гореть… нет, чтобы головой сперва подумать, потом уже резать "ненужные" буквы). Если вы изучали русский язык в школе, если сдавали какие-нибудь экзамены по русскому языку, то начиная с 90х обязательность корректного использования ё точно вернули. Более того — вернули-то раньше, я просто не задавался вопросом, когда именно. Для кверти-клавиатуры в ЙЦУКЕН-раскладке ё располагается вот здесь: `~.
Глагол в целой куче случаев можно опускать — это корректно с точки зрения литературного языка, а вот несогласованное предложение, стоящее отдельным абзацем, у вас было. Пример, как его согласовать, я вам написал.
deespater
Простите, но у вас тоже «звонит» неправильно написано. Правильно — «звонит».
HEKET313
Да, вы правы, я сто лет не читал книги по IT-тематике на русском языке, все больше как-то на английском. Зато в диалогах с зарубежными коллегами не возникает никаких проблем в распознавании тех или иных терминов. Сколько себя помню, ни от одного коллеги, ни от русского, ни тем более от иностранца, никогда не слышал слова «обобщения» в значении «дженерик». Возможно в Вашем круге общения все кругом употребляют «обобщения» вместо «дженериков», но у меня это совершенно не так. Поэтому я ума не приложу зачем переводить на русский термин, который давным давно используется повсеместно.
bolk
То есть вы так и говорите с зарубежными коллегами: «Протестировать релиз на стейджинге и отправить его в продакшн» или всё же остальные слова так же переводите на английский?
HEKET313
Разумеется перевожу, но удобно, что спокойно получается использовать одну и ту же терминологию и с иностранцами и с соотечественниками
bolk
То есть 99,99% слов вы переводите, а тут нужен другой принцип?
HEKET313
Разумеется, я перевожу все, что не относится к специфичной профессиональной лексике, в отношении же специфичных слов — тут уже как больше принято или как меня лучше поймут. В конце концов вне зависимости от того, на каком языке человек разговаривает, гуглим мы вопросы по программированию в 90% случаев на английском, я так может и в 99.99% случаев, поэтому и слово «generic» гораздо большему числу людей будет понятно, чем «обобщение». У меня случались ситуации, когда человек не знал слово «дженерик», но в 100% случаев это был человек, который либо в принципе никогда не сталкивался с такой конструкцией языка, либо просто не знал, как такая конструкция в принципе называется
vba
Наверное вам не повезло обучатся в наших ВШ и работать с коллегами которые там обучались.
Не нужно ничего переводить, значение обобщения было в русском языке всегда(ну или почти, язык же не стоит на месте), вы просто о нем не знали, только и всего.
Я живу на западе и работаю на британскую контору, но никаких проблем с общением с коллегами не испытываю и даже в мыслях никакие "дженерики" не появляются. Как с ВШ осталось, шаблоны проектирования, обобщения итд, так оно и есть, все остальное от малограмотности.
siziyman
Для человека, который бравирует своим образованием и выступает за чистоту русского языка, вы совершаете многовато детских ошибок. :)
tsya.ru, запятая после «Наверное», запятая перед придаточным в сложноподчинённом предложении.
vba
Хм, пунктуация в русском языке, тем более, в вышеописанных случаях никогда не являлась делом детским. Вы без вашего сервиса навряд ли смогли бы найти такие ошибки. Я по-русски не пишу уже много лет, все как-то по фр или по английски, а у них с пунктуацией намного проще. Есть все-таки плохое влияние.
siziyman
Запятые в сложноподчинённых предложениях и после вводных слов проходятся, кажется, в 6 классе школы.
Я сервисом не пользуюсь, мне это и без того глаз режет.
vba
Нет, если мне память не изменяет, то это класс так 10.
siziyman
Специально полез проверять: самое начало девятого.
Вот вводные слова — 8-й класс.
Ну в общем это всё ещё не вяжется с ратованием за чистоту русского языка, если честно.
site6893
1C программист?
bolk
За что же вы так родной язык-то не любите? Дефис-то поставьте.
brainunavailable
Понравился ваш пост с названием «флоатинг», красиво
bolknote.ru/selected
bolk
Какое слово вы предлагаете использовать?
site6893
не угадал, насчет родного языка!