Мысленный эксперимент: Flutter на Go

    Совсем недавно я открыл для себя Flutter – новый фреймворк от Google для разработки кроссплатформенных мобильных приложений – и даже имел возможность показать основы Flutter человеку, который никогда не программировал до этого. Сам Flutter написан на Dart – языке, родившимся в браузере Chrome и сбежавшим в мир консоли –  и это навело меня на мысль "хм, а ведь Flutter мог вполне бы быть написан на Go!".


    Ведь почему нет? И Go и Dart созданы Google, оба типизированные компилируемые языки – повернись некоторые события чуть иначе, Go был бы отличным кандидатом для реализации такого масштабного проекта, как Flutter. Кто-то скажет – в Go нет классов, дженериков и исключений, поэтому он не подходит.


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



    Что не так с Dart?


    Я следил за этим языком с самого его зарождения в качестве альтернативы JavaScript в браузерах. Dart какое-то время был встроен в браузер Chrome и надежда была на то, что он вытеснит JS. Безумно грустно было читать в марте 2015 года, что поддержка Dart была убрана из Chrome.


    Сам Dart великолепен! Ну, в принципе, после JavaScript любой язык великолепен, но после, скажем, Go, Dart не настолько прекрасен. но вполне ок. В нём есть все мыслимые и немыслимые фичи – классы, дженерики, исключения, futures, async-await, event loop, JIT/AOT, сборщик мусора, перегрузка функций – назовите любую известную фичу из теории языков программирования и в Dart она будет с высокой долей вероятности. У Dart есть специальный синтаксис для почти любой фишки – специальный синтаксис для геттеров/сеттеров, специальный синтаксис для сокращённых конструкторов, специальный синтаксис для специального синтаксиса и много чего другого.


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


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

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


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


    Что не так с Go?


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


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


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


    Некоторые языки изначально разрабатывались с такими задачами на прицеле, но не Go. Похоже, что написать Flutter на Go будет та ещё задача!


    Ода Flutter


    Если вы ещё не знакомы с Flutter, то я настойчиво рекомендую потратить следующие выходные за просмотром обучающих видео или чтении туториалов, коих множество. Потому как Flutter, безо всякого сомнения, переворачивает правила игры в разработке мобильных приложений. И, вполне вероятно, не только мобильных – уже есть рендереры (в терминах Flutter, embedders) для того, чтобы запускать Flutter приложения как нативные dekstop-приложения, и как веб-приложения.


    Он легко учится, он логичен, идёт с огромнейшей библиотекой красивейших виджетов на Material Design (и не только), у него великолепное и большое коммьюнити и отличный тулинг (если вам нравится легкость работы с go build/run/test в Go, то в Flutter вы получите похожий опыт).


    Ещё год назад мне нужно было написать небольшое мобильное приложение (под iOS и Android, разумеется), и я понимал, что сложность разработки качественного приложения под обе платформы слишком велика (приложение было не основной задачей) – пришлось аутсорсить и платить за него деньги. По факту, написать несложное, но качественное и работающее на всех устройствах приложение было неподъемной задачей даже для человека с почти 20 летним опытом программирования. И это всегда был нонсенс для меня.


    С Flutter я переписал это приложения за 3 вечера, при этом изучая сам фреймворк с нуля. Если бы мне кто-то рассказал что так может быть чуть ранее, я бы не поверил.


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


    Так что рекомендую начать изучение Flutter и вот этот туториал очень хорош.


    "Hello, World" на Flutter


    Когда вы создаёте новое приложение через flutter create, вы получите вот такую программу с заголовком, текстом, счётчиком и кнопкой, увеличивающей счётчик.



    Мне кажется это отличный пример. чтобы написать его на нашем воображаемом Flutter на Go. В нём есть почти все основные концепты фреймворка, на которых можно проверить идею. Давайте посмотрим на код (это один файл):


    import 'package:flutter/material.dart';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: MyHomePage(title: 'Flutter Demo Home Page'),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      MyHomePage({Key key, this.title}) : super(key: key);
      final String title;
    
      @override
      _MyHomePageState createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      int _counter = 0;
    
      void _incrementCounter() {
        setState(() {
          _counter++;
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text(
                  'You have pushed the button this many times:',
                ),
                Text(
                  '$_counter',
                  style: Theme.of(context).textTheme.display1,
                ),
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: _incrementCounter,
            tooltip: 'Increment',
            child: Icon(Icons.add),
          ),
        );
      }
    }

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


    Переводим код на Go


    Начало будет простым и незамысловатым – импорт зависимости и запуск функции main(). Ничего сложного или интересного тут, изменение практически синтаксическое:


    package hello
    
    import "github.com/flutter/flutter"
    
    func main() {
        app := NewApp()
        flutter.Run(app)
    }

    Единственное отличие лишь в том, что вместо запуска MyApp() — функции, которая является конструктором, которая есть специальной функцией, которая спрятана внутри класса с именем MyApp – мы просто вызываем обычную явную и не спрятанную функцию NewApp(). Она делает тоже самое, но гораздо понятней объяснять и понимать, что это такое, как запускается и как работает.


    Классы виджетов


    В Flutter всё состоит из виджетов. В Dart-версии Flutter каждый виджет реализован в виде класса, который наследует специальные классы для виджетов из Flutter.


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



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


    В Go мы представляем любую абстракцию в виде конкретного типа – структуры:


    type MyApp struct {
        // ...
    }

    В Dart-версии Flutter, MyApp должен унаследовать StatelessWidget и переопределить метод build. Это нужно для решения двух задач:


    1. дать нашему виджету (MyApp) некие специальные свойства/методы
    2. дать возможность Flutter вызывать наш код в процессе построения/рендеринга

    Я не знаю внутренностей Flutter, поэтому допустим, что пункт номер 1 не под вопросом, и мы просто должны это сделать. В Go для такой задачи есть единственное и очевидное решение – встраивание (embedding) типов:


    type MyApp struct {
        flutter.Core
        // ...
    }

    Этот код добавит все свойства и методы flutter.Core к нашему типу MyApp. Я назвал его Core вместо Widget, потому что, во-первых, встраивание типа ещё не делает наш MyApp виджетом, а, во-вторых, это название очень удачно используется в GopherJS фреймворке Vecty (что-то вроде React, только для Go). Я коснусь темы похожести Vecty и Flutter чуть позднее.


    Второй момент – реализация метода build(), который сможет использовать движок Flutter – также в Go решается просто и однозначно. Мы лишь должны добавить метод с определённой сигнатурой, удовлетворяющей некоему интерфейсу, определённому где-нибудь в библиотеке нашего вымышленного Flutter на Go:


    flutter.go:


    type Widget interface {
        Build(ctx BuildContext) Widget
    }

    И теперь наш main.go:


    type MyApp struct {
        flutter.Core
        // ...
    }
    
    // Build renders the MyApp widget. Implements Widget interface.
    func (m *MyApp) Build(ctx flutter.BuildContext) flutter.Widget {
        return flutter.MaterialApp()
    }

    Мы можем заметить несколько отличий тут:


    • код несколько более многословен – BuildContext, Widget и MaterialApp указывают на импорт flutter перед ними.
    • код несколько менее голословен – нет слов вроде extends Widget или @override
    • метод Build() начинается с заглавной буквы, потому что это означает "публичность" метода в Go. В Dart публичность определяется тем, начинается имя со знака подчёркивания (_) или нет.

    Итак, чтобы сделать виджет в нашем Flutter на Go, нам необходимо встроить тип flutter.Core и реализовать интерфейс flutter.Widget. С этим разобрались, копаем дальше.


    Состояние


    Вот это была одна из вещей, которая меня сильно смутила во Flutter. Есть два разных класса – StatelessWidget и StatefulWidget. Как по мне, "виджет без состояния" это такой же виджет, просто без, хм, данных, состояния – зачем тут придумывать новый класс? Но окей, я могу с этим жить.


    Но дальше – больше, вы не можете просто так унаследовать другой класс (StatefulWidget), а должны написать вот такую магию (IDE сделает это за вас, но не суть):


    class MyHomePage extends StatefulWidget {
      @override
      _MyHomePageState createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      int _counter = 0;
    
      void _incrementCounter() {
        setState(() {
          _counter++;
        });
      }
    
      @override
      Widget build(BuildContext context) {
          return Scaffold()
      }
    }

    Мда уж, давайте разберемся, что тут происходит.


    Фундаментально задача стоит так: добавить к виджету состояние (state) –  счётчик, в нашем случае – и дать механизм движку Flutter узнавать, когда мы изменили состояние, чтобы перерисовать виджет. Это реальная сложность задачи (essential complexity в терминах Брукса).


    Всё остальное – это добавочная сложность (accidental complexity). Flutter на Dart придумывает новый класс State, который использует дженерики и принимает виджет в качестве параметра типа. Далее, создаётся класс _MyHomePageState, который наследует State виджета MyApp… окей, это ещё можно как-то переварить. Но почему метод build() определяется у класса State, а не у класса который виджет? Бррр....


    Ответ на этот вопрос есть в Flutter FAQ и достаточно подробно рассмотрен тут и краткий ответ – чтобы избежать определённого класса багов при наследовании StatefulWidget. Другими словами, это обходной путь для решения проблемы класс-ориентированного ООП дизайна. Шик.


    Как бы мы сделали это в Go?


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


    Задача, конечно же, состоит в том, чтобы дать Flutter возможность реагировать на изменение состояния (это суть реактивного программирования, как-никак). И если мы можем "попросить" разработчика использовать специальную функцию (setState()), то аналогично можем взамен попросить использовать специальную функцию, чтобы говорить движку, когда надо перерисовывать, а когда нет. В конце-концов, не все изменения состояния требуют перерисовки, и тут у нас будет даже больший контроль:


    type MyHomePage struct {
        flutter.Core
        counter int
    }
    
    // Build renders the MyHomePage widget. Implements Widget interface.
    func (m *MyHomePage) Build(ctx flutter.BuildContext) flutter.Widget {
        return flutter.Scaffold()
    }
    
    // incrementCounter increments widgets's counter by one.
    func (m *MyHomePage) incrementCounter() {
        m.counter++
        flutter.Rerender(m)
        // or m.Rerender()
        // or m.NeedsUpdate()
    }

    Можно поиграться с различными вариантами именования – мне нравится NeedsUpdate() за прямоту и тем, что это свойство виджета (полученное от flutter.Core), но глобальный метод flutter.Rerender() тоже выглядит неплохо. Правда он даёт ложное чувство того, что виджет вот прям немедленно перерисуется, но это не так – он перерисуется на следующем обновлении кадра, а частота вызова метода может быть сильно выше частоты отрисовки – но с этим уже должен разбираться наш движок Flutter.


    Но идея в том, что мы только что решили необходимую задачу без добавления:


    • нового типа
    • дженериков
    • специальных правил для чтения/записи состояния
    • специальных новых переопределённых методов

    Плюс, API намного яснее и понятнее – просто увеличиваем счётчик (как это делали ли бы в любой другой программе) и просим Flutter перерисовать виджет. Это как раз то, что не сильно очевидно, если бы мы просто вызывали setState – которая не просто специальная функция для установки состояния, это функция, которая возвращает функцию (wtf?), в которой мы уже что-то делаем с состоянием. Опять же, скрытая магия в языках и фреймворках сильно затрудняет понимание и читабельность кода.


    В нашем же случае, мы решили ту же задачу, код проще и короче в два раза.


    Виджеты с состоянием в других виджетах


    Как логическое продолжение темы, давайте взглянём, как "виджет с состоянием" используется в другом виджете в Flutter:


    @override
    Widget build(BuildContext context) {
        return MaterialApp(
            title: 'Flutter Demo',
            home: MyHomePage(title: 'Flutter Demo Home Page'),
        );
    }

    MyHomePage тут это "виджет с состоянием" (у него есть счётчик), и мы создаём его вызывая конструктор MyHomePage() во время билда… Постойте, что-что?


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


    Оказывается, Flutter использует это разделение между Widget и State для того, чтобы спрятать вот эту инициализацию/менеджмент состояния от программиста (больше спрятанных вещей, больше!). Он создаёт новый виджет каждый раз, но состояние, если уже было создано, находит автоматически и прикрепляет к виджету. Эта магия происходит невидимо и я без понятия, как именно это работает – надо читать код.


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


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


    // MyApp is our top application widget.
    type MyApp struct {
        flutter.Core
        homePage *MyHomePage
    }
    
    // NewMyApp instantiates a new MyApp widget
    func NewMyApp() *MyApp {
        app := &MyApp{}
        app.homePage = &MyHomePage{}
        return app
    }
    
    // Build renders the MyApp widget. Implements Widget interface.
    func (m *MyApp) Build(ctx flutter.BuildContext) flutter.Widget {
        return m.homePage
    }
    
    // MyHomePage is a home page widget.
    type MyHomePage struct {
        flutter.Core
        counter int
    }
    
    // Build renders the MyHomePage widget. Implements Widget interface.
    func (m *MyHomePage) Build(ctx flutter.BuildContext) flutter.Widget {
        return flutter.Scaffold()
    }
    
    // incrementCounter increments app's counter by one.
    func (m *MyHomePage) incrementCounter() {
        m.counter++
        flutter.Rerender(m)
    }

    Этот код проигрывает версии на Dart в том, что если я захочу убрать homePage из дерева виджетов и заменить на что-то другое, то мне придётся убирать его в трёх местах, вместо одного. Но взамен мы получаем полную картинку того, что, где и как происходит, где выделяется память, кто кого вызывает и так далее – код на ладони, понятен и легкочитаем.


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


    DSL


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


    Мне кажется вариант на Dart достаточно красив и красноречив:


    return Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text('You have pushed the button this many times:'),
                Text(
                  '$_counter',
                  style: Theme.of(context).textTheme.display1,
                ),
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: _incrementCounter,
            tooltip: 'Increment',
            child: Icon(Icons.add),
          ),
    );

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


    Именованные параметры


    На случай, если вы не знакомы с этим термином, то во многих языках параметры функции называются "позиционными", так как для функции имеет значение их позиция:


    Foo(arg1, arg2, arg3)

    , а в случае с именованными параметрами, всё решает их имя в вызове:


    Foo(name: arg1, description: arg2, size: arg3)

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


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


    return Scaffold(
          AppBar(
              Text(widget.title),
          ),
          Center(
            Column(
              MainAxisAlignment.center,
              <Widget>[
                Text('You have pushed the button this many times:'),
                Text(
                  '$_counter',
                  Theme.of(context).textTheme.display1,
                ),
              ],
            ),
          ),
          FloatingActionButton(
            _incrementCounter,
            'Increment',
            Icon(Icons.add),
          ),
        );

    Не то. правда? Его не только сложнее понимать (нужно держать в памяти, что означает каждый параметр и каков его тип, и это существенная когнитивная нагрузка), но и также не даёт нам свободы в выборе какие параметры мы хотим передать. Например, вы можете не хотеть для вашего Material приложения FloatingActionButton, поэтому вы просто его не указываете в параметрах. Без именованных параметров, нам придётся либо принуждать указывать все возможные виджеты, либо прибегать к магии с reflection, чтобы узнать, какие именно виджеты были переданы.


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


    Дерево виджетов в Go


    Версия 1


    Давайте ближе взглянем на объект Scaffold, который представляет из себя удобную обёртку для мобильного приложения. У него есть несколько свойств – appBar, drawe, home, bottomNavigationBar, floatingActionBar – и это всё виджеты. Создавая дерево виджетов, мы фактически должны как-то инициализировать этот объект, передав ему вышеупомянутые свойства-виджеты. Ну, это не слишком отличается от обычного создания и инициализации объектов.


    Давайте попробуем подход "в лоб":


    return flutter.NewScaffold(
        flutter.NewAppBar(
            flutter.Text("Flutter Go app", nil),
        ),
        nil,
        nil,
        flutter.NewCenter(
            flutter.NewColumn(
                flutter.MainAxisCenterAlignment,
                nil,
                []flutter.Widget{
                    flutter.Text("You have pushed the button this many times:", nil),
                    flutter.Text(fmt.Sprintf("%d", m.counter), ctx.Theme.textTheme.display1),
                },
            ),
        ),
        flutter.FloatingActionButton(
            flutter.NewIcon(icons.Add),
            "Increment",
            m.onPressed,
            nil,
            nil,
        ),
    )

    Не самый красивый UI код, однозначно. Слово flutter повсюду и так и просится. чтобы его спрятать (вообще-то, я должен был назвать пакет material, а не flutter, но не суть), безымянные параметры совершенно неочевидны, а эти nils повсюду откровенно сбивают с толку.


    Версия 2


    Поскольку всё равно большая часть кода будет использовать тот или иной тип/функцию из пакета flutter, мы можем использовать "точечный импорт" (dot import) формат, чтобы импортировать пакет в наше пространство имён и, тем самым, "спрятать" имя пакета:


    import . "github.com/flutter/flutter"

    Теперь вместо flutter.Text мы можем написать просто Text. Это обычно плохая практика, но мы же работает с фреймворком, и этот импорт будет буквально в каждой строчке. Из моей практики, это именно тот случай, для которого подобный импорт является допустимым – например, как при использовании замечательного фреймворка для тестирования GoConvey.


    Давайте посмотрим, как будет выглядеть код:


    return NewScaffold(
        NewAppBar(
            Text("Flutter Go app", nil),
        ),
        nil,
        nil,
        NewCenter(
            NewColumn(
                MainAxisCenterAlignment,
                nil,
                []Widget{
                    Text("You have pushed the button this many times:", nil),
                    Text(fmt.Sprintf("%d", m.counter), ctx.Theme.textTheme.display1),
                },
            ),
        ),
        FloatingActionButton(
            NewIcon(icons.Add),
            "Increment",
            m.onPressed,
            nil,
            nil,
        ),
    )

    Уже лучше, но эти nil-ы и неименованные параметры....


    Версия 3


    Давайте посмотрим, как будет выглядеть код, если мы используем reflection (возможность инспекции кода во время работы программы) для анализа переданных параметров. Такой подход используется в нескольких ранних HTTP-фреймворках на Go (martini, например), и считается очень плохой практикой – он небезопасен, теряет удобство системы типов, относительно медленный и добавляет магию в код – но ради эксперимента можно попробовать:


    return NewScaffold(
        NewAppBar(
            Text("Flutter Go app"),
        ),
        NewCenter(
            NewColumn(
                MainAxisCenterAlignment,
                []Widget{
                    Text("You have pushed the button this many times:"),
                    Text(fmt.Sprintf("%d", m.counter), ctx.Theme.textTheme.display1),
                },
            ),
        ),
        FloatingActionButton(
            NewIcon(icons.Add),
            "Increment",
            m.onPressed,
        ),
    )

    Неплохо, и похоже на оригинальную версию из Dart, но нехватка именованных параметров всё равно сильно режет глаз.


    Версия 4


    Давайте немного отступим назад и зададимся вопросом – что собственно мы пытаемся сделать. Нам необязательно слепо копировать подход Dart (хотя это будет приятный бонус – меньше нового учить людям, уже знакомым с Flutter на Dart). По сути, мы просто создаём новые объекты и присваиваем им свойства.


    Может попробовать вот таким способом?


    scaffold := NewScaffold()
    scaffold.AppBar = NewAppBar(Text("Flutter Go app"))
    
    column := NewColumn()
    column.MainAxisAlignment = MainAxisCenterAlignment
    
    counterText := Text(fmt.Sprintf("%d", m.counter))
    counterText.Style = ctx.Theme.textTheme.display1
    column.Children = []Widget{
      Text("You have pushed the button this many times:"),
      counterText,
    }
    
    center := NewCenter()
    center.Child = column
    scaffold.Home = center
    
    icon := NewIcon(icons.Add),
    fab := NewFloatingActionButton()
    fab.Icon = icon
    fab.Text = "Increment"
    fab.Handler = m.onPressed
    
    scaffold.FloatingActionButton = fab
    
    return scaffold

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


    Кстати, этот подход использовался очень давно в UI фреймворках вроде GTK или Qt. Посмотрите, например, на код из документации последнего Qt 5:


     QGridLayout *layout = new QGridLayout(this);
    
        layout->addWidget(new QLabel(tr("Object name:")), 0, 0);
        layout->addWidget(m_objectName, 0, 1);
    
        layout->addWidget(new QLabel(tr("Location:")), 1, 0);
        m_location->setEditable(false);
        m_location->addItem(tr("Top"));
        m_location->addItem(tr("Left"));
        m_location->addItem(tr("Right"));
        m_location->addItem(tr("Bottom"));
        m_location->addItem(tr("Restore"));
        layout->addWidget(m_location, 1, 1);
    
        QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
        connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
        connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
        layout->addWidget(buttonBox, 2, 0, 1, 2);
    

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


    Версия 5


    Ещё один вариант, который я хочу попробовать – это создание дополнительных типов с параметрами для передачи в функции-конструкторы. Например:


    func Build() Widget {
        return NewScaffold(ScaffoldParams{
            AppBar: NewAppBar(AppBarParams{
                Title: Text(TextParams{
                    Text: "My Home Page",
                }),
            }),
            Body: NewCenter(CenterParams{
                Child: NewColumn(ColumnParams{
                    MainAxisAlignment: MainAxisAlignment.center,
                    Children: []Widget{
                        Text(TextParams{
                            Text: "You have pushed the button this many times:",
                        }),
                        Text(TextParams{
                            Text:  fmt.Sprintf("%d", m.counter),
                            Style: ctx.textTheme.display1,
                        }),
                    },
                }),
            }),
            FloatingActionButton: NewFloatingActionButton(
                FloatingActionButtonParams{
                    OnPressed: m.incrementCounter,
                    Tooltip:   "Increment",
                    Child: NewIcon(IconParams{
                        Icon: Icons.add,
                    }),
                },
            ),
        })
    }

    Ухты! Это, очень даже неплохо. Вот эти типы ...Params немного бросаются в глаза, но всё равно это сильно лучше остальных вариантов пока что. Такой подход, кстати, довольно часто используется и библиотеках на Go и особенно хорошо работает, когда у вас есть лишь пару структур, которые нужно создавать таким образом.


    Вообще-то, есть способ убрать многословность ...Params, но для этого потребуется изменение в языке. Есть даже предложение (proposal) как раз под это — "нетипизированные составные литералы". По сути, это означает возможность сократить FloatingActionButtonParameters{...} до {...} в теле параметров функции. Вот как будет выглядеть код:


    func Build() Widget {
        return NewScaffold({
            AppBar: NewAppBar({
                Title: Text({
                    Text: "My Home Page",
                }),
            }),
            Body: NewCenter({
                Child: NewColumn({
                    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: NewFloatingActionButton({
                    OnPressed: m.incrementCounter,
                    Tooltip:   "Increment",
                    Child: NewIcon({
                        Icon: Icons.add,
                    }),
                },
            ),
        })
    }

    Это почти идеальное совпадение с версией на Dart! Хотя она и потребует создание типов для каждого виджета.


    Версия 6


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


    Идея в том, что при создании объекта, мы его возвращаем, и тут же можем вызывать метод-сеттер, который возвращает изменённый объект – и так один за другим:


    button := NewButton().
        WithText("Click me").
        WithStyle(MyButtonStyle1)

    или


    button := NewButton().
        Text("Click me").
        Style(MyButtonStyle1)

    Тогда наш код для Scaffold-виджета будет выглядеть вот так:


    // Build renders the MyHomePage widget. Implements Widget interface.
    func (m *MyHomePage) Build(ctx flutter.BuildContext) flutter.Widget {
        return NewScaffold().
            AppBar(NewAppBar().
                Text("Flutter Go app")).
            Child(NewCenter().
                Child(NewColumn().
                    MainAxisAlignment(MainAxisCenterAlignment).
                    Children([]Widget{
                        Text("You have pushed the button this many times:"),
                        Text(fmt.Sprintf("%d", m.counter)).
                            Style(ctx.Theme.textTheme.display1),
                    }))).
            FloatingActionButton(NewFloatingActionButton().
                Icon(NewIcon(icons.Add)).
                Text("Increment").
                Handler(m.onPressed))
    }

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


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

    Также во всех примерах мне нравится использование классического именования New...() для конструкторов – просто функция, которая создаёт объект. Это сильно проще объяснять новичку в программировании, чем объяснять конструкторы — "это тоже функция, но у неё имя такое же, как у класса, но ты не увидишь эту функцию, потому что она специальная, и просто глядя на функцию, ты не можешь легко понять – это функция или конструктор объекта с таким именем".


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


    Финальная версия кода


    Соберём все части вместе и попробуем записать наш "hello, world" на воображаемом Flutter на Go:


    main.go


    package hello
    
    import "github.com/flutter/flutter"
    
    func main() {
        flutter.Run(NewMyApp())
    }

    app.go:


    package hello
    
    import . "github.com/flutter/flutter"
    
    // MyApp is our top application widget.
    type MyApp struct {
        Core
        homePage *MyHomePage
    }
    
    // NewMyApp instantiates a new MyApp widget
    func NewMyApp() *MyApp {
        app := &MyApp{}
        app.homePage = &MyHomePage{}
        return app
    }
    
    // Build renders the MyApp widget. Implements Widget interface.
    func (m *MyApp) Build(ctx BuildContext) Widget {
        return m.homePage
    }

    home_page.go:


    package hello
    
    import (
        "fmt"
        . "github.com/flutter/flutter"
    )
    
    // MyHomePage is a home page widget.
    type MyHomePage struct {
        Core
        counter int
    }
    
    // Build renders the MyHomePage widget. Implements Widget interface.
    func (m *MyHomePage) Build(ctx BuildContext) Widget {
        return NewScaffold(ScaffoldParams{
            AppBar: NewAppBar(AppBarParams{
                Title: Text(TextParams{
                    Text: "My Home Page",
                }),
            }),
            Body: NewCenter(CenterParams{
                Child: NewColumn(ColumnParams{
                    MainAxisAlignment: MainAxisAlignment.center,
                    Children: []Widget{
                        Text(TextParams{
                            Text: "You have pushed the button this many times:",
                        }),
                        Text(TextParams{
                            Text:  fmt.Sprintf("%d", m.counter),
                            Style: ctx.textTheme.display1,
                        }),
                    },
                }),
            }),
            FloatingActionButton: NewFloatingActionButton(
                FloatingActionButtonParameters{
                    OnPressed: m.incrementCounter,
                    Tooltip:   "Increment",
                    Child: NewIcon(IconParams{
                        Icon: Icons.add,
                    }),
                },
            ),
        })
    }
    
    // incrementCounter increments app's counter by one.
    func (m *MyHomePage) incrementCounter() {
        m.counter++
        flutter.Rerender(m)
    }

    Очень даже ничего!


    Заключение


    Похожесть с Vecty


    Я не мог не обратить внимание на то, как сильно моё решение напоминает то, как мы пишем код на Vecty. Во многом, они, в принципе, похожи, только Vecty выводит результат в DOM/CSS/JS, а Flutter под собой несёт мощный и написанный с нуля движок рендеринга и анимаций, дающий красивейшую графику и крутую анимацию на 120 кадрах в секунду. Но мне кажется, что дизайн Vecty очень удачен, и моё решение для Flutter на Go напоминает Vecty неспроста.


    Лучшее понимание дизайна Flutter


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


    Недостатки Go


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


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


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


    Мысли о будущем Flutter


    Мои заключительные мысли будут о том, что Flutter необыкновенно хорош, несмотря на всё то бурчание, которое я себе позволил в этой статье. Соотношение "крутота/так себе" на удивление велико, и Dart достаточно легко схватывается (как минимум, людям, знакомым с другими языками программирования). Учитывая браузерную родословную Dart, я мечтаю, что однажды все браузерные движки (хотя, сколько их там осталось) будут идти с DartVM вместо V8, и Flutter будет интегрирован нативно – и все Flutter приложения автоматически будут также и веб-приложениями.


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


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


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


    Ссылки


    Поделиться публикацией

    Комментарии 72

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

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


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

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

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

          Можно пруфы?


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

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


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


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

            –1
            Можно пруфы?

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


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

            Ничуть.


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

            Ок, спасибо.

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

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

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

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

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


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


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

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


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

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

                  +1

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


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


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


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

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

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


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


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

                        –1

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


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

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


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

                          –1

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


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


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

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

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

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

                              0

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


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


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


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

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

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

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


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

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


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


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


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


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

                                  0

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


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


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


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


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

                                    0

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

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

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


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

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


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

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


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


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

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

                                      –1

                                      Вы рассуждаете не с позиции "в 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{}. А между тем с помощью нормально описанных интерфейсов можно здорово упростить себе жизнь.


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

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

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


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

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


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

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

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

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

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


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


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


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

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

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


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

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


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

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

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

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

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

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

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

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


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


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

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


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


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

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

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

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

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

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

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

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


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

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

                                                    Как часто вам приходится использовать 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() ты сразу понимаешь какие именно действия совершаются в указанном куске кода.
                                                      0
                                                      Как часто вам приходится использовать iota или кодогенерацию?

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

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

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


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

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


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

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


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


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

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

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

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

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

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

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

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

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


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

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

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


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

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


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

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

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

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

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

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

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

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

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

                                                          0

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


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


                                                              return s.(string) + " mapped"

                                                          С ними:


                                                              return s + " mapped"

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

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

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

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


                                                            Вам нужно:


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

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


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


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


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


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

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

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


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

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

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

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


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

                                                                0
                                                                Но вы ведь понимаете, что это не равнозначные примеры? Вы сравниваете свой «хелпер-метод» с методом из 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;
                                                                }


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

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


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


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


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


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


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

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

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

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

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

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

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


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

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


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

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

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

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


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


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

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

                                                                  0

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


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

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

                                                                    0

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

                                                                0

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


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


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


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

                                                                  –1

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

                                                                    0

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


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

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

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

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

                                                                    Раз уж мы так любим примеры, можете привести пример какой-нибудь реальной проблемы и реальных случаев при которых она доставляет неудобства?
                                                                      0
                                                                      О чем вы спорите?
                                                                      Вроде бы в GO2 собираются дженерики вводить :-)
                                          +1
                                          Просто сразу в закладки, а теперь читать))
                                            +2

                                            Отлично, вы поделили все виджеты на системные (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 { }
                                            }
                                              0
                                              Спасибо, отличный комментарий и пример. В принципе, там где виджеты без стейта, то API «пользовательских виджетов» остаётся таким же, а со стейтом — да, надо либо оставлять подход, как у меня описан (в build виджет не создаётся), либо придумывать что-то иное.

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

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

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

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


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


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

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

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

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


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

                                                    0

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


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


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

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

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

                                                        0

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


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


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

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


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


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




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

                                                          0

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


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

                                                            0
                                                            Сразу прокомментирую вот этот момент — "у 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 — состояние.

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

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


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

                                                                  +1

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

                                              0
                                              Я думаю что Dart если не дропнут в следющих релизах Flutter, то добавят рядом kotlin. Go думаю не добавят.

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

                                              Самое читаемое