Делаю третий проект на Flutter. И как-то стал задумывать какой же ущербный язык Dart. Там как бы все по минималке.
Сравнить с Kotlin (ближийший конкурент Flutter — это Kotlin Multiplatform). А именно:
1. Для всех целых чисел один тип int. Там все. Если вам нужна экономия байт — подключайте спец. библиотеки с экономными типами (1, 2, 4 байта и т.д.).
2. Область видимости методов класса — только public и private. И то нет даже спец. слова — просто все что начинается со знака подчеркивания — это private.
3. Генерики — без сложных ограничений или ковариантности.
4. Базовые коллекции — 3 штуки — список, словарь и set (уникальные).
5. Нет даже деструкторов — ни встроенного dispose ничего. Т.е. руками.
6. С лямбдами раз в 5 меньше фишек.
Но оно в принципе работает. Т.е. язык до максимума примитивен, но тем не менее он строго типизирован и все эти мелочи, в принципе, не так уж и важны.
Здравствуйте, Shmj, Вы писали:
S>Делаю третий проект на Flutter. И как-то стал задумывать какой же ущербный язык Dart. Там как бы все по минималке.
Go так же начинался. Со временем добавили некоторые вещи.
Чем проще — тем легче поддерживать.
Легче писать поддержку в редакторе, проще анализировать, проще проводить ревью.
Не требует высокой квалификации людей.
Одни плюсы для корпорации.
Здравствуйте, _NN_, Вы писали:
_NN>Легче писать поддержку в редакторе, проще анализировать, проще проводить ревью. _NN>Не требует высокой квалификации людей.
Ну на счет квалификации — если нет встроенного механизма dispose — это не значит что теперь ресурсы станут бесконечными и вам не нужно будет об этом беспокоиться.
Здравствуйте, Shmj, Вы писали:
S>Делаю третий проект на Flutter. И как-то стал задумывать какой же ущербный язык Dart. Там как бы все по минималке.
S>Сравнить с Kotlin (ближийший конкурент Flutter — это Kotlin Multiplatform). А именно:
S>4. Базовые коллекции — 3 штуки — список, словарь и set (уникальные).
Чем-то похоже на swift, тоже queue и stack в стандартныц набор на входят. Кстати, protected тоже нет, но есть деление на public и open. Ещё fileprivate есть зачем-то. Но generic использовать в swift удобно, поскольку protocols позволяют, скажем, конструкторы указывать, а ещё для них доступен zero cost abstraction.
Здравствуйте, Shmj, Вы писали:
S>1. Для всех целых чисел один тип int. Там все. Если вам нужна экономия байт — подключайте спец. библиотеки с экономными типами (1, 2, 4 байта и т.д.).
вот эта мелочь нужна только в очень специфических случаях, когда твои типы данных жестко заданы, например поток звука 2byte 8khz unsinged, или 32bit float normalised 30khz, (это я условно)
внутри своей программы возможно гораздо быстрее будет работать с более широкими типами, лишь потому, что процессор поддерживает лишь такие векторные вычисления. Возможно сумбурно объясняю, всё еще Новый Год не закончился
но смысл в общем такой — экономь байты только если реально не хватает производительности, а в остальном положись на компилятор, его писали умные люди
Здравствуйте, Shmj, Вы писали:
S>1. Для всех целых чисел один тип int.
Который Int64, т.е. сам язык Dart был разработан для актуальной нынешней ширины слова процессоров в мейнстриме.
Плюс интероперабельность с JS, где есть только Number (Float64) и BigInt — в Dart-е аналогично поддерживаются эти типы.
Не забываем, что и v8, и Dart писался одной конторой и в большинстве своём одними и теми же людьми.
Унутре v8 хранит в том числе целые числа (хотя в спецификации JS их нет, т.е. это результат JIT-оптимизаций), но хранит или по указателю целиком 64 бит, или как 32-битные "плоские" значения (младший бит 1 — указатель, младший бит 0 — плоское значение в старших 32 битах).
Dart позволяет лучшую типизированность, поэтому может хранить 64-битные целые без траты бит на признаки типа значения.
S>Там все. Если вам нужна экономия байт — подключайте спец. библиотеки с экономными типами (1, 2, 4 байта и т.д.).
Всё верно, бо ширина данных меньше слова оправдана только при хранении заметного кол-ва данных, поэтому, эти типы представлены сразу в виде их хранилищ.
Dart разрабатывался как язык, который может работать с сильной типизацией или нет.
А для слабой/динамической типизации обилие базовых типов — зло.
Не зря же скриптовые языки имеют минимум базовых типов.
S>2. Область видимости методов класса — только public и private. И то нет даже спец. слова — просто все что начинается со знака подчеркивания — это private.
Что тоже отражает популярный мейнстримовый приём — именовать приватные члены с подчёркивания.
Ведь в теле методов нет аннотаций мемберов, верно? Ну и вот...
S>3. Генерики — без сложных ограничений или ковариантности.
А в C# какие есть "сложные" ограничения, кроме появившихся не так давно struct и unmanaged?
В Dart такие же точно ограничения на основе интерфейсов (неявных оных в механике Dart, образованных из публичных членов классов).
Плюс крутой typedef, затрагивающий, в том числе, генерики и функциональные типы:
typedef ListMapper<X> = Map<X, List<X>>;
Map<String, List<String>> m1 = {}; // Verbose.
ListMapper<String> m2 = {}; // Same thing but shorter and clearer.
typedef Compare<T> = int Function(T a, T b);
int sort(int a, int b) => a - b;
void main() {
assert(sort is Compare<int>); // True!
}
S>4. Базовые коллекции — 3 штуки — список, словарь и set (уникальные).
В поставке идёт намного больше, чего стоит один только TypedDataList или Uint8ClampedList. ))
Т.е., в Dart пошли от обратного — "плоский массив" — это отдельный класс и только для байт, либо же для view массива байт, где этот view может маппиться только на последовательность чисел различной разрядности.
А встроенные в язык массивы — динамические, представленные генериком List<>.
Это позволило оперировать удобным синтаксисом [], не хуже, чем в JS.
Для сравнения, в C# и Джаве в плане синтаксиса для динамических массивов всё плохо.
Плюс они избежали многих проблем, вызванных в C# или Джаве одновременным наличием встроенных массивов и высокоуровневых объектов List/ArrayList.
Плюс сделали операторы+ для списков, что сделало синтаксис приятней.
В общем, язык явно писался "для себя", и потому что особенности других мейнстримовых недоязыков заметно подбешивают своей сыростью-недоработанностью.
Как ни крути, но C# и Джава — это очень сырые, очень незрелые, местами крайне раздражающие своими детскими болезнями языки, которые еще дорабатывать и дорабатывать.
А ведь им по 25-30 лет.
Котлин чуть повзрослее будет, но тоже еще расти ему прилично.
Dart сразу появился относительно взрослым, ему расти меньше всех.
S>5. Нет даже деструкторов — ни встроенного dispose ничего. Т.е. руками.
В GC-языках по-определению не может быть scope-based финализации, она отродясь там жила через finally.
В C# придумали уродский сахарок using(IDisposable obj = ...) {}, которому потребовалось аж 22 года, чтобы превратиться в человеческий using var obj=...; в текущем scope.
S>6. С лямбдами раз в 5 меньше фишек.
С лямбдами там раз в 10 лучше, чем в C#.
Они не сделали ошибки типизации делегатов, там функциональный тип вполне честный, как в JS, поэтому всё нааамного проще/интероперабельней.
S>Но оно в принципе работает. Т.е. язык до максимума примитивен
Он не примитивен, он выразителен.
В нём сразу избежали многих ненужных повторений и громозкости, как в дихотомии массивов/ArrayList/List<> в C# или в несовместимых их делегатах.
S>но тем не менее он строго типизирован и все эти мелочи, в принципе, не так уж и важны.
Это не мелочи.
Это отсутствие постоянного раздражения на ровном месте.
Понятно же, что внутренний перфекционист любого адекватного разработчика страдает при пользовании Джавой, C#, JS, Питоном, С++...
Dart в этом смысле наименее из раздражающих глупостями или унаследованными атавизмами-уродствами язык в современном мейстриме.
Банально "уютный" язык.
Просто надо помнить, что он не для битовыжимания и не для матана, а для выразительности и читабельности при неплохой производительности того самого "бизнес-кода", куда можно отнести и GUI.
Flutter относится к батарейке экономно и вообще летает.
S>Ваше мнение — важны ли вам мелочи?
У Дарта с либами из поставки всё неплохо, т.е. на любую мелочь почти всегда есть готовый ответ.
Постепенно обрастает и третьесторонними либами...
И что впечатляет — это размеры исходного кода этих либ (базовых и третьесторонних).
Сравнить с оными из C# или Джавы.
А ведь "оно же" работает не только для либ, но и для твоего кода! ))
Здравствуйте, wl., Вы писали:
S>>1. Для всех целых чисел один тип int. Там все. Если вам нужна экономия байт — подключайте спец. библиотеки с экономными типами (1, 2, 4 байта и т.д.). wl.>вот эта мелочь нужна только в очень специфических случаях, когда твои типы данных жестко заданы, например поток звука 2byte 8khz unsinged, или 32bit float normalised 30khz, (это я условно)
Для этих сценариев есть встроенные либы-контейнеры, либо объекты-view, когда массив байт рассматривается как последовательность float32, к примеру.
wl.>но смысл в общем такой — экономь байты только если реально не хватает производительности, а в остальном положись на компилятор, его писали умные люди
При хранении заметного объема данных их ширина может играть рояль.
Для этого сценария в Dart всё есть.
Здравствуйте, Shmj, Вы писали:
S>Ну на счет квалификации — если нет встроенного механизма dispose — это не значит что теперь ресурсы станут бесконечными и вам не нужно будет об этом беспокоиться.
А это из опыта C#.
Отложенная финализация по-умолчанию — злейшее зло, как показала практика.
Этот механизм де-факто не работает, увы, хотя был разработан для сценариев, где невнимательный программист прошляпил своевременное освобождение ресурсов.
Например, лапша-программер забыл своевременно освободить какой-нить сокет-листенер — и затем прога не может повторно сесть на порт новым сокетом в течении десятков минут или даже нескольких часов, если прежний сокет уже убежал во второе поколение, а нагрузка на приложуху небольшая, т.е. второе поколения не трогается GC оч долго.
Поэтому, для чувствительных к освобождению ресурсов объектов рулит или своевременная финализация, или никакая.
Плюс, для ресурсоёмких shared-объектов иерархия владения должна быть только однонаправленной, тут хорошо работает ref-counting, не зря есть объекты InspectorReferenceData, которые обслуживают этот сценарий (в несколько строк ты можешь нарисовать свои хелперы для аналогичного).
Ну и, для отложенной финализации есть классы Finalizer и NativeFinalizer, которые, в отличие от C#, надо дёргать явно, т.е. в очередь финализации попадает не каждый объект.
Тут убивается сразу два зайца:
— явное задействование механизма подразумевает, что программер продумал схему управления ресурсами для целевых объектов, т.е. что отложенная финализация для целевых ресурсов допустима;
— в очередь финализации ставятся не все объекты подряд, а лишь единичные, что делает сборку мусора резко дешевле, чем в том же C#.
Плюс еще есть WeakReference в языке, а самое главное — миксины!
Миксины позволяют реализовать различные политики управления ресурсами лишь однажды (в неких своих миниатюрных хелперах, в т.ч. которые refcount), а затем использовать их в произвольных объектах.
Здравствуйте, Shmj, Вы писали:
S>Здравствуйте, _NN_, Вы писали:
_NN>>Легче писать поддержку в редакторе, проще анализировать, проще проводить ревью. _NN>>Не требует высокой квалификации людей.
S>Ну на счет квалификации — если нет встроенного механизма dispose — это не значит что теперь ресурсы станут бесконечными и вам не нужно будет об этом беспокоиться.
Всё верно. Однако это упрощает язык.
Обычно в языках со сборщиком мусора в основной массе объекты явно освобождать не требуется.
В JS так работает со создания языка и ничего, как-то справляемся.
Это относительно недавно предложение со словом “using” дошло до 3-й стадии и это ещё не на гарантирует попадание в язык в таком же виде.
S>Для Windows 32 — нету Но благо что таких девайсов все меньше.
Периодически посматриваю на средства кроссплатформенной разработки (для личных проектов).
И конечно 32-битный MS Windows не хотелось бы выкидывать, хотя бы потому что я сам пока в пользовании такое имею.
Смотрел ещё Vala (тоже язык с автоматическим управлением памятью, но компиляцией в нативный код). Оно работает в 32битных ОС. Но там (вернее в glib) поддержку winvista убрали
Хотя это поправимо, благо opensource, можно и старую версию кода взять, и новую пропатчить.
Но если нужно совсем без лишний возни, то IMHO лучшн взять Java: оно работало под всем, где есть JRE, причем ещё лет 20 назад.
Здравствуйте, m2user, Вы писали:
M>Но если нужно совсем без лишний возни, то IMHO лучшн взять Java: оно работало под всем, где есть JRE, причем ещё лет 20 назад.
Но вот реально как Java-приложение запустить на iOS?
Да и Java то разная бывает. Пишите под Android — это тоже Java, но оно не будет работать как десктопное приложение на Windows или MacOS или в браузере.
Просто когда реально пытаешься сделать кросс-платформу и запустить на всех 6 платформах (Android, iOS, Windows, MacOS, Linux и WebAssembly) — то возникают препятствия.ж
V>С лямбдами там раз в 10 лучше, чем в C#. V>Они не сделали ошибки типизации делегатов, там функциональный тип вполне честный, как в JS, поэтому всё нааамного проще/интероперабельней.
S>Но вот реально как Java-приложение запустить на iOS? S>Да и Java то разная бывает. Пишите под Android — это тоже Java, но оно не будет работать как десктопное приложение на Windows или MacOS или в браузере.
я думаю это следствие общей нестабильности и переменчивости в разработке для мобильных устройств.
Раньше было актуальлно j2me, WindowsMobile и Symbian, сейчас актуально Andoid/iOS, через 10-15 лет будет опять что-то другое.
S>Просто когда реально пытаешься сделать кросс-платформу и запустить на всех 6 платформах (Android, iOS, Windows, MacOS, Linux и WebAssembly) — то возникают препятствия.ж
Я думаю, что написать одно приложение под desktop и наладонник в общём случае невозможно и не нужно. Просто в виду слишком различных параметров устройств ввода/вывода.
Но вот сделать так, чтобы они использовали общие библиотеки конечно хотелось бы, чтобы не переписывать бизнес-логику.
И вроде как это решается через нативные библиотеки: тут были обсуждения, почему некоторые приложения под андроид такие большие, а там внутри куча нативных библиотек причем для нескольких arm железок.
Здравствуйте, m2user, Вы писали:
M>Я думаю, что написать одно приложение под desktop и наладонник в общём случае невозможно и не нужно. Просто в виду слишком различных параметров устройств ввода/вывода. M>Но вот сделать так, чтобы они использовали общие библиотеки конечно хотелось бы, чтобы не переписывать бизнес-логику. M>И вроде как это решается через нативные библиотеки: тут были обсуждения, почему некоторые приложения под андроид такие большие, а там внутри куча нативных библиотек причем для нескольких arm железок.
Можно написать, адаптивный дизайн называется. Просто еще это только входит в нашу жизнь. Но уже есть, работают в т.ч. на MacOS работают многие iOS -приложения.
Здравствуйте, Sharov, Вы писали:
V>>С лямбдами там раз в 10 лучше, чем в C#. V>>Они не сделали ошибки типизации делегатов, там функциональный тип вполне честный, как в JS, поэтому всё нааамного проще/интероперабельней. S>А что не так с типизацией делегатов в C#?
1. Делегаты разных типов не совместимы, даже если совместимы их сигнатуры. "Приведение совместимых сигнатур" на деле означает создание нового делегата, который вызывает метод Invoke у целевого.
2. Само создание делегатов очень затратное — примерно в 3-5 раз дороже создания обычного объекта. Без привязки к специфическому типу и без инициализации доп.полей, служащих исключительно и только целям рефлексии, без генерации тела Invoke на лету (оно разное для экземплярных и статических методов) создание делегата было бы не дороже создания обычного объекта, как оно есть в остальных языках, поддерживающих функциональный тип.
Например, в статически-типизированных языках все thunk-и созданы заранее для всех мест, где создаются лямбды. В дотнете, однако, даже для статически-типизированных мест вызывается тот же код инициализации делегата, что и при создании делегата динамически из хендла-метаинформации метода. И это настолько забористая дичь, однако, что нема словей, одни выражовывания.
Одним словом, изначальное качество проектирования платформы ни к чёрту.
Местами такое ощущение, что самоучки от IT ваяли. ))
А теперь уже хрен переиграешь.
3. Библиотечные костыли, навроде Action<> и Func<>, призванные исправить недостаток п.1. хотя бы в самых общих местах нового кода (старый остался смердеть навечно из-за требований совместимости с г-ном 22-хлетней давности), в любом случае не покрывают in/ref/out-спецификации аргументов. В этом месте надо описывать уникальные делегаты, привет п.1.
=====================
В своём коде накатал struct-based библиотеку функциональных объектов с полагающимся минимальным джентльменским набором биндинга, реордеринга, apply и прочего.
Удалось единообразно покрыть как старые делегаты, так и новые unmanaged-делегаты, то бишь простые указатели на ф-ии, которые работают со скоростью света — примерно как в С/С++, т.е. ничего не стоят при создании и почти ничего не стоят при вызове. Ну и свои легковесные struct-based и class-based функторы на манер C++, разумеется. Жаль, пока не удалось допилить внутренние кишки async-автоматов, генерируемые компилятором (т.е., встроиться туда для своих Task-like объектов удалось легко, но заставить генерить код автомата без использования делегатов — пока нет, руки не дошли... и пока что асинхронщина дотнета — это та еще гиря на ногах, еле пыхтит, бедненькая... )) )
И да, в 64-битных ABI вызовы экземплярных и статических методов одинаковы, этим хаком можно с должной аккуратностью пользоваться на 64-битных платформах, т.е. покрывать указателями на мемберы не только статические методы, но и экземплярные.
V>2. Само создание делегатов очень затратное — примерно в 3-5 раз дороже создания обычного объекта. Без привязки к специфическому типу и без инициализации доп.полей, служащих исключительно и только целям рефлексии, без генерации тела Invoke на лету (оно разное для экземплярных и статических методов) создание делегата было бы не дороже создания обычного объекта, как оно есть в остальных языках, поддерживающих функциональный тип.
А почему тело Invoke на лету генерируется? Это тело где-то должно уже быть сгенерированным. Если только ф-ия вызывается
не первый раз.
Здравствуйте, Sharov, Вы писали:
S>А почему тело Invoke на лету генерируется?
Потому что тело Invoke может быть уникальным для различных экземпляров делегатов одного и того же типа.
Причём, даже если тело Invoke было ранее создано для конкретного SomeObj.SomeMethod, всё равно каждый раз с 0-ля генерится Invoke конкретно для типа SomeObj и его метода SomeMethod.
Т.е., с каждым экземпляром делегата растёт и память, занимаемая областью кода.
И финализация делегатов от этого тоже чуть дороже, потому что надо убирать память из сегментов кода, а там с перемещениями и уплотнениями не так всё радужно, как в сегментах данных.
Здравствуйте, vdimas, Вы писали:
S>>А почему тело Invoke на лету генерируется? V>Потому что тело Invoke может быть уникальным для различных экземпляров делегатов одного и того же типа.
V>Причём, даже если тело Invoke было ранее создано для конкретного SomeObj.SomeMethod, всё равно каждый раз с 0-ля генерится Invoke конкретно для типа SomeObj и его метода SomeMethod. V>Т.е., с каждым экземпляром делегата растёт и память, занимаемая областью кода. V>И финализация делегатов от этого тоже чуть дороже, потому что надо убирать память из сегментов кода, а там с перемещениями и уплотнениями не так всё радужно, как в сегментах данных.
А про это можно где-то почитать, потому что звучит крайне странно? Ладно, в первый раз надо скомплировать il код, но
далее он же кэширутся как для SomeObj.SomeMethod так и для других делегатов. А в новый версиях C# (>= 3.0) это
как-то исправлялось?
Здравствуйте, Sharov, Вы писали:
S>V>Причём, даже если тело Invoke было ранее создано для конкретного SomeObj.SomeMethod, всё равно каждый раз с 0-ля генерится Invoke конкретно для типа SomeObj и его метода SomeMethod. V>>Т.е., с каждым экземпляром делегата растёт и память, занимаемая областью кода. V>>И финализация делегатов от этого тоже чуть дороже, потому что надо убирать память из сегментов кода, а там с перемещениями и уплотнениями не так всё радужно, как в сегментах данных. S>А про это можно где-то почитать, потому что звучит крайне странно?
Это можно продебажить.
S>Ладно, в первый раз надо скомплировать il код, но
Дык, в том и суть, что способы создания функциональных объектов уже много десятилетий хорошо обкатаны в статически-типизированных языках.
Для каждого функционального объекта еще на этапе компиляции создаётся специальная функция, которая исполняет некое своё тело. И создание функционального объекта заключается в создании экземпляра объекта, который хранит данные захваченного контекста и указатель на эту сгенерённую функцию. Таким образом, экземпляры делегатов от одного thunk чаще всего отличаются только фактически захваченными аргументами (контекстом), а в случае, когда контекст (замыкание или частичные применения аргументов) отсутствуют, то делегат может быть вовсе синглтоном-константой (и это известно тоже еще на этапе компиляции).
Т.е., создание функционального объекта должно быть очень дешевым.
Почему в дотнете не сделали так же — вопрос на 100 мильонов. ))
S>далее он же кэширутся как для SomeObj.SomeMethod так и для других делегатов. А в новый версиях C# (>= 3.0) это S>как-то исправлялось?
Это надо смотреть от 8-го дотнета, там что-то пытались оптимизировать, я еще не дебажил.
Может и кешируют уже что-то.
Но до 8-го оно было так.
И я не видел патчей, которые создают эти внутренние свободные функции с телами делегатов на этапе компиляции.
Вряд ли пропустил, но если бы мне самому показали, что это уже довели до ума — было бы здорово, конечно.
Но что-то сомневаюсь, бо это натурально катастрофические изменения компилятора. Ведь рантайм должен поддерживать и старый способ, т.к. не только язык C# там работает, плюс рантайм еще должен уметь исполнять весь старый байт-код. Т.е. тут нужны как новые ср-ва рантайма, так и использование их из компилятора. Я о таком пока не слышал.
Здравствуйте, Shmj, Вы писали:
S>1. Для всех целых чисел один тип int. Там все.
Ну и правильно. А то в Расте том же заманали дрочеры на "доменно-ориентированное" говно. Один такой дрочер в своей либе использует u32, другой i32, третий usize. А потом мне надо сложить два результата или проитерироваться по результату, заполняя массив и начинается, ведь идеоматический Раст запрещает складывать i32 с usize, ну и код превращается в "ехал явно написанный числовой каст через явно написанный числовой каст". За этими приведениями типов логику происходящего не видно.
Нет такой подлости и мерзости, на которую бы не пошёл gcc ради бессмысленных 5% скорости в никому не нужном синтетическом тесте