Совсем недавно я открыл для себя 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, это глоток свежего воздуха.


Ссылки


Комментарии (49)


  1. Borz
    14.01.2019 19:14
    +1

    1. divan0
      14.01.2019 23:01

      Да, я Desktop Embedder и эту ссылку упомянул в статье :)


  1. uvelichitel
    14.01.2019 19:22
    +1

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


    1. divan0
      14.01.2019 23:03
      +2

      Go, напротив, реализует подход communicating sequential processes (CSP).

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


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


      1. uvelichitel
        15.01.2019 00:53

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

        Я поэтому редко пишу (или использую написанное другими) на Go там где достаточно простого вызова функции.


  1. SirEdvin
    14.01.2019 19:44

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

    Можно пруфы?


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

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


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


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


    1. divan0
      14.01.2019 23:33

      Можно пруфы?

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


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

      Ничуть.


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

      Ок, спасибо.


      1. SirEdvin
        15.01.2019 00:03
        -1

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

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


  1. youROCK
    14.01.2019 20:36

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

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


    1. SirEdvin
      14.01.2019 23:01

      Даже при использовании async/await сахара там все еще плохо?


      1. youROCK
        14.01.2019 23:17

        По сравнению с отсутствием сахара — очень хорошо, но по сравнению с Go всё ещё слишком сложно и не так гибко.


    1. divan0
      14.01.2019 23:37

      вполне себе существуют и даже видны на экране, и вполне имеет смысл наследование.

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


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


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

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


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

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


    1. gudvinr
      15.01.2019 01:37

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


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


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


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


      1. mad_nazgul
        15.01.2019 08:55
        +1

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


      1. mayorovp
        15.01.2019 11:27
        +1

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


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


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


        1. gudvinr
          15.01.2019 12:22

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


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

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


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


          1. mayorovp
            15.01.2019 13:00

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


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


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

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


            1. gudvinr
              15.01.2019 15:23

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

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


              1. mayorovp
                15.01.2019 15:50

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


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


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


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

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


                1. gudvinr
                  16.01.2019 00:02
                  -2

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

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


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

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


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


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


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


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


                  1. divan0 Автор
                    16.01.2019 00:54

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


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


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


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


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


                    1. mayorovp
                      16.01.2019 08:57

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


                  1. mayorovp
                    16.01.2019 08:50

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

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


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

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


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

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


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


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

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


                    1. gudvinr
                      16.01.2019 21:23

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


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


                      1. mayorovp
                        17.01.2019 07:05

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

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


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

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


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

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


              1. Bce_npocTo
                15.01.2019 16:43

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

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


                1. gudvinr
                  15.01.2019 23:40
                  -2

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


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


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


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


                  1. SirEdvin
                    16.01.2019 00:07
                    +1

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

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


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

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


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


                  1. Bce_npocTo
                    16.01.2019 10:07

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

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

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

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

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

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


                    1. gudvinr
                      16.01.2019 21:50

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

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


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


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

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


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


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


                      1. Bce_npocTo
                        17.01.2019 09:48

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

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

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

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

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


                        1. Edison
                          17.01.2019 10:10
                          +1

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

                          Дак вроде никто так не считает, gudvinr говорит как раз про «можно жить без дженериков».


                          1. Bce_npocTo
                            17.01.2019 10:58
                            +1

                            Извините, но я не могу по-другому воспринимать его комментарий из другой ветки:

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

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


                            1. divan0 Автор
                              17.01.2019 11:57

                              т.к. мне постоянно приходиться работать с коллекциями в том или ином их виде (как и ооочень большой пласт программистов, я думаю) и я не хочу постоянно писать циклы для фильтрации\модификации списков. А 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. gudvinr
                              17.01.2019 12:11
                              +1

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


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


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


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


  1. prospero78su
    15.01.2019 10:09

    Просто сразу в закладки, а теперь читать))


  1. mayorovp
    15.01.2019 11:12

    Отлично, вы поделили все виджеты на системные (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 { }
    }


    1. divan0 Автор
      15.01.2019 12:50

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

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

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

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


      1. mayorovp
        15.01.2019 13:12

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

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


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


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

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


        1. divan0 Автор
          16.01.2019 00:19

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

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


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


          1. mayorovp
            16.01.2019 09:07

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


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


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


            1. divan0 Автор
              16.01.2019 15:48

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

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


              1. mayorovp
                16.01.2019 16:18

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


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


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

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


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


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




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


                1. divan0 Автор
                  16.01.2019 18:28

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


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


                  1. mayorovp
                    16.01.2019 18:43

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


                    1. divan0 Автор
                      16.01.2019 22:39

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


                      1. mayorovp
                        17.01.2019 07:10

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


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


                        1. divan0 Автор
                          17.01.2019 11:59

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


  1. uncle_doc
    15.01.2019 11:49

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