Pull to refresh
187
0
divan0 @divan0

Пользователь

Send message

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

Классно написано, спасибо!


Если вы принципиально не используете Go Modules (например, в легаси-проекте)

Кстати, я стараюсь не использовать модули для open-source проектов – это стимулирует всегда держать ухо востро, чтобы они работали с последними версиями (через CI или пулл-реквесты "не билдится", гг). И вообще GOPATH это ️

Привет.


Смотрите, допустим вы пишете код, который должен работать с объектами, которые умеет рендерить (упомянутый вами метод Render()) для этого) — скажем UI фреймворк, в котором каждый виджет описывает как он рендерится.


Если у вас всего один такой тип (ну, там, type Window struct{}) – то интерфейс вам не нужен, вы просто везде указываете тип Window и с ним работаете. Компилятор знает, что у этого типа есть метод Render() и вызывает его когда надо.


Если же у вас 100 разных виджетов, то какой тип вы будете передавать в функциях внутри вашего кода? Например, у вас есть метод resizeWidget(w .???) { w.Render(); } — какого типа должен быть параметр w? Вам нужно как-то мочь логически обобщить все ваши виджеты в одну группу и сказать – они тут все подходят, докуда у них есть метод Render().


Вот именно тут и пригождаются интерфейсы – они говорят "плевать какой там конкретный тип, главное чтобы у него был метод Render()`.


Главный бенефит начинается от этого, когда у вас есть много разного кода, написанного разными командами/людьми в разное время. Например, ваш UI движок работающий с интерфейсом Widget (или Renderer) совершенно понятия не имеет какие ему будут передавать виджеты. Любой юзер через 5 лет может написать свой виджет и спокойно его использовать в вашем код – они decoupled и не зависят напрямую друг от друга. Движку не нужно знать ничего о типе вашего виджета, чтобы с ним работать.


Так немного яснее? :)

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

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


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

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


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

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

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

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

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


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


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


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


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


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

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

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


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

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


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

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

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

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));
                    }
                };
            }
        };
    }

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


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

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

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


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

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


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

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


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


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

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

т.к. мне постоянно приходиться работать с коллекциями в том или ином их виде (как и ооочень большой пласт программистов, я думаю) и я не хочу постоянно писать циклы для фильтрации\модификации списков. А 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 действительно спокойно цветет без дженериков. Которые авторы и коммьюнити и не против бы ввести, но без того, чтобы превращать язык в нечитабельного и медленного монстра, как почти все остальные языки – и такого способа пока не придумали, что не случайно.


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

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

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


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

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

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

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


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


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


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


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

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

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


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

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

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

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

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

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


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


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

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


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

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

Можно пруфы?

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


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

Ничуть.


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

Ок, спасибо.

1
23 ...

Information

Rating
Does not participate
Location
Barcelona, Barcelona, Испания
Date of birth
Registered
Activity