основанной на ссылках на динамическую память, вероятно, стоит конкурировать с королями списков - Lisp/Racket.
Языки, в которых полиморфные структуры данные, контейнеры переменных размеров и графы объектов представлены ссылками на динамически аллоцированную память - это вообще все языки программирования.
За мою 30+ летнюю практику в программировании я или сразу реализовывал все приложения на DOM-подобных структурах данных или, приходя в доведенный до ручки проект подкупом, лестью, угрозами переводил задачу на DOM-подобные структуры данных, попутно вычищая тонны костылей и убирая глюки уровня бизнес-логики, утечки и падения. В каком-то смысле вы правы, DOM составляет не 100% от структур данных программы, а только 99%. Остальное можно реализовать на C/C++ тщательно покрыть тестами и выставить на уровень бизнес-логики через FFI.
по поводу классов: от того, что всегда есть значения по умолчанию, всё хорошо будет, не в этой модели, а в общем случае? и не будет ошибок с тем, что потенциально можно пропустить инициализацию какого-то поля с данной моделью их инициализации? хотя понимаю, что более старые языки тоже многие к этому склонны, но всё-таки.
Я бы сказал наоборот, более новые языки отказываются от конструкторов, например тот же Раст. В языках, где есть конструкторы (например в Java) вводятся особые категории объектов (beans) имеющие конструкторы без параметров, чтобы поддерживать сериализацию для сетевого общения и персистентности. Или вводится паттерн builder, который позволяет избавиться от ограничений конструкторов.
и если тут утверждается, что тут счёт ссылок неатомарный, то что будет в многопотоке?
Аргентум возводит неизменяемость шаренных объектов в абсолют (да пребудет с ним сила). Это даёт важную гарантию: любой поток, имеющий шаренную ссылку на объект, может быть уверен, что этот объект и вся доступная из него по любому графу ссылок структура тоже неизменяемы. Более того, все эти объекты гарантированно живы, а ссылки валидны до тех пор, пока поток удерживает корневую ссылку. А корневая ссылка всегда лежит либо в стеке потока, либо в объекте, принадлежащем этому потоку. Отсюда имеем два следствия:
почти весь доступ к таким объектам можно выполнять вообще без операций retain/release;
доступность шареных объектов определяется исключительно поведением самого потока, безотносительно действий других потоков.
Поэтому даже те немногие операции retain/release, которые всё же нужны, можно откладывать, группировать в пачки и свободно переупорядочивать - главное, чтобы retain всегда предшествовал release. Благодаря этому их можно выполнять либо на отдельном служебном потоке, либо под одним мьютексом, захватываемым раз в десятки тысяч операций. В итоге общая стоимость синхронизации тоже делится на те же десятки тысяч.
есть ли какая-то гарантия что я не протащу слабую ссылку в другой поток имея возможность редактировать основную из этого?
Вы можете передавать слабые ссылки между потоками и посылать по ним асинхронные сообщения (система сама доставляет такие сообщения в нужный поток). Но вы не можете синхронно разыменовывать слабую ссылку, указывающую на объект другого потока. Именно поэтому в Аргентуме отсутствуют гонки данных.
А поскольку единственный способ межпоточного взаимодействия — это асинхронная отправка сообщений, дедлоки тоже исключены.
Тема синхронизации и многопоточности в Аргентуме достаточно обширна и заслуживает отдельной статьи, а не короткого комментария.
При существенном кол-ве дочерних объектов вместе паутины ссылок проще было хранить их в каком-нибудь дженерик массиве владельца
Дочерние объекты не кмегда хранятся в одной коллекции, они играют разные роли и хранятся в разных полях - скалярных и коллекциях, они часто разнотипные. Например, в ворд-документе есть коллекция страниц, шаблонов, стилей, дочерний объект с метаданными. Не хранить же все это в одной коллекции.
Аналогично, слабые ссылки из этого объекта наружу тоже различаются - по ролям, типам и арности. Их сложно хранить в одном массиве.
Огромное спасибо за код и анализ! Результат похож на JS-код, что естественно, т.к. те же плюсы и минусы GC. Непосредственно реализация 150 строк, примерно как в C++, но если добавить ручной трекинг преекрестных ссылок, то кода станет примерно как в JS.
Есть ряд вопросов: auto hello_text = cast(TextItem)(doc.items[0].items[0]); это cast без рантайм-проверки?
alias CardItem = DomElement; // <-это все-таки новый тип или прозрачный алиас?Если последнее, у нас дыра в типах - можно хранить документ в карточке.
Паники обычно применяются как раз для недопущения UB либо при нарушении инвариантов.
Когда приложение падает, и моя машина становится неуправляемой на хайвее, или когда я теряю с трудом написанный документ или не могу провести презентацию или заказать билеты на самолет, мне все равно, происходит это из-за segfault или из-за паники в RefCell::drop. Этого и есть отсутствие безопасности, этого просто не должно происходить. И Раст с этим обеспечением безопасности не справляется.
Спасбо за подробные советы как нужно делать. Пожалуйста покажите код переделанный в сответствии с этими советами. Это позволит сравнить плюсы и минусы разных подходов.
Проверил код, во всех случаях move переносит локальные объекты со стека в хип, в поля объектов и элементы векторов. Для строк это позволяет убрать все аллокации и сократить объем кода до пары sse/avx инструкций, а при муве смарт-поинтеров полностью исключить работу с атомиками. "Много перегрузок" не надо, если передавать по не-константному значению и потом мувать. Есть еще оптимизация с &&-ссылками появившаяся в `17, но она тут не нужна.
Если у вас есть поле типа T, в котором определен move-конструктор, и у вашего класса есть конструктор, прямо инициализирующий это поле из своего параметра, самый эффективный способ передать этот параметр - по не-констрантному значению с последующим move-ом. Передача по значению позволяет вызывать конструктор как с lvalue, так и с rvalue. Внутри конструктора параметр - это ваша собственная локальная копия, поэтому перемещать из неё всегда безопасно.
Почему следует избегать варианта с передачей по не-константной ссылке:
Приводит к неожиданному и всегда нежелательному изменению объектов вызывающей стороны.
Не может привязываться к rvalue - такой конструктор не может вызываться с временным объектом, фактически выпадая из композиции функций.
Это подробно обсуждалось Скоттом Мейерсом в Effective C++11 14 Sampler.
Добавил вышеперечисленные языки в статью. Кроме Ü т. к. из его документации не ясно, как он работает с иерархиями объектов в хипе. Буду рад если для него Card DOM реализует сам автор языка.
А вообще было бы неплохо иметь рефенсную имплементацию на Аргентуме, чтобы понимать к чему надо стремиться. Отдельно хотелось бы попросить сделать репозиторий с вашими имплементациями на разных языках.
Добавил табличку для сравнения Zig, Odin, Jai, GoLang, Python, The V. Все перечисленные языки попали в две категории - с ручным управлением и с GC.
Все языки построенные на сборщике мусора будут иметь реализацию DOMа аналогичную моему предыдущему примеру на JS. А все языки с ручными управлением памятью зарание и гарантированно уступают всему ранее рассмотренному.
При этом я не говорю, что эти языки плохие. Просто сфера их приложения находится не в области DOM-задач.
Вы всегда можете написать и показать свой более хороший вариант. Или еще проще - взять мой 330-строчный код по ссылке и отрефакторить его в соответствии с вашими предложениями. Это позволит одновременно показать более идеоматический подход и сравнить плюсы и минусы разных вариантов.
Этот подход не применим в embedded/robotics/desktop/mobile. Везде, где падения - это потери данных и долгая починка. Этот подход ограниченно применим только в сетевых решениях, только на бакенде и только если ваш продукт легко переносит roll-back к предыдущей версии, если этот roll-back автоматический и быстрый и если ваши клиенты готовы терперь outages. Даже в этом случае нет гарании, что предыдущая версия на этом же самом упавшем edge-case не упадет снова, потому что эта "логика в коде существовала всегда".
Я помню как в Google Drive тоже пытались делать "надежность через харакири". Кончилось все очень плохо - потерянной репутацией, потерянными корпоративными клиентами и потерей кучи денег. Потом был resilience/hardening проект длительностью в год чтобы заменить все падения восстановлениями.
Предположим, что у меня есть класс с десятком методов, все они пользуются данными класса, 3 из 10 являются реализацией интерфейса, а остальные - или методами других интерфейсов или методы непосредственно класса. У предлагаемого решения есть три пути реализации:
Пусть три метода принимают self как &[mut] Rc<RefCell<Self>> а остальные как &[mut] Self. В этом случае только три дожны делать borrow[_mut] и делать drop-borrow, вызывая друг друга. При этом они могут свободно вызывать остальные 7, но эти 7 никогда не смогут вызвать эти 3. Звучит не очень удобно.
Пусть все методы принимают self как &[mut] Rc<RefCell<Self>> мучаются с borrow/drop/borrow. Зато тогда не будет проблем с невозможностью вызова некоторых методов из других методов.
Пусть все методы принимают self как &[mut] Self но для трех методов будут написаны отдельные методы-обертки, принимающие self как &[mut] Rc<RefCell<Self>>, делающие borrow[_mut] и перевызывающие обычные методы. Но тогда мы не сможем передавать self наружу, например в виде Weak (это иногда справедливо и для 1 кстати).
Итого мы имеем или странные на ровном месте ограничения, или тонну рукописной работы по менеджменту времени жизни заимствований, которая очень похожа на ручное управление памятью на максималках или удвоение количества интерфейсных методов и невозможность подписывать свой объект на внешние события через Weak.
По сравнению с перечисленными проблемами хранение в объекте self-weak не такая большая плата.
Языки, в которых полиморфные структуры данные, контейнеры переменных размеров и графы объектов представлены ссылками на динамически аллоцированную память - это вообще все языки программирования.
За мою 30+ летнюю практику в программировании я или сразу реализовывал все приложения на DOM-подобных структурах данных или, приходя в доведенный до ручки проект
подкупом, лестью, угрозамипереводил задачу на DOM-подобные структуры данных, попутно вычищая тонны костылей и убирая глюки уровня бизнес-логики, утечки и падения. В каком-то смысле вы правы, DOM составляет не 100% от структур данных программы, а только 99%. Остальное можно реализовать на C/C++ тщательно покрыть тестами и выставить на уровень бизнес-логики через FFI.Я у своих очков отпилил дремелем половину стекол и нижнюю часть оправы. И только после этого в них стало реально можно работать.
На видео показан мой стол с клавиатурой и теркболом. На столе нет монитора, он в очках.
Я бы сказал наоборот, более новые языки отказываются от конструкторов, например тот же Раст. В языках, где есть конструкторы (например в Java) вводятся особые категории объектов (beans) имеющие конструкторы без параметров, чтобы поддерживать сериализацию для сетевого общения и персистентности. Или вводится паттерн builder, который позволяет избавиться от ограничений конструкторов.
Аргентум возводит неизменяемость шаренных объектов в абсолют (да пребудет с ним сила). Это даёт важную гарантию: любой поток, имеющий шаренную ссылку на объект, может быть уверен, что этот объект и вся доступная из него по любому графу ссылок структура тоже неизменяемы. Более того, все эти объекты гарантированно живы, а ссылки валидны до тех пор, пока поток удерживает корневую ссылку. А корневая ссылка всегда лежит либо в стеке потока, либо в объекте, принадлежащем этому потоку. Отсюда имеем два следствия:
почти весь доступ к таким объектам можно выполнять вообще без операций retain/release;
доступность шареных объектов определяется исключительно поведением самого потока, безотносительно действий других потоков.
Поэтому даже те немногие операции retain/release, которые всё же нужны, можно откладывать, группировать в пачки и свободно переупорядочивать - главное, чтобы retain всегда предшествовал release. Благодаря этому их можно выполнять либо на отдельном служебном потоке, либо под одним мьютексом, захватываемым раз в десятки тысяч операций. В итоге общая стоимость синхронизации тоже делится на те же десятки тысяч.
Вы можете передавать слабые ссылки между потоками и посылать по ним асинхронные сообщения (система сама доставляет такие сообщения в нужный поток). Но вы не можете синхронно разыменовывать слабую ссылку, указывающую на объект другого потока. Именно поэтому в Аргентуме отсутствуют гонки данных.
А поскольку единственный способ межпоточного взаимодействия — это асинхронная отправка сообщений, дедлоки тоже исключены.
Тема синхронизации и многопоточности в Аргентуме достаточно обширна и заслуживает отдельной статьи, а не короткого комментария.
Я говорю про задачу CardDOM, с жестко прописанными условиями и критериями оценки.
Дочерние объекты не кмегда хранятся в одной коллекции, они играют разные роли и хранятся в разных полях - скалярных и коллекциях, они часто разнотипные. Например, в ворд-документе есть коллекция страниц, шаблонов, стилей, дочерний объект с метаданными. Не хранить же все это в одной коллекции.
Аналогично, слабые ссылки из этого объекта наружу тоже различаются - по ролям, типам и арности. Их сложно хранить в одном массиве.
Огромное спасибо за код и анализ! Результат похож на JS-код, что естественно, т.к. те же плюсы и минусы GC. Непосредственно реализация 150 строк, примерно как в C++, но если добавить ручной трекинг преекрестных ссылок, то кода станет примерно как в JS.
Есть ряд вопросов: auto hello_text = cast(TextItem)(doc.items[0].items[0]);
это cast без рантайм-проверки?
alias CardItem = DomElement; // <-это все-таки новый тип или прозрачный алиас?Если последнее, у нас дыра в типах - можно хранить документ в карточке.
Когда приложение падает, и моя машина становится неуправляемой на хайвее, или когда я теряю с трудом написанный документ или не могу провести презентацию или заказать билеты на самолет, мне все равно, происходит это из-за segfault или из-за паники в RefCell::drop. Этого и есть отсутствие безопасности, этого просто не должно происходить. И Раст с этим обеспечением безопасности не справляется.
Спасбо за подробные советы как нужно делать. Пожалуйста покажите код переделанный в сответствии с этими советами. Это позволит сравнить плюсы и минусы разных подходов.
Проверил код, во всех случаях
moveпереносит локальные объекты со стека в хип, в поля объектов и элементы векторов. Для строк это позволяет убрать все аллокации и сократить объем кода до пары sse/avx инструкций, а при муве смарт-поинтеров полностью исключить работу с атомиками. "Много перегрузок" не надо, если передавать по не-константному значению и потом мувать. Есть еще оптимизация с &&-ссылками появившаяся в `17, но она тут не нужна.Напишите CardDOM на любом из этих языков и покажите их сильные стороны.
Не совсем.
Если у вас есть поле типа T, в котором определен move-конструктор, и у вашего класса есть конструктор, прямо инициализирующий это поле из своего параметра, самый эффективный способ передать этот параметр - по не-констрантному значению с последующим move-ом. Передача по значению позволяет вызывать конструктор как с lvalue, так и с rvalue. Внутри конструктора параметр - это ваша собственная локальная копия, поэтому перемещать из неё всегда безопасно.
Почему следует избегать варианта с передачей по не-константной ссылке:
Приводит к неожиданному и всегда нежелательному изменению объектов вызывающей стороны.
Не может привязываться к rvalue - такой конструктор не может вызываться с временным объектом, фактически выпадая из композиции функций.
Это подробно обсуждалось Скоттом Мейерсом в Effective C++11 14 Sampler.
Добавил вышеперечисленные языки в статью. Кроме Ü т. к. из его документации не ясно, как он работает с иерархиями объектов в хипе. Буду рад если для него Card DOM реализует сам автор языка.
сделаю
Добавил табличку для сравнения Zig, Odin, Jai, GoLang, Python, The V.
Все перечисленные языки попали в две категории - с ручным управлением и с GC.
Все языки построенные на сборщике мусора будут иметь реализацию DOMа аналогичную моему предыдущему примеру на JS. А все языки с ручными управлением памятью зарание и гарантированно уступают всему ранее рассмотренному.
При этом я не говорю, что эти языки плохие. Просто сфера их приложения находится не в области DOM-задач.
Вы всегда можете написать и показать свой более хороший вариант. Или еще проще - взять мой 330-строчный код по ссылке и отрефакторить его в соответствии с вашими предложениями. Это позволит одновременно показать более идеоматический подход и сравнить плюсы и минусы разных вариантов.
Этот подход не применим в embedded/robotics/desktop/mobile. Везде, где падения - это потери данных и долгая починка. Этот подход ограниченно применим только в сетевых решениях, только на бакенде и только если ваш продукт легко переносит roll-back к предыдущей версии, если этот roll-back автоматический и быстрый и если ваши клиенты готовы терперь outages. Даже в этом случае нет гарании, что предыдущая версия на этом же самом упавшем edge-case не упадет снова, потому что эта "логика в коде существовала всегда".
Я помню как в Google Drive тоже пытались делать "надежность через харакири". Кончилось все очень плохо - потерянной репутацией, потерянными корпоративными клиентами и потерей кучи денег. Потом был resilience/hardening проект длительностью в год чтобы заменить все падения восстановлениями.
Очень абстрактно.
Можно ли безопасно и эффективно реализовать на С++ с вашей memsafe-библиотекой или на newlang эту модель данных для редактора карточек?
Будет ли она чем-то лучше стандартного С++ или JS?
Этот подход как раз обсуждается двумя комментами выше (он имеет большие проблемы).
Предположим, что у меня есть класс с десятком методов, все они пользуются данными класса, 3 из 10 являются реализацией интерфейса, а остальные - или методами других интерфейсов или методы непосредственно класса. У предлагаемого решения есть три пути реализации:
Пусть три метода принимают self как &[mut] Rc<RefCell<Self>> а остальные как &[mut] Self. В этом случае только три дожны делать borrow[_mut] и делать drop-borrow, вызывая друг друга. При этом они могут свободно вызывать остальные 7, но эти 7 никогда не смогут вызвать эти 3. Звучит не очень удобно.
Пусть все методы принимают self как &[mut] Rc<RefCell<Self>> мучаются с borrow/drop/borrow. Зато тогда не будет проблем с невозможностью вызова некоторых методов из других методов.
Пусть все методы принимают self как &[mut] Self но для трех методов будут написаны отдельные методы-обертки, принимающие self как &[mut] Rc<RefCell<Self>>, делающие borrow[_mut] и перевызывающие обычные методы. Но тогда мы не сможем передавать self наружу, например в виде Weak (это иногда справедливо и для 1 кстати).
Итого мы имеем или странные на ровном месте ограничения, или тонну рукописной работы по менеджменту времени жизни заимствований, которая очень похожа на ручное управление памятью на максималках или удвоение количества интерфейсных методов и невозможность подписывать свой объект на внешние события через Weak.
По сравнению с перечисленными проблемами хранение в объекте self-weak не такая большая плата.