Т.е. ошибка так или иначе возвращается клиенту. Получается, что первый и второй случаи с точки зрения клиента отличаются лишь подробностью описания ошибки, а с точки зрения сервиса — уровнем логирования ошибки: в первом случае WARN — потому что система сама предотвратила некорректное поведение программы, отвалидировав входящие параметры; во втором случае — ERROR, потому что несмотря на наши усилия некорректное поведение все таки произошло.
Но в обоих случаях способ обработки ошибки один и тот же — возврат этой самой ошибки клиенту, для того чтобы он самостоятельно обработал ее на своей стороне (сделал ретрай\поправил код\написал вашему суппорту). И это противоречит вашему утверждению, что запись в лог является обработкой ошибки.
По поводу первого типа: почему мы не должны сохранять у себя информацию о том, что от клиента пришел запрос с невалидными параметрами, о чем он был соответствующим образом уведомлен? Я вижу только один случай, при котором такой подход может быть оправдан: клиентский сервис — это какой-то из ваших микросервисов и он сам залоггирует ошибку, полученную от другого сервиса. В противном случае логгирование и мониторинг подобных ошибок как минимум понадобится при коммуникации с командой разработчиков другого сервиса.
По поводу второго типа: в каких случаях мы не должны уведомлять клиента о «необработанных ошибках», хотя бы теми же кодами http-статусов?
Как уведомлять разработчика программы, о том что произошла ошибка? Разработчик другой системы, получив респанс с ошибкой, должен писать в суппорт вашей программы? Или все таки само приложение должно залогировать ошибку(в файл, базу, сентри или что-нибудь похожее), а разработчик, получив уведомление от какой-либо системы мониторинга, должен сесть и разбираться с ним?
Кроме того, рассматривать сообщение об ошибке в отрыве от логов других уровней может быть очень неудобным, т.к. прийдется по таймпшампу\id сессии или реквеста узнавать что именно приложение делало в определенный момент времени и строить теории относительно того, на каком именно этапе произошла ошибка. Сохранить всю эту информацию в самой ошибке может быть невозможным в силу особенностей алгоритма\архитектуры\неожиданностью появления конкретно этой ошибки.
Забавно, как ваши Java-реализации несовместимы друг с другом без приведения типов
Поясните, пожалуйста, что вы имеете в виду. Я не вижу в своем примере явного приведения типов — ни в самой реализации метода, ни в примерах его использования. Но признаю, что есть опечатка в коде — это, само собой, не filter, а map, и еще можно было бы заиспользовать super/extends для еще большой универсальности.
Но нет, в попытке обобщить (DRY! цикл писать два раза – зло!) мы создаём (есть же дженерики, значит надо всё дженерилизовать!) реализацию map(), придумав для этого новую концепцию Streams
Концепция стримов была добавлена как часть функциональной парадигмы. Она отличается от процедурной, к ней предъявляются другие, особые требования, на которые еще и накладываются требование сохранения обратной совместимости с предыдущими версиями языка. Именно поэтому их реализация выглядит так громоздко, а не потому что дженерики ухудшают читабельность в общем случае.
Я плохо ориентируюсь в исходниках го, но убежден что там тоже полно кода, на понятие которого требуется время — и это абсолютно нормально, потому что реализация языка программирования и системных возможностей не может быть простой. И сокрытие сложности за апи не являться недостатком.
что если вам нужно обработать конкретный json и передать дальше – вам не нужно писать «универсальную структуру»
К сожалению выше у меня не получилось выразиться более ясно — в моих комментариях речь не идет о том, что нужно постоянно писать новые структуры, нет, речь идет только о переиспользовании существующего кода и уменьшению количества бойлерплейта. С тем, что реализация новых структур данных в проекте — это очень редкий юзкейс я в полной мере согласен, но речь идет, еще раз, об использовании. Прочитать абстрактный набор данных, выбрать из него только лишь необходимое, обработать и передать дальше — вы ведь согласны что это чуть ли не самый частый кейс в программировании? И из-за частоты этого кейса вы будете очень часто повторять одни и те же конструкции, которые будут зашумливать самое важное — бизнес логику работы. Весь этот шум можно и нужно прятать за каким-либо апи, но го не позволяет это сделать полноценным образом. И речь идет вовсе не о принципе DRY, а об упрощении понимания написанного кода.
Представьте, что у вас отобрали проверку совместимости типов в методе append и вам приходится каждый раз самостоятельно делать тайп чек или оборачивать вызов этого метода во враппер. Но в append проверка есть — вот только лично мне недостаточно проверки только лишь в этом методе, мне ее не хватает в filter/map/reduce и во всех остальных местах. И мне не нравится то, что лишь разработчики языка выбирают список мест, в котором тайп чек будет, потому что я тоже хочу так делать. И снова повторюсь — я знаю как применять систему типов и интерфейсов в го, но как как бы ты не использовал интерфейсы — полноценно заменить дженерики ими не получится.
переиспользования дженериков по поводу и без повода – как раз из-за отстутствия чего Go и выигрывает в читабельности и понятности
Можно обходиться необходимым минимумом функциональных возможностей, которые предоставляет го. Можно обкладываться всем этим бойлерплейтом и даже можно натренировать мозг игнорировать весь этот шум в коде. Но ведь само присутствие этого шума в коде — это уже неприятно. Шум и читабельность просто не совместимы.
А переиспользовать и доводить до абсурда можно практически любые конструкции практически в любом языке. Но эти самые конструкции так же дают возможность писать поддерживаемый, элегантный и читаемый код. Я понимаю чего опасаетесь вы и авторы языка, но сдвигание баланса в сторону минимализма так же и уменьшает сферу применения языка(речь не столько про сферы практического применения в целом, сколько про требования предъявляемые к языку и к коду конкретной командой в конкретном проекте) и количество человек, желающим с ним работать.
Но вы ведь понимаете, что это не равнозначные примеры? Вы сравниваете свой «хелпер-метод» с методом из 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;
}
Столько же строк, но при этом метод на джаве можно использовать с любыми типами данных, а не только со строками. И на мой взгляд вы несколько переоцениваете ту сложность, которую привносят дженерики в год.
В итоге все сводится к тому, что у нас разные представления о красоте и читабельности кода.
Для такого дженерики вообще не нужны ни разу. Вы сейчас хорошо показали, в чём проблема – начинается с «я хочу писать реюзабельные структуры данных», а заканчивается «я не могу массив из json обработать без дженериков». И это проблема. :D
Где-то было сказано про невозможность обработать этот кейс с помощью го? Или что конкретно я не могу это сделать? Мой аргумент начинался с того, что я не могу написать универсальную структуру — не важно список, дерево или что-то еще — которую смогу переиспользовать типобезопасным образом и без бойлерплейта. Проблема именно в этом, а не в чем-то другом, так что давайте обойдемся без пассивно-агрессивных нападок на мой уровень программирования, хорошо?
Напомню почему считаю, что это проблема — потому что ручное приведение типов не удобно и не безопасно, и оба этих критерия очень важны для меня.
Понимаешь, что там три вложенных цикла (разворачиваешь бойлерплейт у себя в голове, что есть дополнительной когнитивной нагрузкой). Или не понимаешь, конечно – и лепишь монстроидальные однострочные конструкции .map.filter.map.collect..., а потом удивляешься, почему всё так медленно работает.
В случае с filter вы осознаете работу цикла прочитав 6 символов, а не несколько строчек (объявление промежуточного слайса, итератор, проверка условия, добавление объекта в промежуточный слайс), т.е. когнитивная нагрузка будет ниже, еще и глазами можно меньше двигать. С последующими map/reduce/collect — то же самое. В результате, вы можете добиться того, что весь пайп обработки у вас помещается на один экран, и осознать что там происходит — дело нескольких секунд. И не забываем про те 15 секунд, которые требуются на написание каждого подобного цикла. А уж что выгоднее экономить — такты процессора, или рабочее время коллег каждый выбирает самостоятельно.
То что это в мозгу разворачивается в циклы, происходит примерный подсчет сложности этого алгоритма и т.д. и т.п. — когда вы читаете литературу на любом известном вам языке в вашем могу происходят те же самые процессы. Не думаю, что это отнимает много времени, или что это доставляет вам хоть какое-то неудобство. Но при этом при письменном обмене информацией все еще сильно ценится лаконичность. В некоторых случаях это доводится до абсолюта — вспомнить те же карточки для запоминания иностранных слов, когда на оборотной стороне вместе с переводом присутствует картинка-значение. В общем подобное разворачивание хорошо знакомого метода в мозгу — это обычный приобретаемый навык, который оттачивается до рефлексов.
По поводу быстро или медленно — но это же просто итерации по коллекциям, при чем обычно вам заранее будет известен примерный объем данных, которые необходимо обработать — по крайней мере их порядок (из тз, спецификации сервиса или из вашего собственного практического опыта). Обычно вы заранее в состоянии прикинуть узкие места вашего алгоритма — при чем в 99% случаях узким горлом будут не итерации по коллекции, а какое-либо IO. Так зачем зачем заставлять заранее оптимизировать этот 1% случаев, если это можно сделать потом, и то при условии, что умный компилятор или рантайм не сделают этого за вас? Зачем заставлять экономить пару тактов процессора, если можно сэкономить несколько минут работы вашим коллегам — рабочее время которых ценится выше, чем эти самые такты?
Это же техническое решение было, а не политическое.
Я не осуждаю авторов го. Моя позиция — не оспаривание решения разработчиков го, а лишь «фичареквест» реализации которой я очень жду. Я не сомневаюсь в опыте этих людей и я также прекрасно осознаю сложности связанные с добавлением поддержки дженериков, и уж само собой я знаю как в go выживать без дженериков. Но почему-то в обсуждении вопроса «какую конкретно проблему могут решить дженерики» на мой вполне себе конкретный пример мне начали говорить, что проблема по большой части надуманная, случай редкий, и вообще я либо не умею программировать, либо просто еще не привык к го. А дженерики — это сложно, люди сразу же начнут говнокодить и писать непонятные штуки. В общем касти вручную, пиши боллерплейт и радуйся жизни (сразу же прошу прощения за ёрничанье).
Первый раз вижу подобную реакцию от комьюнити в обсуждении фича-реквеста. Если зайти к тому же джава-комьюнити и сказать что ты ждешь добавления элвис-оператора или интерполяции строк — тебе никто не скажет, что if'ов и String.format в 99% случаев хватает, скорее скажут что Oracle #$%!& и пора уходить на котлин\c#.
Я не против дженериков, я говорю, что все, кто утверждает, что го без дженериков не нужен и что они там необходимы
Ну вы тоже перевираете, ведь в этой ветке никто такое и не говорил (по крайней мере я такого не говорил). Начиная с вашего комментария идет обсуждение проблем, которые могли бы решить дженерики если бы их добавили в го.
не могут аргументировать это
Почему вы не принимаете аргумент с невозможностью переиспользовать код без дополнительного бойлреплейта? Неужели это столь несущественная проблема?
какие были бы в других языках (в которых есть), убрав их оттуда.
Раз уж мы так любим примеры, можете привести пример какой-нибудь реальной проблемы и реальных случаев при которых она доставляет неудобства?
И тут, мне кажется, мы снова приходим к вопросу о частоте использования обсуждаемых кейсов.
Как часто вам приходится использовать 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() ты сразу понимаешь какие именно действия совершаются в указанном куске кода.
Извините, но я не могу по-другому воспринимать его комментарий из другой ветки:
В случае с Go дженерики могли бы в некоторых случаях упростить жизнь, но это не серебряная пуля, и в большинстве практических задач они не нужны вообще. В рамках реального проекта интерфейсов (нормальных, с описанием методов) достаточно, чтобы спокойно жить и не беспокоиться о том, что в каком-то другом языке дженерики есть а тут нет.
Просто чтобы уточнить — я не хейтер го. Я писал на нем экспортеры для прометеуса и небольшие сервисы для проксирования\балансировки запросов и я не испытывал особого отвращения от языка в этих кейсах.
Но перекладывая весь свой опыт в Java/Groovy на Golang, я понимаю, что он неприменим(точнее слишком уж неудобен) в большой части случаев из моей практики. И по большей части причина этого как раз таки в отсутствии дженериков, т.к. мне постоянно приходиться работать с коллекциями в том или ином их виде (как и ооочень большой пласт программистов, я думаю) и я не хочу постоянно писать циклы для фильтрации\модификации списков. А golang не позволяет мне вынести весь этот бойлерплейт в хелпер-методы, сохранив при этом безопасность работы с типами. Так же как не позволяет создать удобные унифицированные структуры данных.
Отсутствие дженериков — т.е. отсутствие возможности реюзать чужой код(в общем случае) — это проблема, как для меня так и для множества других разработчиков. И система типов\интерфейсов го никак не позволяет решить эту проблему. Это все к вопросу про то, какие бы проблемы решили дженерики если бы их добавили в язык.
Но в реальности это происходит не настолько часто.
Как бы то ни было, но в данном случае отсутствие дженериков не позволяет реюзать код (по крайней мере без каких-либо доделок, типо оборачивания во враппер), а это существенный довод в пользу дженериков.
В первом случае даже в языке, который предоставляет дженерики, придётся писать для каждого объекта, который попадает в шину, свои методы/свойства и в Go это можно решить требованием объекта предоставлять некоторый интерфейс.
В случае с Go вы будете пушить\пулить в\из шины какой-то условный абстрактный Loggable, а вам нужна шина, которая работает с конкретной имплементацией этого Loggable — условным Request. Т.е. опять приходится либо оборачивать шину в обертку, либо снова каждый раз приводить типы вручную.
Да, опять же, без этого можно жить и с этим можно смириться. Но считать, что без дженериков удобней чем с ними — извините, но я не понимаю этого. Тем более почему-то в go можно объявить с каким типом объектов работают каналы\массивы\слайсы — так почему бы не дать разработчикам возможность делать то же самое, но и с другими структурами?
Вы выкидываете sync.Map, оборачиваете обычный map[UUID]UserStruct в структуру с RWMutex, добавляете в эту структуру поле со счётчиком и вместо методов Delete/Load с интерфейсами в качестве параметра и возвращаемого значения делаете свои обёртки, которые принимают UUID как ключ и возвращают указатели на структуру пользователя.
И каждый раз, когда вам понадобится простейшая реализация кеша с синхронизированным доступом и метриками вам прийдется писать этот бойлерплейт заново. Ну либо пользоваться interface {} и писать тесты, которые будут проверять, что вы нигде не сделали опечатку и не прикастили объект к неправильному типу.
Другой пример: опять же, простейшая реализация message bus с теми же самыми метриками и логгированием. В каждом конкретном случае вы знаете какой интерфейс реализуют сообщения в этой шине, но написать универсальную шину без дженериков у вас не получится.
Pipe-фреймворки и пулы объектов — при наличии метрик и логгирования уже простыми каналами\слайсами не обойтись. И либо в каждом проекте пишутся заново, либо используется готовая реализация с interface{}, использовать который — дурной тон.
И совсем уже реальный пример из моей практики: пара деревьев, одно хранит содержимое корзины (речь идет про ретейл), другое дерево — сложные правила расчета маржей. Правила обхода деревьев и их балансировки отличаются, как и типы хранимых данных. При чем второе дерево расшарено между потоками, т.е. должно быть синхронизированным. Если реализовывать без interface{}, то получатся две +- одинаковых структуры и две группы методов работы с ними.
И вот, вы сидите со многостраничной спецификации по модулю, но вместо реализаций требований заказчика реализуете структуры данных\методы (обязательно эффективные реализации, а в случае с обходом и балансировкой деревьев это может быть достаточно сложный алгоритм), хотя могли бы воспользоваться проверенной библиотекой от какого-либо вендора.
Да, можно использовать библиотечные реализации структур данных с interface{}, и спрятать из за оберткой с конкретными интерфейсами, но это workaround, а не решение.
Почему вас не устраивает пример с sync.Map? Из-за отсутствия дженериков приходится постоянно приводить значение к нужному типу, а это и менее удобно, и менее безопасно. Тоже самое и с любыми другими универсальными структурами данных и методов работы с ними.
Без дженериков, конечно, можно жить. Но с ними гораздо удобней, т.к. компилятор берет на себя проверку совместимости типов.
Говоря, что образование в университетах в упадке почему вы не вспомнили ICPC и то, что наши вузы за последние 16 лет 10 раз были победителями соревнования? Получается все таки есть хорошие вузы в стране? Тогда почему бы вам не не смягчить ваши формулировки?
«университеты преподают то, что уже не используется в мейнстриме» — нужно открывать кафедру Erlang'а в вузах? Изучать языки\технологии и расширять кругозор — это по-большей части задача студента.
Но в обоих случаях способ обработки ошибки один и тот же — возврат этой самой ошибки клиенту, для того чтобы он самостоятельно обработал ее на своей стороне (сделал ретрай\поправил код\написал вашему суппорту). И это противоречит вашему утверждению, что запись в лог является обработкой ошибки.
По поводу второго типа: в каких случаях мы не должны уведомлять клиента о «необработанных ошибках», хотя бы теми же кодами http-статусов?
Кроме того, рассматривать сообщение об ошибке в отрыве от логов других уровней может быть очень неудобным, т.к. прийдется по таймпшампу\id сессии или реквеста узнавать что именно приложение делало в определенный момент времени и строить теории относительно того, на каком именно этапе произошла ошибка. Сохранить всю эту информацию в самой ошибке может быть невозможным в силу особенностей алгоритма\архитектуры\неожиданностью появления конкретно этой ошибки.
Поясните, пожалуйста, что вы имеете в виду. Я не вижу в своем примере явного приведения типов — ни в самой реализации метода, ни в примерах его использования. Но признаю, что есть опечатка в коде — это, само собой, не filter, а map, и еще можно было бы заиспользовать super/extends для еще большой универсальности.
Концепция стримов была добавлена как часть функциональной парадигмы. Она отличается от процедурной, к ней предъявляются другие, особые требования, на которые еще и накладываются требование сохранения обратной совместимости с предыдущими версиями языка. Именно поэтому их реализация выглядит так громоздко, а не потому что дженерики ухудшают читабельность в общем случае.
Я плохо ориентируюсь в исходниках го, но убежден что там тоже полно кода, на понятие которого требуется время — и это абсолютно нормально, потому что реализация языка программирования и системных возможностей не может быть простой. И сокрытие сложности за апи не являться недостатком.
К сожалению выше у меня не получилось выразиться более ясно — в моих комментариях речь не идет о том, что нужно постоянно писать новые структуры, нет, речь идет только о переиспользовании существующего кода и уменьшению количества бойлерплейта. С тем, что реализация новых структур данных в проекте — это очень редкий юзкейс я в полной мере согласен, но речь идет, еще раз, об использовании. Прочитать абстрактный набор данных, выбрать из него только лишь необходимое, обработать и передать дальше — вы ведь согласны что это чуть ли не самый частый кейс в программировании? И из-за частоты этого кейса вы будете очень часто повторять одни и те же конструкции, которые будут зашумливать самое важное — бизнес логику работы. Весь этот шум можно и нужно прятать за каким-либо апи, но го не позволяет это сделать полноценным образом. И речь идет вовсе не о принципе DRY, а об упрощении понимания написанного кода.
Представьте, что у вас отобрали проверку совместимости типов в методе append и вам приходится каждый раз самостоятельно делать тайп чек или оборачивать вызов этого метода во враппер. Но в append проверка есть — вот только лично мне недостаточно проверки только лишь в этом методе, мне ее не хватает в filter/map/reduce и во всех остальных местах. И мне не нравится то, что лишь разработчики языка выбирают список мест, в котором тайп чек будет, потому что я тоже хочу так делать. И снова повторюсь — я знаю как применять систему типов и интерфейсов в го, но как как бы ты не использовал интерфейсы — полноценно заменить дженерики ими не получится.
Можно обходиться необходимым минимумом функциональных возможностей, которые предоставляет го. Можно обкладываться всем этим бойлерплейтом и даже можно натренировать мозг игнорировать весь этот шум в коде. Но ведь само присутствие этого шума в коде — это уже неприятно. Шум и читабельность просто не совместимы.
А переиспользовать и доводить до абсурда можно практически любые конструкции практически в любом языке. Но эти самые конструкции так же дают возможность писать поддерживаемый, элегантный и читаемый код. Я понимаю чего опасаетесь вы и авторы языка, но сдвигание баланса в сторону минимализма так же и уменьшает сферу применения языка(речь не столько про сферы практического применения в целом, сколько про требования предъявляемые к языку и к коду конкретной командой в конкретном проекте) и количество человек, желающим с ним работать.
А аналогом вашему коду — т.е. без проверок на null и различных стратегий обработки упорядоченных\неупорядоченных списков — будет выглядеть так:
Столько же строк, но при этом метод на джаве можно использовать с любыми типами данных, а не только со строками. И на мой взгляд вы несколько переоцениваете ту сложность, которую привносят дженерики в год.
Где-то было сказано про невозможность обработать этот кейс с помощью го? Или что конкретно я не могу это сделать? Мой аргумент начинался с того, что я не могу написать универсальную структуру — не важно список, дерево или что-то еще — которую смогу переиспользовать типобезопасным образом и без бойлерплейта. Проблема именно в этом, а не в чем-то другом, так что давайте обойдемся без пассивно-агрессивных нападок на мой уровень программирования, хорошо?
Напомню почему считаю, что это проблема — потому что ручное приведение типов не удобно и не безопасно, и оба этих критерия очень важны для меня.
В случае с filter вы осознаете работу цикла прочитав 6 символов, а не несколько строчек (объявление промежуточного слайса, итератор, проверка условия, добавление объекта в промежуточный слайс), т.е. когнитивная нагрузка будет ниже, еще и глазами можно меньше двигать. С последующими map/reduce/collect — то же самое. В результате, вы можете добиться того, что весь пайп обработки у вас помещается на один экран, и осознать что там происходит — дело нескольких секунд. И не забываем про те 15 секунд, которые требуются на написание каждого подобного цикла. А уж что выгоднее экономить — такты процессора, или рабочее время коллег каждый выбирает самостоятельно.
То что это в мозгу разворачивается в циклы, происходит примерный подсчет сложности этого алгоритма и т.д. и т.п. — когда вы читаете литературу на любом известном вам языке в вашем могу происходят те же самые процессы. Не думаю, что это отнимает много времени, или что это доставляет вам хоть какое-то неудобство. Но при этом при письменном обмене информацией все еще сильно ценится лаконичность. В некоторых случаях это доводится до абсолюта — вспомнить те же карточки для запоминания иностранных слов, когда на оборотной стороне вместе с переводом присутствует картинка-значение. В общем подобное разворачивание хорошо знакомого метода в мозгу — это обычный приобретаемый навык, который оттачивается до рефлексов.
По поводу быстро или медленно — но это же просто итерации по коллекциям, при чем обычно вам заранее будет известен примерный объем данных, которые необходимо обработать — по крайней мере их порядок (из тз, спецификации сервиса или из вашего собственного практического опыта). Обычно вы заранее в состоянии прикинуть узкие места вашего алгоритма — при чем в 99% случаях узким горлом будут не итерации по коллекции, а какое-либо IO. Так зачем зачем заставлять заранее оптимизировать этот 1% случаев, если это можно сделать потом, и то при условии, что умный компилятор или рантайм не сделают этого за вас? Зачем заставлять экономить пару тактов процессора, если можно сэкономить несколько минут работы вашим коллегам — рабочее время которых ценится выше, чем эти самые такты?
Я не осуждаю авторов го. Моя позиция — не оспаривание решения разработчиков го, а лишь «фичареквест» реализации которой я очень жду. Я не сомневаюсь в опыте этих людей и я также прекрасно осознаю сложности связанные с добавлением поддержки дженериков, и уж само собой я знаю как в go выживать без дженериков. Но почему-то в обсуждении вопроса «какую конкретно проблему могут решить дженерики» на мой вполне себе конкретный пример мне начали говорить, что проблема по большой части надуманная, случай редкий, и вообще я либо не умею программировать, либо просто еще не привык к го. А дженерики — это сложно, люди сразу же начнут говнокодить и писать непонятные штуки. В общем касти вручную, пиши боллерплейт и радуйся жизни (сразу же прошу прощения за ёрничанье).
Первый раз вижу подобную реакцию от комьюнити в обсуждении фича-реквеста. Если зайти к тому же джава-комьюнити и сказать что ты ждешь добавления элвис-оператора или интерполяции строк — тебе никто не скажет, что if'ов и String.format в 99% случаев хватает, скорее скажут что Oracle #$%!& и пора уходить на котлин\c#.
Ну вы тоже перевираете, ведь в этой ветке никто такое и не говорил (по крайней мере я такого не говорил). Начиная с вашего комментария идет обсуждение проблем, которые могли бы решить дженерики если бы их добавили в го.
Почему вы не принимаете аргумент с невозможностью переиспользовать код без дополнительного бойлреплейта? Неужели это столь несущественная проблема?
Раз уж мы так любим примеры, можете привести пример какой-нибудь реальной проблемы и реальных случаев при которых она доставляет неудобства?
Как часто вам приходится использовать iota (ну не считаю я этот огрызок заменой enum'ов) или кодогенерацию? Лично на мой взгляд это на столько специфичные вещи, необходимость в которых возникает очень редко, однако они добавлены в язык. А дженерики — нет.
В случае с деревьями — да, очень редко. Буквально пару-тройку раз за всю практику. Но вот очереди, кеши, листы — существующие в проекте я использую каждый день, добавляю новые экземпляры в проект — ну тут прийдется взять цифру с потолка, потому что никогда не обращаю на это внимание. Скажем, условные пару раз в месяц. По поводу упомянутых хелпер-методов — использую во множестве раз каждый день, будь то библиотечные методы, или написанные самостоятельно. А так же есть Optional'ы и функциональные интерфейсы, которые так же очень способствуют написанию лаконичного и понятного кода. И без дженериков они не реализуемы.
Однако это бойлерплейт, и от его присутствия проекта хочется избавиться. По поводу вероятности ошибиться — на хабре есть блог компании pvs studio, в котором они выкладывают результаты анализов различных проектов, и практически в каждом репорте присутствует категория ошибок с вязанных с опечатками и copy-paste ошибками. В случае с go кто-нибудь может скопировать существующий код, и заменить привести к другому типу объектов. И я не говорю уже про многословность подобных решений с обертками, которая замыливает бизнеслогику.
Фундаментальная разница в том, что вам самим прийдется взять на себя проверку совместимости типов. Как я уже говорил выше с этим можно жить, но это не удобно и не безопасно.
Простейшая задача — получить от внешнего сервиса коллекцию объектов, обработать их, трансформировать в нужный вид и передать дальше. Если менее абстрактный пример: получаем от внешнего сервиса список товаров в определенном виде, фильтруем по какому-либо параметру (категория, цена или что угодно еще), трансформируем в объект нужного типа и передаем куда-то дальше. Это не лабораторная работа и не какая-нибудь биг-дата — это чуть ли не самый популярный кейс в ретейле.
Я не могу считать бойлерплейт из навороченных и вложенных циклов читабельным. Описанный выше кейс в Java решается в 5-10 строк с помощью stream-api или той же гуавы\rxjava. И видя в коде цепочку filter().map().collect() ты сразу понимаешь какие именно действия совершаются в указанном куске кода.
Просто чтобы уточнить — я не хейтер го. Я писал на нем экспортеры для прометеуса и небольшие сервисы для проксирования\балансировки запросов и я не испытывал особого отвращения от языка в этих кейсах.
Но перекладывая весь свой опыт в Java/Groovy на Golang, я понимаю, что он неприменим(точнее слишком уж неудобен) в большой части случаев из моей практики. И по большей части причина этого как раз таки в отсутствии дженериков, т.к. мне постоянно приходиться работать с коллекциями в том или ином их виде (как и ооочень большой пласт программистов, я думаю) и я не хочу постоянно писать циклы для фильтрации\модификации списков. А golang не позволяет мне вынести весь этот бойлерплейт в хелпер-методы, сохранив при этом безопасность работы с типами. Так же как не позволяет создать удобные унифицированные структуры данных.
Отсутствие дженериков — т.е. отсутствие возможности реюзать чужой код(в общем случае) — это проблема, как для меня так и для множества других разработчиков. И система типов\интерфейсов го никак не позволяет решить эту проблему. Это все к вопросу про то, какие бы проблемы решили дженерики если бы их добавили в язык.
Как бы то ни было, но в данном случае отсутствие дженериков не позволяет реюзать код (по крайней мере без каких-либо доделок, типо оборачивания во враппер), а это существенный довод в пользу дженериков.
В случае с Go вы будете пушить\пулить в\из шины какой-то условный абстрактный Loggable, а вам нужна шина, которая работает с конкретной имплементацией этого Loggable — условным Request. Т.е. опять приходится либо оборачивать шину в обертку, либо снова каждый раз приводить типы вручную.
Да, опять же, без этого можно жить и с этим можно смириться. Но считать, что без дженериков удобней чем с ними — извините, но я не понимаю этого. Тем более почему-то в go можно объявить с каким типом объектов работают каналы\массивы\слайсы — так почему бы не дать разработчикам возможность делать то же самое, но и с другими структурами?
И каждый раз, когда вам понадобится простейшая реализация кеша с синхронизированным доступом и метриками вам прийдется писать этот бойлерплейт заново. Ну либо пользоваться interface {} и писать тесты, которые будут проверять, что вы нигде не сделали опечатку и не прикастили объект к неправильному типу.
Другой пример: опять же, простейшая реализация message bus с теми же самыми метриками и логгированием. В каждом конкретном случае вы знаете какой интерфейс реализуют сообщения в этой шине, но написать универсальную шину без дженериков у вас не получится.
Pipe-фреймворки и пулы объектов — при наличии метрик и логгирования уже простыми каналами\слайсами не обойтись. И либо в каждом проекте пишутся заново, либо используется готовая реализация с interface{}, использовать который — дурной тон.
И совсем уже реальный пример из моей практики: пара деревьев, одно хранит содержимое корзины (речь идет про ретейл), другое дерево — сложные правила расчета маржей. Правила обхода деревьев и их балансировки отличаются, как и типы хранимых данных. При чем второе дерево расшарено между потоками, т.е. должно быть синхронизированным. Если реализовывать без interface{}, то получатся две +- одинаковых структуры и две группы методов работы с ними.
И вот, вы сидите со многостраничной спецификации по модулю, но вместо реализаций требований заказчика реализуете структуры данных\методы (обязательно эффективные реализации, а в случае с обходом и балансировкой деревьев это может быть достаточно сложный алгоритм), хотя могли бы воспользоваться проверенной библиотекой от какого-либо вендора.
Да, можно использовать библиотечные реализации структур данных с interface{}, и спрятать из за оберткой с конкретными интерфейсами, но это workaround, а не решение.
Без дженериков, конечно, можно жить. Но с ними гораздо удобней, т.к. компилятор берет на себя проверку совместимости типов.
«университеты преподают то, что уже не используется в мейнстриме» — нужно открывать кафедру Erlang'а в вузах? Изучать языки\технологии и расширять кругозор — это по-большей части задача студента.