Pull to refresh

Comments 73

Да, я Desktop Embedder и эту ссылку упомянул в статье :)
Не согласен с автором идейно. Flutter построен вокруг концепции реактивного программирования. Go, напротив, реализует подход `communicating sequential processes (CSP)`. Вы же, нигде в вашем мысленном эксперименте не используете goroutines — фундаментальное преимущество модели языка Go. На мой взгляд, user interface на Go должен следовать принципам обозначенным Rob Pike в `Concurrent Window System by Rob Pike — Plan9 OS — `, как например редактор acme из Plan9.
Go, напротив, реализует подход communicating sequential processes (CSP).

CSP и реактивное программирование не взаимоисключающи.


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

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

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

Можно пруфы?


все "специальные" фичи языка запутывали – "специальный метод под названием конструктор", "специальный синтаксис для автоматической инициализации", "специальный синтаксис для именованных параметров" и т.д.
все "скрытое" запутывало – "из какого импорта эта функция? это скрыто, глядя на код узнать это нельзя", "почему в этом классе есть конструктор, а в этом нет? он там есть, но он скрыт" и так далее
всё "неоднозначное" запутывало – "так тут создавать параметры функции с именами или без?", "тут должно быть const или final?", "тут использовать нормальный синтаксис функции или ''сокращённый со стрелочкой''" и т.д.

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


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


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

Можно пруфы?

В цитате, на которую вы ссылаетесь, есть ссылка "на пруфы".


Это как-то очень печально, если вас запутывает

Ничуть.


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

Ок, спасибо.

В цитате, на которую вы ссылаетесь, есть ссылка "на пруфы".

И там нет ни слова про golang. Ну в целом как обычно :)

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

Мне лично во Flutter (или, скорее в Dart) больше всего не понравилась работа с сетью, особенно в сравнении с Go — то, что в Go можно было сделать на каналах и горутинах, в Dart нужно делать адской асинхронщиной и промисами, к совершенно нечитаемыми бектрейсами :). Наверное, было бы удобно, если бы сетевую часть можно было бы написать на Go, а для UI пусть будет Dart, но, опять же, в данный момент интеграция сделана очень неудобным и платформозависимым способом.
Даже при использовании async/await сахара там все еще плохо?
По сравнению с отсутствием сахара — очень хорошо, но по сравнению с Go всё ещё слишком сложно и не так гибко.
вполне себе существуют и даже видны на экране, и вполне имеет смысл наследование.

Если честно, не совсем уловил суть.


Go отлично позволяет передать взаимосвязь между объектами и виджетами, и я не вижу, что именно наследование тут может улучшить. Может покажете на примере кода?


как отсутствие дженериков и «нормального» наследования в Go — для UI обычно это весьма полезно

Опять же, не сочтите за троллинг, но можно ли на примере кода показать, как именно дженерики тут улучшат код?


Наверное, было бы удобно, если бы сетевую часть можно было бы написать на Go

Вот я сейчас исследую насколько gomobile легко подключить к Flutter приложению. Есть и помимо сети масса кейсов :)

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


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


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


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

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

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


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


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

Не могли бы вы привести пример (не абстрактные рассуждения, которые повторяют просто определение "generics programming") задачи, которая бы часто встречалась при программировании на Go, и которую без дженериков решить было бы невозможно, или ужасно трудно?


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

Но его ведь не забыли ввести, верно? К чему тогда это?


Фундаментальных типов данных не бесконечное количество и при желании реализовать можно какую угодно новую структуру данных (без красивого синтаксического сахара, но можно), если чего-то очень важного нет. sync.Map, например, так и реализован.

sync.Map, например, использует тип interface{}. Да, тут я соглашусь, с использованием interface{} можно реализовать что угодно.


Но разве interface{} — это хорошо?


Но его ведь не забыли ввести, верно? К чему тогда это?

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

Но разве interface{} — это хорошо?
Нет, это ужасно, и за пределами стандартной библиотеки так делать не нужно.

Тем не менее, вместо общих фраз о том, что дженерики — это необходимость и без них никак, хотелось бы всё таки услышать что-то более приближенное к реальному программированию.
Потому что пока получается, как и всегда: "- Дженерики нужны. — А зачем? — Ну потому что надо, универсально, без них нельзя. — Покажите, как можно использовать. — Ну вот без них нельзя и всё тут."

Так я вам назвал совершенно реальную ситуацию: структуры данных.


Стек, очередь, приоритетная очередь, АВЛ-дерево, декартово дерево… Первые три структуры даже в стандартной библиотеке go есть — значит, ими точно кто-то пользуется.


Еще можно вспомнить про алгоритмы. Вот в прошлом году мне пришлось писать алгоритм Хиршберга для предотвращения уплывания текущей позиции при фоновом обновлении контента. И там тоже есть где применить дженерик...


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

Это ужасно даже в стандартной библиотеке. Дженерики проще, понятнее и надёжнее чем interface{}

И там тоже есть где применить дженерик...

Вопрос — нужно ли?


совершенно реальную ситуацию

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


Но как часто в веб-сервисах, консольных утилитах и иных более-менее реальных приложениях на Go нужно делать обобщённые структуры данных, которые будут универсальны и применимы во многих других приложениях и библиотеках?


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


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


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

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


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


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


Зло от дженериков в том, что они дают опасные надежды на то, что можно не сильно утруждаться дизайном типов данных, и дают ложное ощущение гибкости – которое зачастую порождает монстроидальные дизайны, которые сложно понимать, поддерживать и рефакторить (в комментариях ниже есть пример). Кроме того, в разработке фокус смещается на код, а не на данные ("я хочу писать код, который работает с любыми типами") – что в 99% опасный подход. Сортировка битового массива и сортировка терабайтного массива данных генома потребуют сильно разных подходов и компромиссов. Сначала нужно думать про данные и типы, потом про код.


Плюс накладывается мантра "повторять код нельзя" (DRY), которые многие новички возводят в абсолют, не понимая, что повторять код можно и нужно, пока он не повторяется, как минимум, 3 раза :) И дженерики тут кажутся какой-то магической пилюлей, которой и пользуются налево и направо, когда она есть, и утверждаются в мысли, что это необходимый компонент языков программирования.

Проблема не в написании универсальных структур данных, проблема в их использовании. Написать-то универсальную структуру данных очень просто, interface{} в помощь. А вот чтобы её потом использовать — приходится писать много "грязного" кода.

Вопрос — нужно ли?

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


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

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


Но как часто в веб-сервисах, консольных утилитах и иных более-менее реальных приложениях на Go нужно делать обобщённые структуры данных, которые будут универсальны и применимы во многих других приложениях и библиотеках?

Именно на Go так не нужно делать никогда, потому что те кому нужны дженерики — либо ушли с Go, либо никогда на него не переходили.


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


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

Ничем не обоснованное утверждение.

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


Потому что с ним код оказался понятнее, быстрее и проще.

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


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

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


те кому нужны дженерики — либо ушли с Go, либо никогда на него не переходили.

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


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

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


А я не использую дженерики в Go, но очень интенсивно использую шаблоны в C++.
При этом в Go есть композиция, а в C++ её нет в таком виде, в котором она есть в Go. Значит ли это, что на C++ нельзя решать задачи, которые можно сделать на Go используя композицию?
Копируя 1 в 1 — безусловно нельзя, но если пользоваться средствами C++ — конечная логика будет отличной от Go, но давать будет тот же результат.


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

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


Ничем не обоснованное утверждение.

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


Если хотя бы какое-то время поработать с библиотеками и проектами на Go, можно заметить, что подход к проектированию там не такой, что нужно "выполнять действия НАД объектом типа Х", а "вызывать У объекта методы реализующие интерфейс Х".


И при обсуждении дженериков почему-то все выпускают из вида, что интерфейс — это не только пустой interface{}. А между тем с помощью нормально описанных интерфейсов можно здорово упростить себе жизнь.


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

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

Да, именно это оно и означает.


Если хотя бы какое-то время поработать с библиотеками и проектами на Go, можно заметить, что подход к проектированию там не такой, что нужно "выполнять действия НАД объектом типа Х", а "вызывать У объекта методы реализующие интерфейс Х".

И этот подход никак не помогает.


Как уже было упомянуто выше — более-менее часто неудобства возникают в паре мест

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

Почему вас не устраивает пример с sync.Map? Из-за отсутствия дженериков приходится постоянно приводить значение к нужному типу, а это и менее удобно, и менее безопасно. Тоже самое и с любыми другими универсальными структурами данных и методов работы с ними.

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

sync.Map — это довольно рафинированый пример.
Предположим, мы делаем какой-то веб-сервис. Для чего нам использовать map с синхронизацией? Если map используется как сессионное хранилище (время жизни запроса), то скорее всего, к нему доступа за рамками этого запроса не производится и можно использовать обычный map.
В качестве глобального применения с ходу приходит в голову хранилище короткоживущих сессий, токенов и т.п. с привязкой к пользователю.


Итак, у нас есть map, который расшарен между потоками и его надо синхронизировать.
Вы берёте sync.Map, начинаете туда писать, допустим, UUID в качестве ключа и структуру в качестве значения.
Потом оказывается нужно помимо самого map иметь ещё какой-нибудь счётчик сессий незарегистрированных юзеров.
Вы выкидываете sync.Map, оборачиваете обычный map[UUID]UserStruct в структуру с RWMutex, добавляете в эту структуру поле со счётчиком и вместо методов Delete/Load с интерфейсами в качестве параметра и возвращаемого значения делаете свои обёртки, которые принимают UUID как ключ и возвращают указатели на структуру пользователя.


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


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

хотелось бы всё таки услышать что-то более приближенное к реальному программированию.

Скажите, пожалуйста, а какие вещи нужны в реальном программировании? Просто так вполне можно прийти к тому, что кроме ассемблера в целом ничего особо не нужно.


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

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


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

Вы выкидываете sync.Map, оборачиваете обычный map[UUID]UserStruct в структуру с RWMutex, добавляете в эту структуру поле со счётчиком и вместо методов Delete/Load с интерфейсами в качестве параметра и возвращаемого значения делаете свои обёртки, которые принимают UUID как ключ и возвращают указатели на структуру пользователя.

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

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

Pipe-фреймворки и пулы объектов — при наличии метрик и логгирования уже простыми каналами\слайсами не обойтись. И либо в каждом проекте пишутся заново, либо используется готовая реализация с interface{}, использовать который — дурной тон.

И совсем уже реальный пример из моей практики: пара деревьев, одно хранит содержимое корзины (речь идет про ретейл), другое дерево — сложные правила расчета маржей. Правила обхода деревьев и их балансировки отличаются, как и типы хранимых данных. При чем второе дерево расшарено между потоками, т.е. должно быть синхронизированным. Если реализовывать без interface{}, то получатся две +- одинаковых структуры и две группы методов работы с ними.
И вот, вы сидите со многостраничной спецификации по модулю, но вместо реализаций требований заказчика реализуете структуры данных\методы (обязательно эффективные реализации, а в случае с обходом и балансировкой деревьев это может быть достаточно сложный алгоритм), хотя могли бы воспользоваться проверенной библиотекой от какого-либо вендора.

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

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


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


Другой пример: опять же, простейшая реализация message bus
Pipe-фреймворки и пулы объектов

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


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


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

Но в реальности это происходит не настолько часто.

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

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

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

Да, опять же, без этого можно жить и с этим можно смириться. Но считать, что без дженериков удобней чем с ними — извините, но я не понимаю этого. Тем более почему-то в go можно объявить с каким типом объектов работают каналы\массивы\слайсы — так почему бы не дать разработчикам возможность делать то же самое, но и с другими структурами?
Но считать, что без дженериков удобней чем с ними — извините, но я не понимаю этого.

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

Просто чтобы уточнить — я не хейтер го. Я писал на нем экспортеры для прометеуса и небольшие сервисы для проксирования\балансировки запросов и я не испытывал особого отвращения от языка в этих кейсах.
Но перекладывая весь свой опыт в Java/Groovy на Golang, я понимаю, что он неприменим(точнее слишком уж неудобен) в большой части случаев из моей практики. И по большей части причина этого как раз таки в отсутствии дженериков, т.к. мне постоянно приходиться работать с коллекциями в том или ином их виде (как и ооочень большой пласт программистов, я думаю) и я не хочу постоянно писать циклы для фильтрации\модификации списков. А golang не позволяет мне вынести весь этот бойлерплейт в хелпер-методы, сохранив при этом безопасность работы с типами. Так же как не позволяет создать удобные унифицированные структуры данных.
Отсутствие дженериков — т.е. отсутствие возможности реюзать чужой код(в общем случае) — это проблема, как для меня так и для множества других разработчиков. И система типов\интерфейсов го никак не позволяет решить эту проблему. Это все к вопросу про то, какие бы проблемы решили дженерики если бы их добавили в язык.
т.к. мне постоянно приходиться работать с коллекциями в том или ином их виде (как и ооочень большой пласт программистов, я думаю) и я не хочу постоянно писать циклы для фильтрации\модификации списков. А golang не позволяет мне вынести весь этот бойлерплейт в хелпер-методы, сохранив при этом безопасность работы с типами.

Всё Go позволяет, просто вы пока мыслите на языке Java, а писать пытаетесь на Go (судя даже по формулировке задачи – "коллекции", "фильтрации списков" и т.д.). На самом деле вы, конечно же, совершенно валидную проблему описываете, но давайте я уточню, правильно ли я понял – вы говорите, что большую часть вашего ежедневного кода составляют а) нестандартные для Go структуры данных б) операции над массивами map/filter/reduce и вам не хочется писать их в виде циклов (чем они, по сути и являются) и вы не видите способ написать их на Go. Я верно понял суть проблемы?


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


  • как часто вам приходится выбирать структуру данных, которой нет из коробки в Go — скажем, red-black tree? (интересует конкретное число – там, 3 раза в день, или 10 раз в месяц)

Второй вопрос по поводу оформления map во враппер – это, конечно же делается – блин, это буквально циклы, их писать 15 секунд и вероятность ошибиться 0.0001%. Это пишется быстрее, чем комментарий о том, как сложно жить без дженериков:


func Map(in []string, fn func(string)string) []string {
    out := make([]string, len(in))

    for i, val := range in {
        out[i] = fn(val)
    }

    return out
}
...
in := []string{"1234", "sadd", "3434"}
out := Map(in, func(s string) string {
    return s + " mapped"
})

Если вы совсем уж уверены, что у вас такой специфический кейс, что в каждой программы вам нужно сотни раз делать map/filter/reduce на 100 разных типов в каждой строке, то вариант с интерфейсами пишется один раз на всю жизнь, и дальше единственное отличие от привычного вам в том, что нужно привести тип один раз. Давайте, чтобы вам было проще понять фундаментальную разницу (точнее отсутствие оной), я переименую interface{} в Object:


type Object = interface{} // don't do this outside of Habr examples
type mapf func(Object) Object

func Map(in Object, fn mapf) Object {
    val := reflect.ValueOf(in)
    out := make([]Object, val.Len())

    for i := 0; i < val.Len(); i++ {
        out[i] = fn(val.Index(i).Interface())
    }

    return out
}
...
in := []string{"1234", "sadd", "3434"}
out := Map(in, func(s Object) Object {
    return s.(string) + " mapped"
})

И я вас прекрасно понимаю – если вам приходится map использовать сотни раз в день на все типы, то подход Go будет казаться многословным. Но я из головы могу придумать только один вариант, когда это будет реальностью – "лабораторные по информатике", на которых люди учат map/reduce/filter. В практической разработке – это либо неправильно выбранный инструмент (может вам R нужен и вы тупо данные молотите), либо это сильно преувеличенная потребность.


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


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

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

Как часто вам приходится использовать iota (ну не считаю я этот огрызок заменой enum'ов) или кодогенерацию? Лично на мой взгляд это на столько специфичные вещи, необходимость в которых возникает очень редко, однако они добавлены в язык. А дженерики — нет.

как часто вам приходится выбирать структуру данных, которой нет из коробки в Go — скажем, red-black tree? (интересует конкретное число – там, 3 раза в день, или 10 раз в месяц)

В случае с деревьями — да, очень редко. Буквально пару-тройку раз за всю практику. Но вот очереди, кеши, листы — существующие в проекте я использую каждый день, добавляю новые экземпляры в проект — ну тут прийдется взять цифру с потолка, потому что никогда не обращаю на это внимание. Скажем, условные пару раз в месяц. По поводу упомянутых хелпер-методов — использую во множестве раз каждый день, будь то библиотечные методы, или написанные самостоятельно. А так же есть Optional'ы и функциональные интерфейсы, которые так же очень способствуют написанию лаконичного и понятного кода. И без дженериков они не реализуемы.

Второй вопрос по поводу оформления map во враппер – это, конечно же делается – блин, это буквально циклы, их писать 15 секунд и вероятность ошибиться 0.0001%. Это пишется быстрее, чем комментарий о том, как сложно жить без дженериков:

Однако это бойлерплейт, и от его присутствия проекта хочется избавиться. По поводу вероятности ошибиться — на хабре есть блог компании pvs studio, в котором они выкладывают результаты анализов различных проектов, и практически в каждом репорте присутствует категория ошибок с вязанных с опечатками и copy-paste ошибками. В случае с go кто-нибудь может скопировать существующий код, и заменить привести к другому типу объектов. И я не говорю уже про многословность подобных решений с обертками, которая замыливает бизнеслогику.

Давайте, чтобы вам было проще понять фундаментальную разницу (точнее отсутствие оной), я переименую interface{} в Object:

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

И я вас прекрасно понимаю – если вам приходится map использовать сотни раз в день на все типы, то подход Go будет казаться многословным. Но я из головы могу придумать только один вариант, когда это будет реальностью – «лабораторные по информатике», на которых люди учат map/reduce/filter.

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

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

Я не могу считать бойлерплейт из навороченных и вложенных циклов читабельным. Описанный выше кейс в Java решается в 5-10 строк с помощью stream-api или той же гуавы\rxjava. И видя в коде цепочку filter().map().collect() ты сразу понимаешь какие именно действия совершаются в указанном куске кода.
Как часто вам приходится использовать iota или кодогенерацию?

К слову, я, например, пользуюсь iota и кодогенерацией строковых представлений типов практически в каждом проекте.
Не так удобно, как enum, конечно, но в C++ без кодогенерации enum из строки в значение перевести тоже нельзя, например. Разве что с макросами заморочиться.

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

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


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

Для такого дженерики вообще не нужны ни разу. Вы сейчас хорошо показали, в чём проблема – начинается с "я хочу писать реюзабельные структуры данных", а заканчивается "я не могу массив из json обработать без дженериков". И это проблема :D


И видя в коде цепочку filter().map().collect()

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


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


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

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

Для такого дженерики вообще не нужны ни разу. Вы сейчас хорошо показали, в чём проблема – начинается с «я хочу писать реюзабельные структуры данных», а заканчивается «я не могу массив из json обработать без дженериков». И это проблема. :D

Где-то было сказано про невозможность обработать этот кейс с помощью го? Или что конкретно я не могу это сделать? Мой аргумент начинался с того, что я не могу написать универсальную структуру — не важно список, дерево или что-то еще — которую смогу переиспользовать типобезопасным образом и без бойлерплейта. Проблема именно в этом, а не в чем-то другом, так что давайте обойдемся без пассивно-агрессивных нападок на мой уровень программирования, хорошо?
Напомню почему считаю, что это проблема — потому что ручное приведение типов не удобно и не безопасно, и оба этих критерия очень важны для меня.
Понимаешь, что там три вложенных цикла (разворачиваешь бойлерплейт у себя в голове, что есть дополнительной когнитивной нагрузкой). Или не понимаешь, конечно – и лепишь монстроидальные однострочные конструкции .map.filter.map.collect..., а потом удивляешься, почему всё так медленно работает.

В случае с filter вы осознаете работу цикла прочитав 6 символов, а не несколько строчек (объявление промежуточного слайса, итератор, проверка условия, добавление объекта в промежуточный слайс), т.е. когнитивная нагрузка будет ниже, еще и глазами можно меньше двигать. С последующими map/reduce/collect — то же самое. В результате, вы можете добиться того, что весь пайп обработки у вас помещается на один экран, и осознать что там происходит — дело нескольких секунд. И не забываем про те 15 секунд, которые требуются на написание каждого подобного цикла. А уж что выгоднее экономить — такты процессора, или рабочее время коллег каждый выбирает самостоятельно.

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

По поводу быстро или медленно — но это же просто итерации по коллекциям, при чем обычно вам заранее будет известен примерный объем данных, которые необходимо обработать — по крайней мере их порядок (из тз, спецификации сервиса или из вашего собственного практического опыта). Обычно вы заранее в состоянии прикинуть узкие места вашего алгоритма — при чем в 99% случаях узким горлом будут не итерации по коллекции, а какое-либо IO. Так зачем зачем заставлять заранее оптимизировать этот 1% случаев, если это можно сделать потом, и то при условии, что умный компилятор или рантайм не сделают этого за вас? Зачем заставлять экономить пару тактов процессора, если можно сэкономить несколько минут работы вашим коллегам — рабочее время которых ценится выше, чем эти самые такты?

Это же техническое решение было, а не политическое.
Я не осуждаю авторов го. Моя позиция — не оспаривание решения разработчиков го, а лишь «фичареквест» реализации которой я очень жду. Я не сомневаюсь в опыте этих людей и я также прекрасно осознаю сложности связанные с добавлением поддержки дженериков, и уж само собой я знаю как в go выживать без дженериков. Но почему-то в обсуждении вопроса «какую конкретно проблему могут решить дженерики» на мой вполне себе конкретный пример мне начали говорить, что проблема по большой части надуманная, случай редкий, и вообще я либо не умею программировать, либо просто еще не привык к го. А дженерики — это сложно, люди сразу же начнут говнокодить и писать непонятные штуки. В общем касти вручную, пиши боллерплейт и радуйся жизни (сразу же прошу прощения за ёрничанье).
Первый раз вижу подобную реакцию от комьюнити в обсуждении фича-реквеста. Если зайти к тому же джава-комьюнити и сказать что ты ждешь добавления элвис-оператора или интерполяции строк — тебе никто не скажет, что if'ов и String.format в 99% случаев хватает, скорее скажут что Oracle #$%!& и пора уходить на котлин\c#.
я не могу написать универсальную структуру

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


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

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

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


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

А в C++ коммьюнити вас ещё и на руках будут носить за предложение новых фич. В Go ж как раз это главное отличие – жесткая позиция по поводу необходимости новых фич – так что непонятно, к чему этот пример.


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

Вы правы, но мы тут говорим о двух разных вещах. Первая – это сколько "разворачивания" перекладывать на код, и сколько на мозг. Функциональные языки вроде Haskell, например, тяготеют к тому, что сначала нужно много всякого "загрузить" в мозг, чтобы потом максимально короткими языковыми конструкциями можно было выразить максимум. Они прям прутся от этого и считают это благим намерением и самоцелью. Я никогда это не понимал, и для меня это ровная противоположность "ясности" и "читабельности".
Вторая – это сколько redundancy в конструкциях должно присутствовать в языке. Если map можно реализовать уже – зачем его добавлять в язык? Потому что вы считаете, что так сделаете код лучше? А другие так не считают. Go не пытается предсказать, как лучше – даёт в руки минимум, на котором можно построить любой из вариантов – хотите map/filter, сделайте себе и пользуйтесь. Не хотите – не пользуйтесь, язык не навязывать. Опять – чем проще язык, тем он гибче и мощнее – что позволяет фокусироваться не на пользовании языком ("а что я должен использовать – цикл или map?"). а на решении бизнес задачи.

что если вам нужно обработать конкретный json и передать дальше – вам не нужно писать «универсальную структуру»

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

Представьте, что у вас отобрали проверку совместимости типов в методе append и вам приходится каждый раз самостоятельно делать тайп чек или оборачивать вызов этого метода во враппер. Но в append проверка есть — вот только лично мне недостаточно проверки только лишь в этом методе, мне ее не хватает в filter/map/reduce и во всех остальных местах. И мне не нравится то, что лишь разработчики языка выбирают список мест, в котором тайп чек будет, потому что я тоже хочу так делать. И снова повторюсь — я знаю как применять систему типов и интерфейсов в го, но как как бы ты не использовал интерфейсы — полноценно заменить дженерики ими не получится.

переиспользования дженериков по поводу и без повода – как раз из-за отстутствия чего Go и выигрывает в читабельности и понятности

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

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

Да, и он называется "цикл". Если я вас правильно понял, вы считаете конструкцию for ... range data {} бойлерплейтом, а data.map(...) - красивым минималистичным кодом? Но я не вижу в этом настолько фундаментальной проблемы, чтобы оправдывать существенное усложнение языка – например такое, которое сейчас рассматривается как стартовый черновик пропозала дженериков для Go 2.

Фундаментальная разница (а точнее, наличие оной) — не в названии типа, а в наличии обязательного приведения типа.


Без дженериков:


    return s.(string) + " mapped"

С ними:


    return s + " mapped"

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

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

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

А каст от "объекта любого типа" к строке происходит в любом языке сам собой магически и с нулевой стоимостью?


Вам нужно:


  • создать правила перевода типа в объект подходящий для сложения со строкой
  • создать правила для сложения чего-то со строкой

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


Будет это метод toString или условный operator+(string) — вообще пофигу, для свежего класса его скорее всего придётся писать самому.


В Go вы для этого создаете для своей структуры метод .String() и передаете интерфейс Stringer.


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


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

А каст от "объекта любого типа" к строке происходит в любом языке сам собой магически и с нулевой стоимостью?

А вы не путайте generic type placeholder и "объект любого типа". Во втором примере s — строка, и складываются строго две строки. Именно так работают дженерики в современных языках, таких как C# или Rust.


А вот как раз в Go без дженериков вы уже успели и метод String() ввести, и интерфейс Stringer зачем-то определить, и кучу других абстракций наворотить — и всё это ради того чтобы преобразовать один массив в другой...

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

mayorovp, да элементарно, дженерики же не только эту строчку в коде меняют ) Вот смотрите, ваши же примеры, но мы смотрим на код map() (который ведь тоже код, который должны люди, особенно ежедневно пишущие свои собственные структуры данных):


Go:


func Map(in []string, fn func(string)string) []string {
    out := make([]string, len(in))
    for i, val := range in {
        out[i] = fn(val)
    }
    return out
}

Java:


@Override
    @SuppressWarnings("unchecked")
    public final <R> Stream<R> map(Function<? super P_OUT, ? extends R> mapper) {
        Objects.requireNonNull(mapper);
        return new StatelessOp<P_OUT, R>(this, StreamShape.REFERENCE,
                                     StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {
            @Override
            Sink<P_OUT> opWrapSink(int flags, Sink<R> sink) {
                return new Sink.ChainedReference<P_OUT, R>(sink) {
                    @Override
                    public void accept(P_OUT u) {
                        downstream.accept(mapper.apply(u));
                    }
                };
            }
        };
    }

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


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

Но вы ведь понимаете, что это не равнозначные примеры? Вы сравниваете свой «хелпер-метод» с методом из stream-api, к которому предъявлялось несоизмеримо больше требований. Мне кажется, что если покопаться в исходниках го, то можно найти множество примеров на столько же на первый взгляд, как и приведенный кусок кода.

А аналогом вашему коду — т.е. без проверок на null и различных стратегий обработки упорядоченных\неупорядоченных списков — будет выглядеть так:

public <T,R> List<R> filter(List<T> in, Function<T,R> fn) {
  List<R> out = new ArrayList();
  for (T val: in) {
    out.add(fn.apply(val));
  }
  return out;
}


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

Забавно, как ваши Java-реализации несовместимы друг с другом без приведения типов :D


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


Но нет, в попытке обобщить (DRY! цикл писать два раза – зло!) мы создаём (есть же дженерики, значит надо всё дженерилизовать!) реализацию map(), придумав для этого новую концепцию Streams – в которую теперь программисты будут бездумно запихивать массив (потому что это Java-way). При этом сама реализация на порядок сложнее и нечитабельней, чем те несовместимые примеры, которые вы привели выше.


Это действительно уменьшит код с 3-х строчек до 1-й, но какой ценой? Ценой привнесения в язык дополнительных килобайт универсального-генерализованного-под-все-случаи-жизни кода – причем кода, который сложно даже увидеть (у меня заняло минут 20 пробиться через толщи абстракций до файле ReferencePipeline.java, в котором находится реализация map).


И всё ради того, чтобы не писать три строчки цикла.


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

Забавно, как ваши Java-реализации несовместимы друг с другом без приведения типов

Поясните, пожалуйста, что вы имеете в виду. Я не вижу в своем примере явного приведения типов — ни в самой реализации метода, ни в примерах его использования. Но признаю, что есть опечатка в коде — это, само собой, не filter, а map, и еще можно было бы заиспользовать super/extends для еще большой универсальности.

Но нет, в попытке обобщить (DRY! цикл писать два раза – зло!) мы создаём (есть же дженерики, значит надо всё дженерилизовать!) реализацию map(), придумав для этого новую концепцию Streams

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

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

Я придираюсь к другой теме (100500 возможностей написать одно и тоже), но у вас ArrayList, а в другой версии – обычный array. Чтобы их использовать между собой, придётся конвертировать из одного в другой, верно?


а не потому что дженерики ухудшают читабельность в общем случае.

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


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

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

но у вас ArrayList, а в другой версии – обычный array. Чтобы их использовать между собой, придётся конвертировать из одного в другой, верно?

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


В том же C# массивы реализуют интерфейсы IList и IList<T> .


психологическая травма после 10 лет работы с магией темплейтов в С++

В С++ главная проблема темплейтов — в том, что они образуют тьюринг-полный нетипизированный функциональный язык программирования, и это в статически типизированном императивном языке! Не надо смотреть C++, смотрите Java (ту часть, которая не касается взаимодействия дженериков и встроенных типов данных), C# или Rust.

А теперь посмотрите на эквивалентную реализацию:


public static <P, R> R[] map(P[] in, Function<? super P, ? extends R> mapper) {
    R[] out = new R[in.length];
    for (int i=0; i<in.length; i++) {
        out[i] = mapper.apply(in[i]);
    }
    return out;
}

Вы уверены, что это и правда как-то сложнее?

Меряться с джавой количеством строчек — это удар ниже пояса :)
На C++ или Rust дженерики отъедают около одной строки.
Куда большая разница будет из-за разницы в идиомах.

Может быть быть всё-таки не стоит перевирать слова?


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


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


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

И какой же иной механизм уберет бойлерплейт при работе с sync.Map?

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


Очевидно, в такой постановке вопроса, какую ставите вы, решения не существует.

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

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

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

Раз уж мы так любим примеры, можете привести пример какой-нибудь реальной проблемы и реальных случаев при которых она доставляет неудобства?
О чем вы спорите?
Вроде бы в GO2 собираются дженерики вводить :-)
UFO just landed and posted this here

Отлично, вы поделили все виджеты на системные (AppBar, Center, Column, Text, FloatingActionButton, Icon) и пользовательские (MyHomePage), заставив их использовать совершенно разное API: системные создаются через DSL при каждой отрисовке, а пользовательские — прямым вызовом конструктора строго один раз. Вы уверены, что это хорошо?


К примеру, представьте что в какой-то момент вам понадобилось отобразить две разные страницы MyHomePage рядом (не спрашивайте зачем, заказчик требует). Как вы будете укладывать свои виджеты внутрь Row?


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




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


Любое другое решение разрушает тот самый стройный DSL, который вы создавали половину статьи. Зато разделение виджета и состояния этот самый DSL еще больше упрощает: теперь виджет — это и есть структура с параметрами, ему больше не нужна особая функция-конструктор.


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


type MyHomePage struct {
    title string
}

type MyHomePageState struct {
    StateCore
    MyHomePage

    counter int
}

func (m *MyHomePage) CreateState() State {
    return &MyHomePageState { MyHomePage: *m }
}

func (m *MyHomePageState) incrementCounter() {
    m.counter++
    m.Rerender()
}

func (m *MyHomePageState) Build(ctx BuildContext) Widget {
    return Scaffold {
        AppBar: AppBar {
            Title: Text {
                Text: m.title,
            },
        },
        Body: Center {
            Child: Column {
                MainAxisAlignment: MainAxisAlignment.center,
                Children: []Widget{
                    Text {
                        Text: "You have pushed the button this many times:",
                    },
                    Text {
                        Text:  fmt.Sprintf("%d", m.counter),
                        Style: ctx.textTheme.display1,
                    },
                },
            },
        },
        FloatingActionButton: FloatingActionButton {
            OnPressed: m.incrementCounter,
            Tooltip:   "Increment",
            Child: Icon {
                Icon: Icons.add,
            },
        },
    }
}

Вроде пока всё красиво. Но не решена проблема обновления MyHomePageState::title при обновлении MyHomePage::title. И вот тут-то на отсутствие дженериков мы и напарываемся:


type MyHomePage struct {
    title string
}

type MyHomePageState struct {
    StateCore
    MyHomePage

    counter int
}

func (m *MyHomePage) CreateState() State {
    return &MyHomePageState { }
}

func (m *MyHomePage) UpdateState(state State) {
    // мы знаем, что state - это MyHomePageState
    // но система типов языка не способна выразить это знание
    s := state.(MyHomePageState)

    s.MyHomePage = *m
}

При наличии дженериков этот код мог бы содержать на 1 приведение типа меньше. Еще в нем, при желании, можно было бы избавиться от UpdateState полностью:


type MyHomePage struct {
    title string
}

type MyHomePageState struct {
    StateCore<MyHomePage>
    counter int
}

func (m *MyHomePage) CreateState() State {
    return &MyHomePageState { }
}
Спасибо, отличный комментарий и пример. В принципе, там где виджеты без стейта, то API «пользовательских виджетов» остаётся таким же, а со стейтом — да, надо либо оставлять подход, как у меня описан (в build виджет не создаётся), либо придумывать что-то иное.

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

Казалось бы – ну и что, в чём проблема? Но из моей практики, чем точнее код отражает то, как мы думаем о проблемной области, тем он дальше будет легче в понимании, рефакторинге и поддержке. Когда из «реального мира» появляется новое требование, оно основано на взаимосвязях вещей в реальном мире, и может сильно плохо ложится на тот код, который мы придумали, вопреки логичности маппинга.

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

Ну да, интерфейс Widget определенно стоило бы назвать WidgetProps или WidgetDefinition.


Однако, при наличии дженериков факт нахождения виджета внутри стейта был бы лишь деталью реализации StateCore, всё что требуется от разработчика — знать, что методы стейта имеют доступ как к внешним свойствам, так и к внутреннему стейту. Ментальная модель не страдает.


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

Оставить подход как он описан у вас не получится при всём желании, поскольку он не даёт вкладывать имеющие стейт виджеты внутрь не имеющих его. Помните, что простейший TextField уже имеет состояние! Это разрушает весь DSL.

Однако, при наличии дженериков факт нахождения виджета внутри стейта был бы лишь деталью реализации StateCore, всё что требуется от разработчика — знать, что методы стейта имеют доступ как к внешним свойствам, так и к внутреннему стейту. Ментальная модель не страдает.

То есть есть стейт виджета, который embedd-ит некий StateCore, который магией дженериков параметризирован под наш виджет, а сам виджет находится внутри стейта? Мне даже представлять это больно, и моя ментальная модель (виджет  -> стейт) страдает. Вам реально нравится такой дизайн?


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

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


Так, если бы мне пришлось пользоваться написанным выше, то этот самый StateCore был бы для меня наименьшей из загадок. Гораздо большая загадка — это методы CreateState, (UpdateState) и Build, в которых написан код, которых вроде бы что-то означает, но методы при этом нигде не вызываются...


С точки же зрения разработчика фреймворка — да, архитектура выглядит странно. Но дженерики тут ни при чём, виной тому — неудачные наименования. Скажите, если Widget обозвать WidgetProps, а State — WidgetImpl, это починит вашу ментальную модель?

Но дженерики тут ни при чём, виной тому — неудачные наименования. Скажите, если Widget обозвать WidgetProps, а State — WidgetImpl, это починит вашу ментальную модель?

Нет. Моя (и, полагаю, ваша тоже) ментальная модель это "виджет", у которого есть или нет "свойства", которые влияют на отображение. Чем лучше код "маппится" на ментальную модель, тем он проще, лучше и понятней. WidgetProps и WidgetImpl звучат как хаки вокруг дизайна языка программирования, а не как попытка смаппить проблемную область на код.
Я понимаю ваш подход, но это мой личный pet peeve – на моей практике такой код при любом следующем изменении в реальной задаче (например, много виджетов будут шерить один стейт) уже не будет поддаваться гармоническому рефакторингу и будет порождать всё более ужасные конструкции (GroupedCoreStatePropsWidget?).

Э-э-э, нет. Это не хак, это растет из предметной области.


Посмотрите на пример с MyHomePage с моими изменениями: у него есть свойства title и counter, и они оба влияют на отображение. Но у них разная природа!


  • title — это настройка, передаваемая от родителя, сам виджет не может её менять;
  • counter — это состояние, определяемое самим виджетом, родитель не может его установить.

В принципе, можно было бы совместить их в одной структуре, сделав title "публичным" тем или иным способом, а counter "приватным" (что бы эти два слова ни означали в Go) — и это бы даже работало… но только не во Flutter.


Потому что архитектура Flutter подразумевает, что у публичных свойств и у состояния виджета разные времена жизни. Объект WidgetProps может быть создан любое число раз в методах Build, но реализация WidgetImpl должна быть порождена только 1 раз. По-другому DSL у Flutter работать не способен в принципе.


Предлагаю вам вернуться к своей статье и посмотреть на свои же варианты DSL. Вы же сами в итоге предложили ввести AppBarParams, TextParams, ColumnParams и прочие вспомогательные структуры.




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

Сразу прокомментирую вот этот момент — "у title и counter разная природа": для меня это был сюрприз, потому что в моём понимании нет никакого ограничения, почему бы виджет сам не мог изменить себе title.


Я прекрасно понимаю подход Flutter. "Стейт это и есть виджет" это как раз то, что я пытаюсь объяснить – у нас и так уже есть у каждого виджета "стейт" (поля класса). Новая сущность "стейт" – это уже другая сущность и в моём понимании, на каждую сущность в ментальной модели (в голове) должна приходится одна сущность (тип) в коде. Плодить пачку типов для одной сущности – это признак какой-то путаницы в голове, и самый простой способ сделать код малопонятным и малочитаемым. Вот то, что я упоминал в статье про "зачем мы метод build() определяем не на виджет, а на стейт" – это имеет мало смысла, если не понимать, что всё это пляски вокруг дизайна.

Сразу прокомментирую вот этот момент — "у title и counter разная природа": для меня это был сюрприз, потому что в моём понимании нет никакого ограничения, почему бы виджет сам не мог изменить себе title.

Это как раз просто. Просто на верхнем уровне у нас есть вот такой метод:


func (m *MyApp) Build(ctx BuildContext) WidgetProps {
    return MyHomePage { title = "My Home Page" }
}

И каждый раз, когда у виджета MyApp будет происходить Rerender (а он может происходить потенциально когда угодно) — фреймворк будет присваивать title значение "My Home Page".


Даже если виджет MyHomePage что-то в этот title запишет — при любом событии, включая нажатие на любую кнопку, поворот экрана или приход сетевого запроса, title потенциально может измениться обратно. А может и не измениться — в зависимости от того, будет делаться Rerender для MyApp или нет.


И с полем counter та же самая ситуация: если хотя бы допустить возможность установки значения counter "снаружи" — оно будет сбрасываться в 0 в произвольные (для MyHomePage) моменты времени.


Потому и приходится разделять: title — свойство, counter — состояние.

Мы как-то сильно разошлись. Я имел ввиду, что это требование «title – свойство, counter — состояние» вы откуда-то сами принесли ) Но я бы не сильно налегал тут, потому что снова же – при любом малейшем изменении требований, «свойство» легко превращается в «состояние», и фундаментального отличия между ними нет. Это просто код по разному пытается на этих отличиях оптимизировать внутренные процессы отрисовки и менеджмента стейта.

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


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

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

Я думаю что Dart если не дропнут в следющих релизах Flutter, то добавят рядом kotlin. Go думаю не добавят.
Наткнулся на Ваши статьи по GO и вот сюда по итогу выплыл )
Очень и очень рад знакомству!

Спасибо за потраченное время
Sign up to leave a comment.

Articles