"Если вам нравятся Руби, Свифт, Дарт, Эликсир, Эльм, С++, Питон или даже С, используйте их ради бога. Но выучите Кложур, и выучите его хорошо" — Дядя Боб (твит, а также твиты: 1, 2, 3).

Статей о Clojure написано много, цель этой — дать свое видение некоторых преимуществ языка для кросплатформенной разработки на Flutter. Ориентируюсь в первую очередь на dart-разработчиков, но статья может быть интересна всем, кто работает с Clojure и/или Flutter.

Очень краткая история

Clojure не был написан второпях (JS), не пытался захватывать рынок с многомиллионным бюджетом на маркетинг (Java). За ним не стояло огромных компаний (Go, Dart, Kotlin), он не был единственным языком для платформы (Swift, C#). Даже начать программировать на нем достаточно сложно (в отличие от Ruby или Python).

Так почему же Clojure набрал критическую массу, является самым высокооплачиваемым языком и одним из трех самых любимых языков? Зачем для него пишут порты на Java, JS, C#, Unity, Elm, Python ... и, наконец, Dart?

Ответ на этот вопрос — в докладе Рича Хикки "Simple Made Easy", есть перевод на Хабре. Моя непростительно краткая и бессовестная версия: Рич подумал, как сделать хорошо, руководствуясь принципами простоты, стабильности и практичности, и сделал Clojure, не зависимый от сроков, бюджетов, погони за хайпом.

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

В этом разделе было много громких высказываний, подробнее на многие вопросы отвечаю в FAQ в конце статьи, со ссылками, пруфами и небольшим налетом "субъективщины".

Экскурс в язык

Для приемлемо-комфортного чтения статьи достаточно понимать, что структура выражения на Clojure представляет собой список, где первый элемент — функция, а последнующие элементы — аргументы:

(функция аргумент-1 аргумент-2 ... аргумент-n)

Каждый аргумент и даже сама функция могут быть такими же выражениями, например:

(функция-2 (функция-3 аргумент) аргумент-2)

Когда происходит интерпретация, сперва вычисляются аргументы, затем они применяются к функции. Выражение выше на превратится в:

(функция-2 вычисленный-аргумент аргумент-2)

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

Простота синтаксиса и консистентность

Примеры ниже на Dart и на Clojure, делающие одно и то же:

  • Dart: max(1, 2);

  • Clojure: (max 1 2)

  • Dart: a > b || (c > d && d > e && e >f);

  • Clojure: (or (> a b) (> c d e f))

  • Dart: if (a == 1) a else b;

  • Clojure: (if (= a 1) a b)

  • Dart: int square(int n) => n * n;

  • Clojure: (defn square [n] (* n n))

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

В Clojure скобочки — это всегда группировка выражения, где на первом месте стоит функция или макрос, а далее — аргументы.

Кроме того, на Dart не очевидно (надо знать или оборачивать в скобочки), какой оператор выполнится первым — <, || или &&, — на Clojure же все в скобочках.

  • Dart: ++i;

  • Clojure: (inc i)

В Dart используются инфиксная (1 < 2), префиксная (max(1, 2)) и постфиксная (i++) нотации, тогда как на Clojure — только префиксная.

Синтаксис Clojure значительно проще синтаксиса Dart, так как он более консистентный и описывается меньшим количеством правил. Самый простой способ показать эту разницу — сравнить Antlr-парсеры (и лексеры) Clojure и Dart.

В ссылках, что я привел, Clojure описывается в 5 раз короче (по количество слов). Если взять официальный парсер Dart (spec), то будет разница в 9 раз.

А вот грамматика языков (clj, dart), описанная для другого парсера — Tree-sitter. Clojure понадобилось 1.6к строк, Dart — 10k.

Элегантный и краткий

Приведу несколько примеров кода, которые показывают, почему и за счет чего код на Clojure, как правило, короче, чем на других языках. Вот пример для визуального сравнения. Еще один — physics-simulation flutter cookbook, реализованный на "кложе" в 2 раза короче.

Победа над вложенностью шестью строчками кода

Вложенные виджеты на дарте превращаются в лесенку,

Container(
  color: Colors.red,
  child: const SizedBox(
    child: Center(
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Text(
          'Oh my god, how “clutter” is it!',
        ),
      ),
    ),
  ),
);

Если мы знаем, что у каждого вложенного виджета есть child, то почему бы не избавиться от дублирования, превратив код во что-то вроде:

nest(
  Container(color: Colors.red),
  SizedBox(),
  Center(),
  Padding(padding: EdgeInsets.all(16)),
  Text('Oh my god, how “flutter” is it!'));

Решить это в compile time невозможно (и как я написал выше, и с дополнительными аннотациями), так как :child некоторых виджетов — обязательный параметр. Можно попросить разработчиков компилятора сделать nest, но захотят ли они добавлять новое ключевое слово (нет)?

Решить для ограниченного количества классов виджетов в runtime можно при помощи рефлексии, но так как поля :childfinal, придется копировать виджеты целиком. То есть понадобится огромное количество кода и будет работать медленно.

Решить в runtime для общего случая — невозможно, так как мы не знаем, какие кастомные виджеты нам передаст пользователь. Может, он напишет такой виджет, где сохранит переданный в :child аргумент с другим именем someTrickyName — и мы не сможем найти нужное поле). И опять же, :child может быть required.

Что касается Clojure, то изначальный код выглядит так:

(Container 
 .color m.Colors/red
 .child 
 (SizedBox 
  .child
  (Center 
   .child 
   (Padding 
    .padding (EdgeInsets/all 16)
    .child (Text "Oh my god, how “clutter” is it")))))

Но мы легко можем переписать его:

(nest
  (Container .color m.Colors/red)
  (SizedBox)
  (Center)
  (Padding .padding (EdgeInsets/all 16))
  (Text "Oh my god, how “flutter” is it"))

И это будет работать для общего случая с решением всех проблем, описанных выше.

Как такое возможно?

Гомоиконичность, также известная как code as data. Это когда программу, написанную на языке, можно положить в структуру данных этого же языка (и выполнить). Например, код, написанный на дарте, нельзя положить в массив этого же дарта, а с кложурой такое возможно.

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

(defmacro nest [form & forms]
  (let [[form & forms] (reverse (cons form forms))]
    `(->> ~form ~@(for [form forms] (-> form 
                                        (cond-> (symbol? form) list) 
                                        (concat  [.child]) 
                                        (with-meta (meta form)))))))

Писать этот макрос не обязательно (и понимать тоже, если вы незнакомы с Clojure), так как он уже есть в библиотеке (nest). И основной макрос widget тоже поддерживает "выпрямление".

Magic apply

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

Как проверить, что коллекция отсортирована?

На Dart:

bool isSorted(List<int> list) {
  if (list.length < 2) return true;
  int prev = list.first;
  for (var i = 1; i < list.length; i++) {
    int next = list[i];
    if (prev > next) return false;
    prev = next;
  }
  return true;
}

isSorted([0, 1, 2, 3, 4, 5]); // true

var list = [for(var i=0; i<6; i+=1) i];
isSorted(list); // true

На Clojure:

(apply < [0 1 2 3 4 5]) ;;=> true
(apply < (range 6))     ;;=> true

Как это работает? Apply берет функцию и список, и передает элементы из списка в качестве аргументов. То есть выражение превращается в: (< 1 2 3 4 5). У "кложи" тут есть еще преимущество в том, что < принимает любое число агрументов.

Рассмотрим еще один, более сложный пример.

Как транспонировать матрицу?

На Dart:

List<List<int>> transposeList<R>(List<List<int>> input) {
  return List.generate(
    input[0].length,
    (i) => List.generate(input.length, (j) => input[j][i]),
  );
}

transposeList([[1, 2], 
               [3, 4],
               [5, 6]]); // [[1, 3, 5], 
                         //  [2, 4, 6]]

На Clojure будет функция в одну строчку:

;; объявление функции "transpose" средствами языка, без библиотек
(defn transpose [m] (apply map list m))

;; и вызов функции
(transpose [[1 2]
            [3 4]    ;; => [[1 3 5]
            [5 6]])  ;;     [2 4 6]]

Как это работает? Функция map ожидает на вход функцию и одну или несколько коллекций. Иногда проще понять смысл работы чего-то по примерам:

(map inc [1 2 3])     ;;=> (2 3 4)
(map odd? [1 2 3])    ;;=> (true false true)
(map + [1] [3])       ;;=> (4)
(map + [1 8] [1 1])   ;;=> (2 9)
(map max [1 2] [2 1]) ;;=> (2 2)

И так выражение (apply map list [[1 2] [3 4] [5 6]]) благодаря apply превращается в:

(map list [1 2] [3 4] [5 6])

Вызов функции map происходит с векторами из двух элементов. Это значит, что в итоге получим всего 2 вызова функции list, которую передали первым аргументом в map. При первом вызове придут аргументы 1, 3 и 5, а при втором — 2, 4, 6. Имеем:

[(list 1 3 5) (list 2 4 6)] ;;=> [(1 3 5) (2 4 6)] 

Коллекции работают так, как надо

Тезис не только про то, что equals, compare, sort и тому подобное будет работать предсказуемо (без подключения библиотеки Collection, как это делается в Dart для сравнения структур данных).

Core коллекции в Clojure — иммутабельные и персистентные, то есть позволяют создавать копии себя за практически константное время. Например, добавление ключа в мапу не изменяет исходную мапу, но возвращает новую с добавленным ключом. И это не за О(n), а за О(Log32n) (читай константа).

Память на новую коллекцию не расходуется (только на разницу между коллекциями), в глубинах реализации и новая, и старая "мапы" переиспользуют одно и то же дерево (подробнее). Это позволяет писать эффективный, thread-safe код.

Полноценный data oriented programming

Data oriented подход можно вкратце описать, как разделение кода и данных, что приводит также к разделению их иерархии и далее ведет к меньшей связности (coupling).

Скриншоты из статьи на тему:

без разделения

с разделением

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

Здесь же остановлюсь на одном аспекте отказа от классов.

Не нужно описывать, читать и разбираться со всем бойлерплейтом, сопряженным с созданием класса (hash, ==, toMap, fromMap, toString, copyWith), он доступен из коробки. Доступны также сотни функций для работы с коллекциями, которые используют с данными (а не изобретаются заново).

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

(def human {:name "Bob", :age 30})
(def old-human (update human :age inc)) ;=> {:name "Bob", :age 31}

В обычном ООП-подходе мы могли бы создать метод классу Human, увеличивающий возраст (increaseAge). А что, если нам понадобится уменьшить возраст? В Clojure передадим dec (вместо inc), а в Dart — создадим новый метод?

Проблема такого подхода не только в том, что нам необходимо дописывать код, но и в том, что мы постоянно работаем с новыми классами, у которых какие-то свои методы. Это особенно заметно, если сравнивать опыт знакомства с новой библиотекой на Dart (Java, Kotlin, Swift) и на Clojure. С последней работать проще как минимум за счет того, что не нужно изучать новые методы новых классов.

Другими словами то же самое (цитата с официального сайта Clojure):

Putting information in such classes is a problem, much like having every book being written in a different language would be a problem. You can no longer take a generic approach to information processing. This results in an explosion of needless specificity, and a dearth of reuse.

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

It is better to have 100 functions operate on one data structure than to have 10 functions operate on 10 data structures.

Стабильность языка

Я занимался Android-разработкой около 7 лет, и за это время только в узком сегменте мнопоточности и "асинхронщины" было довольно много изменений.

Сперва люди пользовались AsyncTask, IntentService и просто голыми тредами. Затем начала набирать популярность RxJava. На нее переписывали модные библиотеки, о ней писали статьи и книги.

Помню, как уговаривал тимлида переходить на RxJava2. Он не согласился — и правильно сделал, потому что уже через год вышел RxJava3, а популярны стали корутины. WorkManager пришел на замену JobScheduler-у, пришедшему на замену AlarmManager (+ BroadcastReceiver, + Service). И я уверен, что через 2-3 года его так же заменят.

В Clojure мире 9 лет назад появилась библиотека core.async, которая популярна до сих пор. И это не единичный пример. Код на Clojure — долговечен. Вот ссылка на "A History of Clojure" Рича Хикки, где (26 страница, или этот твит) приводится сравнение со Scala на предмет того, как долго поддерживается код.

Все на одном языке

В конце концов, Clojure может быть полезен не только для Flutter-разработки. Ниже приведу все сферы, которые считаю практически применимыми:

На подходе хост на С++ — jank.

Небольшой пример. В моем последнем проекте — переводчике с Dart на Clojure — все написано на одном языке: core проекта, файлы с зависимостями, приложение для командной строки, приложение, доступное из npm и скрипт, публикующий сборки. Один и тот же Clojure-код переиспользуется и для npm-библиотеки, и для jar файла, и для нативных сборок (через GraalVM).

FAQ

Можно ли как-то подтвердить эффективность Clojure?

Если не вдаваться в субъективщину, вроде того, что мне лично так кажется, или что Clojure — третий любимый ЯП, согласно StackOverflow survey, — есть несколько подтверждений.

Вот reproduction research (повторили предыдущий) On the impact of programming languages on code quality. Вкратце: взяли проекты с гитхаба, посмотрели на количество коммитов с исправлением багов.

Несколько вырезок оттуда:

  • "Языки ассоциирующиеся с меньшим количеством багов TypeScript, Clojure, Haskell, Ruby, and Scala, тогда как C, C++, Objective-C, JavaScript, PHP, Python ассоциируются с большим".

  • Коммитов с исправлением багов в Clojure было меньше всего, в C++ — больше всего.

Другие возможные подтверждение — размер библиотек и зарплаты (2 следующих вопроса).

Почему "библиотека на Clojure меньше по размеру... в 2, в 3, в 5, в 10, в 100 раз"?

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

И пара статей на английском: Статья со сравнением 24 фреймворков — Clojure на втором месте по количеству строк кода. "Любовное письмо к Кложуре" — приложение переписали с JS (1500 строк) на Cljs (500).

Сколько платят Clojure-разработчикам?

Больше всех, согласно StackOverflow survey за 2022 (и 2021). Феномен известный, попадался тред на Reddit с объяснением, что это не за язык платят много, а дорогие опытные специалисты выбирают его (пруф через другой график того же survey).

Тезис также можно подтвердить через State of Clojure 2022, вопрос 8. Более 76% разработчиков имеют более 6 лет опыта, 50% — более 11 лет опыта.

Если Lisp такой мощный диалект, то почему не популярный?

Тут есть 3 тезиса. Первый — популярность языка не обуславливается его качеством (JS).

Второй и третий тезисы завязаны на так называемое "проклятье Лиспа". Могу рассказать грубо, но постараюсь кратко, подробнее можно почитать Lisp curse.

Подразумевается, что Лисп позволяет одному разработчику делать то, для чего иначе были бы нужны целые компании. Может звучать неправдоподобно, в качестве небольшого примера напомню решению проблемы вложенности виджетов в 3 строки (ссылка на абзац).

В итоге это ведет к индивидуализму (второй тезис). Зачем собираться командой, разрабатывать стандарт и доводить продукт (будь то какая библиотека или редактор кода) до ума, решать 99% кейсов, когда можно в одного решить свои, скажем, 40% кейсов.

Возьмем пример с редактором: весь мир пишет Java в IntelliJ IDEA (2021 год, 75%), тогда как на Clojure редакторы распределены более-менее равномерно (Emacs, Idea, VSCode, Vim и менее популярные Atom, Sublime, NightCode).

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

Есть мнение, что Clojure, отчасти нивелирует проблемы "проклятья Лиспа" за счет своей "хостовости" (возможности использовать фреймворки и библиотеки на Java, Dart и JS).

Почему функциональное программирование менее популярно, чем ООП?

Простой ответ дать сложно, но не стоит забывать, что рынок языков программирования — это рынок. В рекламу Java, например, было вложено более 500млн$ (пишу "более", поэтому это не единственная кампания). Почему Java — OOP? Может быть, потому что это было проще продать разработчикам С++.

Если эта тема интересна, можно посмотреть Why Isn't Functional Programming the Norm?.

Должна ли смущать динамическая типизация?

Если вы противник динамической типизации из-за опыта с JavaScript, то возможно, что вам не понравилась неявная и слабая типизация, а не динамическая (подробнее о типизации есть ликбез на хабре.). С Clojure вам не придется разбираться с "багами" из-за неявного приведения типов, как в JS:

1 === '1';    // false
1 == '1';     // true
true === 1;   // false
true == 1;    // true
[0] == 0      // true 
{} + [] == 0  // true

Если вы противник динамической типизации из-за опыта с Python (или другими "строгими" языками), наверное, две проблемы приходят на ум: производительность и поддержка (чтение чужого) кода.

Проблем с прозиводительностью, как в Python, нет (в большинстве случаев). Функции компилируются в методы языка, в который компилируется Clojure. Критически места можно оптимизировать, добавляя type hints, используя структуры данных платформы. Чтение кода тоже можно оптимизировать, используя REPL (который пока не поддержали в ClojureDart).

Если вы противник концептуальный, понимаете плюсы статической типизации, но не видите плюсы динамической, есть смысл обратить внимание на теорему о неполноте или посмотреть ответ Андрея Бреслава на критику Питона и динамической типизации:

Более долгое видео на тему — доклад Рича Maybe not.

А если очень хочется типы?

Для начала, давайте определимся, нужны ли нам типы, или классы. Неплохой разбор отличий приводится в статье Ивана Гришаева про core.spec, который позволяет описать тип и форму данных более гибко. Подобные описания можно использовать для тестов, документации, значений в Swagger и т.д.

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

Как начать c ClojureDart?

Если опыта с Clojure нет, читаем Learn X in Y minutes, решаем (идиоматично, как в ответах) хотя бы треть задачек с 4clojure. Practical.li — тоже неплохой ресурс для начала, а для книжных червей подойдет BraveClojure.

Если вы уже знакомы с Clojure и хотите сразу перейти к Flutter, посетите страницу Clojure Dart и гляньте на краткое руководство по началу работы. Есть еще статья от меня — Как начать писать приложения на ClojureDart.

Другие варианты - Clojure Dart workshop, YouTube-канал и просто примеры.

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

Что не было упомянуто

Read Evaluate Print Loop. В Clojure REPL подключается к приложению, и вы можете редактировать код работающей программы, не теряя промежуточный стейт. Например, подключиться к хендлеру на бекенде в проде и посмотреть, какие данные через него проходят и там же поправить критичный баг без редеплоя. REPL интегрирован в workflow (и в редактор, и в приложение).

На консолях/реплах/shell в Dart, JS, Python, Swift, Kotlin, Java, Scala, Haskell такое невозможно. Их репл не связан с приложением, и скорее похож либо на дебаггер, либо на консоль, которую можно запустить в стороне и проверить, как работают какие-то кусочки кода.

REPL пока не реализован в ClojureDart, но вскоре будет.

Summary

Итак, зачем же использовать Clojure для Flutter-разработки? Чтобы получить инструменты, ускоряющие и упрощающие разработку.

Гомоиконичность для расширения языка

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

Персистентные коллекции для упрощения работы с данными

Изучение core функция для работы с данными ставит вас в позицию, когда любой код любого проекта / библиотеки становится понятен и предсказуем, потому что все используют один и тот же подход. Кроме того, писать иммутабельный код проще, когда создание новой коллекции — не линейно, а за O(Log32).

А также

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

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


  1. Tuxman
    14.12.2022 19:07

    Кроссплатформенную разработку в будущем я хотел бы делать на

    Вы обошли стороной Qt | Cross-platform Software Design and Development Tools


    1. arturdumchev Автор
      14.12.2022 19:38
      +1

      Спасибо, добавил варинт.


  1. tttinnny
    14.12.2022 19:38
    +1

    Неплохое интро в кложур, но хотелось бы больше увидеть его как названии статьи (во flutter'e, со стейт менеджерами, а не просто в сравнении с dart'ом при решении некоторых мат. задач


    1. arturdumchev Автор
      14.12.2022 19:49
      +1

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

      Пока что могу только сослаться на примеры в самом репозитории (samples). Во многих есть ссылка на cookbook от flutter.

      И есть еще пример (правда, старенький), где я набросал экранчик с TEA архитектурой.


      1. tttinnny
        14.12.2022 20:10

        С setefull и Bloc виджетами было бы очень интересно увидеть комплексную статью. За примеры благодарен, основные аспекты они показывают что надо)


  1. nikita_dol
    14.12.2022 22:52
    +4

    Дисклеймер

    Я не писал на Clojure, но пишу на Dart под Flutter, поэтому напишу то, что увидел прочитав статью и запустив пример кода ClojureDart

    Всё что далее, будет рассмотрено в контексте того, что ClojureDart на выходе создаёт Dart код (тут, а так же видно при запуске ClojureDart кода)

    Не имею претензий к Clojure, но скорее всего имею к ClojureDart

    1. ClojureDart на выходе создаёт Dart код, поэтому, логичнее было бы сравнивать не с чистым Dart, а с Dart с кодогенерацией

    2. Аргумент, что код короче не такой однозначный:

    • На выходе имеем 40807 строк неформатированного кода Dart (88141 строк после flutter format)

    • Этот код на Dart, из того же примера с симуляцией физики, занимает 49 строк (и на мой взгляд читается легче) - вопрос форматирования

    Тут
    import 'package:flutter/material.dart';
    import 'package:flutter/physics.dart';
    import 'package:functional_widget_annotation/functional_widget_annotation.dart';
    
    part 'main.g.dart';
    
    void main() => runApp(const MaterialApp(home: PhysicsCardDragDemo()));
    
    @swidget
    Widget physicsCardDragDemo() => Scaffold(
        appBar: AppBar(),
        body: const DraggableCard(child: FlutterLogo(size: 128)));
    
    class DraggableCard extends StatefulWidget {
      const DraggableCard({required this.child, super.key});
    
      final Widget child;
    
      @override
      State<DraggableCard> createState() => _DraggableCardState();
    }
    
    class _DraggableCardState extends State<DraggableCard> with SingleTickerProviderStateMixin {
      late final _controller = AnimationController(vsync: this)..addListener(() => setState(() => _dragAlignment = _animation.value));
      Alignment _dragAlignment = Alignment.center;
      late Animation<Alignment> _animation;
    
      void _runAnimation(Offset pixelsPerSecond, Size size) {
        _animation = _controller.drive(AlignmentTween(begin: _dragAlignment, end: Alignment.center));
        final simulation = SpringSimulation(const SpringDescription(mass: 30, stiffness: 1, damping: 1), 0, 1, -Offset(pixelsPerSecond.dx / size.width, pixelsPerSecond.dy / size.height).distance);
        _controller.animateWith(simulation);
      }
    
      @override
      void dispose() {
        _controller.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        final size = MediaQuery.of(context).size;
        return GestureDetector(
            onPanDown: (details) => _controller.stop(),
            onPanUpdate: (details) => setState(() => _dragAlignment += Alignment(details.delta.dx / (size.width / 2), details.delta.dy / (size.height / 2))),
            onPanEnd: (details) => _runAnimation(details.velocity.pixelsPerSecond, size),
            child: Align(alignment: _dragAlignment, child: Card(child: widget.child)));
      }
    }

    3 Для создания плоского вложенного кода есть пакет nested, но это вопрос немного глубже - имея много вложенности, быстро поймёшь, что нужно разбить виджет на несколько, так как сложно понимать, а так же, с большой вероятностью нужно не обновлять все из вложенных виджетов при изменении состояния - вопрос оптимизации
    4. (небольшое отступление) Макросы планирую добавить в Dart (тут)
    5. Можно проверить, что отсортирована с помощью isSorted (из достаточно стандартного пакета collection) и даже сравнивать то, что не имеет реализации < > =
    6. А можно так переворачивать матрицы:

    List<List<int>> transposeList<R>(List<List<int>> input) {
      return List.generate(
        input[0].length,
        (i) => List.generate(input.length, (j) => input[j][i]),
      );
    }

    7.

    Core коллекции в Clojure — иммутабельные и персистентные, то есть позволяют создавать копии себя за практически константное время. 

    Не уверен, что это применимо для ClojureDart, так как добавление элемента в список (как это сделать гуглил)

    (conj [1 2 3] 4)

    генерит Dart код, который создаёт новый List размера 4 и заполняет его новыми элементами - не похоже на константу

    8.

    thread-safe код.

    Тоже не про Dart

    9.

    Не нужно описывать, читать и разбираться со всем бойлерплейтом, сопряженным с созданием класса (hash, ==, toMap, fromMap, toString, copyWith), он доступен из коробки.

    Имея кодогенерацию - не имеем проблем с этим (например, freezed)

    Итого: Если вы знаете только Clojure - то ClojureDart скорее всего ваш выбор. По факту же, генерируется код с кучей dynamic, что сводит на нет оптимизации, которые проводит компилятор Dart, а так же генерируется куча лишнего кода, который заставляет работать Dart как Clojure, при этом заставляя компилятор изрядно попотеть при компиляции (например, hello world занимает примерно 35500 строк до форматирования и 74115 после)


    1. arturdumchev Автор
      14.12.2022 23:34
      +2

      Спасибо за комментарий! Многие тезисы справедливы. Позволю себе прокомментировать каждый.

      1. Смотря для каких целей (см. пункт 2). Пока еще ClojureDart в альфе, так что я ожидаю, что сгенерированный код будет значительно(!) лучше.

      2. Я сравнивал тот код, с которым разработчик работает большую часть времени. Когда я пишу на Clojure (с jvm хостом), я смотрю на сгенерированный код редко — когда требуется производительность или отладка. А пример с физикой — вот тут же 108 сток кода без комментариев, а кложа-вариант — 50.

      3. Кажется, что насчет nest я не нагрешил в статье. Посмотрел реализацию, в рантайме будет дополнительная работа. И чтобы написать такое решение на Dart, потребовалась библиотека, где реализация на 400 строк. Мой же тезис был про то, что когда подобное нужно на Clojure, кода на реализацию получается сильно меньше (говоря о коде, с которым работает разработчик, а не который генерируется).

      4. Макросы не смогут быть такими же как в Clojure, потому что Dart не гомоиконичный язык.

      5. Мой пример про проверку сортировки был только для демонстрации того, что умеет делать apply.

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

      7. Это временно, целевая версия будет работать на персистентных коллекциях. Думаю, это даже можно будет сделать, переиспользуя fast_immutable_collections, спрошу авторов про это, и может подробнее напишу в следующей статье.

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

      9. Да, можно генерировать, но мне не нравится, что кодовая база содержит этот бойлерплейт. Например, отойдя в сторону от Clojure, на том же Kotlin можно хотя бы так описать: data class Human(val age: Int). Так можно описать с десяток моделей в одном файле. И тут вся информация, которая мне нужна. Я не хочу читать кучу методов, если в них предсказуемый бойлерплейт, а не что-то, что мне нужно учесть. Т.е. это просто отвлекает, т.к. в каком-то месте может быть действительно уникальный код, а не типичная реализация.

      P.S. Я немного поправлю статью, допишу про коллекции и приведу сокращенный дарт-код для транспанирования матрицы.


      1. arturdumchev Автор
        15.12.2022 09:22

        Неправильную ссылку привел. Пункт 2. Физика на 108 строк без комментариев.


        1. nikita_dol
          15.12.2022 14:35

          1. Там не сохраняются ваши изменения

          2. Я привёл тот-же пример, что и по вашей ссылке, но убрал красивость форматирования


      1. arturdumchev Автор
        15.12.2022 11:19

        По поводу пункта 7.

        Посмотрел на внутренности ClojureDart — там есть свои реализации PersistentVector и PersistentMap.

        Я запустил Hello world на коммите 0b83ba8dd0bae639f5d457f275d6800eba57ffe4 с таким кодом:

        (ns quickstart.helloworld)
        
        (defn main []
          (let [v1 [1 2 3]
                v2 (conj v1 4)]
            (print (str :v1 v1))
            (print (str :v2 v2))))

        Вот что сгенерировалось:

        import "dart:core" as dc;
        import "helloworld.dart" as lcoq_helloworld;
        import "../cljd/core.dart" as lcoc_core;
        
        // BEGIN main
        dc.dynamic main(){
        final dc.List<dc.dynamic> fl$1=(dc.List<dc.dynamic>.filled(3, 1, ));
        fl$1[1]=2;
        fl$1[2]=3;
        final lcoc_core.PersistentVector v1$1=lcoc_core.$_vec_owning(fl$1, );
        final lcoc_core.PersistentVector coll7498$1=v1$1;
        late final dc.dynamic v2$1;
        if((coll7498$1 is lcoc_core.ICollection$iface)){
        v2$1=((coll7498$1 as lcoc_core.ICollection$iface).$_conj$1(4, ));
        }else{
        v2$1=((lcoc_core.ICollection.extensions((coll7498$1 as dc.dynamic), ) as lcoc_core.ICollection$ext).$_conj$1((coll7498$1 as dc.dynamic), 4, ));
        }
        lcoc_core.print.$_invoke$1((lcoc_core.str.$_invoke$2(const lcoc_core.Keyword(null, "v1", 2915138964, ), v1$1, )), );
        return (lcoc_core.print.$_invoke$1((lcoc_core.str.$_invoke$2(const lcoc_core.Keyword(null, "v2", 1579994680, ), v2$1, )), ));
        }
        
        // END main

        Создание персистентного вектора v1:

        final lcoc_core.PersistentVector coll7498$1=v1$1;

        Создание персистентного вектора v2:

        v2$1=((coll7498$1 as lcoc_core.ICollection$iface).$_conj$1(4, ));

        Т.е. добавляем в первый вектор цифру 4, и это не изменяет первый вектор, а создает новый за O(Log32n), а не линейно.

        Вызваем dart run и видим, что выводится:

        :v1[1 2 3]:v2[1 2 3 4]

        Т.е. добавление в v1 не изменило его.

        Я предположу, что ваш случай c созданием списка тут

        (conj [1 2 3] 4)

        объясняется тем, что компилятор понимает, что не зачем тут сперва создавать вектор из трех элементов, а затем добавлять в него элемент, а проще сразу создать вектор из 4 элементов.


        1. nikita_dol
          15.12.2022 14:48

          Собраю на коммите 1c519109b5dda0c24badc7bea3b89b52f3d3db8f

          Этот код создаёт новый вектор:

              v2$1 = ((coll7502$1 as lcoc_core.ICollection$iface).$_conj$1(
                4,
              ));

          И если пройтись дебаггером, то мы попадём в создание списка и заполнение его

          Скриншот дебаггера


          1. arturdumchev Автор
            15.12.2022 19:13

            Похоже, тут дело в том, что переиспользоваться массивы внутри вектора будут, только если элементов в векторе больше 32 (статья). Спросил разраба ClojureDart — у них такая реализация.


            1. nikita_dol
              15.12.2022 19:49
              +1

              Вижу изменения в работе - оно разделяет на куски по 32

              Решил ещё глянуть как выглядит удаление последнего элемента - это чудо создаётся если в списке на 37 элементов вызвать drop-last

              Дебаггер
              v2$1 = {LazySeq} (1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
               meta = null
               fn = null
               s = {Cons} (1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                meta = null
                $UNDERSCORE_first = 1
                rest = {LazySeq} (2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                 meta = null
                 fn = null
                 s = {Cons} (2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                  meta = null
                  $UNDERSCORE_first = 2
                  rest = {LazySeq} (3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                   meta = null
                   fn = null
                   s = {Cons} (3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                    meta = null
                    $UNDERSCORE_first = 3
                    rest = {LazySeq} (4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                     meta = null
                     fn = null
                     s = {Cons} (4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                      meta = null
                      $UNDERSCORE_first = 4
                      rest = {LazySeq} (5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                       meta = null
                       fn = null
                       s = {Cons} (5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                        meta = null
                        $UNDERSCORE_first = 5
                        rest = {LazySeq} (6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                         meta = null
                         fn = null
                         s = {Cons} (6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                          meta = null
                          $UNDERSCORE_first = 6
                          rest = {LazySeq} (7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                           meta = null
                           fn = null
                           s = {Cons} (7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                            meta = null
                            $UNDERSCORE_first = 7
                            rest = {LazySeq} (8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                             meta = null
                             fn = null
                             s = {Cons} (8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                              meta = null
                              $UNDERSCORE_first = 8
                              rest = {LazySeq} (9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                               meta = null
                               fn = null
                               s = {Cons} (9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                meta = null
                                $UNDERSCORE_first = 9
                                rest = {LazySeq} (10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                 meta = null
                                 fn = null
                                 s = {Cons} (10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                  meta = null
                                  $UNDERSCORE_first = 10
                                  rest = {LazySeq} (11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                   meta = null
                                   fn = null
                                   s = {Cons} (11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                    meta = null
                                    $UNDERSCORE_first = 11
                                    rest = {LazySeq} (12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                     meta = null
                                     fn = null
                                     s = {Cons} (12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                      meta = null
                                      $UNDERSCORE_first = 12
                                      rest = {LazySeq} (13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                       meta = null
                                       fn = null
                                       s = {Cons} (13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                        meta = null
                                        $UNDERSCORE_first = 13
                                        rest = {LazySeq} (14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                         meta = null
                                         fn = null
                                         s = {Cons} (14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                          meta = null
                                          $UNDERSCORE_first = 14
                                          rest = {LazySeq} (15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                           meta = null
                                           fn = null
                                           s = {Cons} (15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                            meta = null
                                            $UNDERSCORE_first = 15
                                            rest = {LazySeq} (16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                             meta = null
                                             fn = null
                                             s = {Cons} (16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                              meta = null
                                              $UNDERSCORE_first = 16
                                              rest = {LazySeq} (17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                               meta = null
                                               fn = null
                                               s = {Cons} (17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                                meta = null
                                                $UNDERSCORE_first = 17
                                                rest = {LazySeq} (18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                                 meta = null
                                                 fn = null
                                                 s = {Cons} (18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                                  meta = null
                                                  $UNDERSCORE_first = 18
                                                  rest = {LazySeq} (19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                                   meta = null
                                                   fn = null
                                                   s = {Cons} (19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                                    meta = null
                                                    $UNDERSCORE_first = 19
                                                    rest = {LazySeq} (20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                                     meta = null
                                                     fn = null
                                                     s = {Cons} (20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                                      meta = null
                                                      $UNDERSCORE_first = 20
                                                      rest = {LazySeq} (21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                                       meta = null
                                                       fn = null
                                                       s = {Cons} (21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                                        meta = null
                                                        $UNDERSCORE_first = 21
                                                        rest = {LazySeq} (22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                                         meta = null
                                                         fn = null
                                                         s = {Cons} (22 23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                                          meta = null
                                                          $UNDERSCORE_first = 22
                                                          rest = {LazySeq} (23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                                           meta = null
                                                           fn = null
                                                           s = {Cons} (23 24 25 26 27 28 29 30 31 32 33 34 35 36)
                                                            meta = null
                                                            $UNDERSCORE_first = 23
                                                            rest = {LazySeq} (24 25 26 27 28 29 30 31 32 33 34 35 36)
                                                             meta = null
                                                             fn = null
                                                             s = {Cons} (24 25 26 27 28 29 30 31 32 33 34 35 36)
                                                              meta = null
                                                              $UNDERSCORE_first = 24
                                                              rest = {LazySeq} (25 26 27 28 29 30 31 32 33 34 35 36)
                                                               meta = null
                                                               fn = null
                                                               s = {Cons} (25 26 27 28 29 30 31 32 33 34 35 36)
                                                                meta = null
                                                                $UNDERSCORE_first = 25
                                                                rest = {LazySeq} (26 27 28 29 30 31 32 33 34 35 36)
                                                                 meta = null
                                                                 fn = null
                                                                 s = {Cons} (26 27 28 29 30 31 32 33 34 35 36)
                                                                  meta = null
                                                                  $UNDERSCORE_first = 26
                                                                  rest = {LazySeq} (27 28 29 30 31 32 33 34 35 36)
                                                                   meta = null
                                                                   fn = null
                                                                   s = {Cons} (27 28 29 30 31 32 33 34 35 36)
                                                                    meta = null
                                                                    $UNDERSCORE_first = 27
                                                                    rest = {LazySeq} (28 29 30 31 32 33 34 35 36)
                                                                     meta = null
                                                                     fn = null
                                                                     s = {Cons} (28 29 30 31 32 33 34 35 36)
                                                                      meta = null
                                                                      $UNDERSCORE_first = 28
                                                                      rest = {LazySeq} (29 30 31 32 33 34 35 36)
                                                                       meta = null
                                                                       fn = null
                                                                       s = {Cons} (29 30 31 32 33 34 35 36)
                                                                        meta = null
                                                                        $UNDERSCORE_first = 29
                                                                        rest = {LazySeq} (30 31 32 33 34 35 36)
                                                                         meta = null
                                                                         fn = null
                                                                         s = {Cons} (30 31 32 33 34 35 36)
                                                                          meta = null
                                                                          $UNDERSCORE_first = 30
                                                                          rest = {LazySeq} (31 32 33 34 35 36)
                                                                           meta = null
                                                                           fn = null
                                                                           s = {Cons} (31 32 33 34 35 36)
                                                                            meta = null
                                                                            $UNDERSCORE_first = 31
                                                                            rest = {LazySeq} (32 33 34 35 36)
                                                                             meta = null
                                                                             fn = null
                                                                             s = {Cons} (32 33 34 35 36)
                                                                              meta = null
                                                                              $UNDERSCORE_first = 32
                                                                              rest = {LazySeq} (33 34 35 36)
                                                                               meta = null
                                                                               fn = null
                                                                               s = {Cons} (33 34 35 36)
                                                                                meta = null
                                                                                $UNDERSCORE_first = 33
                                                                                rest = {LazySeq} (34 35 36)
                                                                                 meta = null
                                                                                 fn = null
                                                                                 s = {Cons} (34 35 36)
                                                                                  meta = null
                                                                                  $UNDERSCORE_first = 34
                                                                                  rest = {LazySeq} (35 36)
                                                                                   meta = null
                                                                                   fn = null
                                                                                   s = {Cons} (35 36)
                                                                                    meta = null
                                                                                    $UNDERSCORE_first = 35
                                                                                    rest = {LazySeq} (36)
                                                                                     meta = null
                                                                                     fn = null
                                                                                     s = {Cons} (36)
                                                                                      meta = null
                                                                                      $UNDERSCORE_first = 36
                                                                                      rest = {LazySeq} ()
                                                                                       meta = null
                                                                                       fn = null
                                                                                       s = null
                                                                                       $UNDERSCORE_$UNDERSCORE_hash = -1
                                                                                      $UNDERSCORE_$UNDERSCORE_hash = -1
                                                                                     $UNDERSCORE_$UNDERSCORE_hash = -1
                                                                                    $UNDERSCORE_$UNDERSCORE_hash = -1
                                                                                   $UNDERSCORE_$UNDERSCORE_hash = -1
                                                                                  $UNDERSCORE_$UNDERSCORE_hash = -1
                                                                                 $UNDERSCORE_$UNDERSCORE_hash = -1
                                                                                $UNDERSCORE_$UNDERSCORE_hash = -1
                                                                               $UNDERSCORE_$UNDERSCORE_hash = -1
                                                                              $UNDERSCORE_$UNDERSCORE_hash = -1
                                                                             $UNDERSCORE_$UNDERSCORE_hash = -1
                                                                            $UNDERSCORE_$UNDERSCORE_hash = -1
                                                                           $UNDERSCORE_$UNDERSCORE_hash = -1
                                                                          $UNDERSCORE_$UNDERSCORE_hash = -1
                                                                         $UNDERSCORE_$UNDERSCORE_hash = -1
                                                                        $UNDERSCORE_$UNDERSCORE_hash = -1
                                                                       $UNDERSCORE_$UNDERSCORE_hash = -1
                                                                      $UNDERSCORE_$UNDERSCORE_hash = -1
                                                                     $UNDERSCORE_$UNDERSCORE_hash = -1
                                                                    $UNDERSCORE_$UNDERSCORE_hash = -1
                                                                   $UNDERSCORE_$UNDERSCORE_hash = -1
                                                                  $UNDERSCORE_$UNDERSCORE_hash = -1
                                                                 $UNDERSCORE_$UNDERSCORE_hash = -1
                                                                $UNDERSCORE_$UNDERSCORE_hash = -1
                                                               $UNDERSCORE_$UNDERSCORE_hash = -1
                                                              $UNDERSCORE_$UNDERSCORE_hash = -1
                                                             $UNDERSCORE_$UNDERSCORE_hash = -1
                                                            $UNDERSCORE_$UNDERSCORE_hash = -1
                                                           $UNDERSCORE_$UNDERSCORE_hash = -1
                                                          $UNDERSCORE_$UNDERSCORE_hash = -1
                                                         $UNDERSCORE_$UNDERSCORE_hash = -1
                                                        $UNDERSCORE_$UNDERSCORE_hash = -1
                                                       $UNDERSCORE_$UNDERSCORE_hash = -1
                                                      $UNDERSCORE_$UNDERSCORE_hash = -1
                                                     $UNDERSCORE_$UNDERSCORE_hash = -1
                                                    $UNDERSCORE_$UNDERSCORE_hash = -1
                                                   $UNDERSCORE_$UNDERSCORE_hash = -1
                                                  $UNDERSCORE_$UNDERSCORE_hash = -1
                                                 $UNDERSCORE_$UNDERSCORE_hash = -1
                                                $UNDERSCORE_$UNDERSCORE_hash = -1
                                               $UNDERSCORE_$UNDERSCORE_hash = -1
                                              $UNDERSCORE_$UNDERSCORE_hash = -1
                                             $UNDERSCORE_$UNDERSCORE_hash = -1
                                            $UNDERSCORE_$UNDERSCORE_hash = -1
                                           $UNDERSCORE_$UNDERSCORE_hash = -1
                                          $UNDERSCORE_$UNDERSCORE_hash = -1
                                         $UNDERSCORE_$UNDERSCORE_hash = -1
                                        $UNDERSCORE_$UNDERSCORE_hash = -1
                                       $UNDERSCORE_$UNDERSCORE_hash = -1
                                      $UNDERSCORE_$UNDERSCORE_hash = -1
                                     $UNDERSCORE_$UNDERSCORE_hash = -1
                                    $UNDERSCORE_$UNDERSCORE_hash = -1
                                   $UNDERSCORE_$UNDERSCORE_hash = -1
                                  $UNDERSCORE_$UNDERSCORE_hash = -1
                                 $UNDERSCORE_$UNDERSCORE_hash = -1
                                $UNDERSCORE_$UNDERSCORE_hash = -1
                               $UNDERSCORE_$UNDERSCORE_hash = -1
                              $UNDERSCORE_$UNDERSCORE_hash = -1
                             $UNDERSCORE_$UNDERSCORE_hash = -1
                            $UNDERSCORE_$UNDERSCORE_hash = -1
                           $UNDERSCORE_$UNDERSCORE_hash = -1
                          $UNDERSCORE_$UNDERSCORE_hash = -1
                         $UNDERSCORE_$UNDERSCORE_hash = -1
                        $UNDERSCORE_$UNDERSCORE_hash = -1
                       $UNDERSCORE_$UNDERSCORE_hash = -1
                      $UNDERSCORE_$UNDERSCORE_hash = -1
                     $UNDERSCORE_$UNDERSCORE_hash = -1
                    $UNDERSCORE_$UNDERSCORE_hash = -1
                   $UNDERSCORE_$UNDERSCORE_hash = -1
                  $UNDERSCORE_$UNDERSCORE_hash = -1
                 $UNDERSCORE_$UNDERSCORE_hash = -1
                $UNDERSCORE_$UNDERSCORE_hash = -1
               $UNDERSCORE_$UNDERSCORE_hash = -1

              Итого:

              1. Пока ещё очень сыро.

              2. Компилятор Dart не доволен

              3. Оптимизатор тоже не доволен

              4. Дебаггер в шоке

              5. Стоит подумать над вариантом, когда для web версии компилируется сразу в JS, так как Dart сам оставляет много чего для себя, а ещё будет огромное количество ненужного от ClojureDart


  1. v1z
    16.12.2022 14:42

    А как решается проблема с null safety?