Comments 50
Про важность понимания времени жизни объекта нам уже поведал Rust. В статье так и не увидел ответа на вопрос, зачем же понадобился новый язык, чтобы делать то, что уже проработано в Rust-е? В чем преимущества и в чем недостатки перед Rust-овой моделью?
Короче, пока я вижу это так, что успех раста многим покоя не дает и непременно нужно создать что-то с теми же идеями, но по-другому. При этом зачастую обоснования этого другого как-то и нет.
Они разные. В некотором смысле - противоположные.
Rust позицируется как язык системного программирования, все его сильные и слабые стороны диктуются этим позицированием. Rust не гарантирует отсутствие утечек памяти. Rust фокуссируется на владении и временах жизни стековых и инлайненных объектов (простых и Box-ed), предлагая для сложных структур в хипе указатели Rc, RefCell, которые разрешают шаринг изменяемых объектов циклы и другие архитектурно-опасные решения. Rust не умеет автоматически копировать/клонировать иерархии объектов, соединеных умными указателями, заставляя писать множество опасного кода вручную. Rust любит панику и рантайм проверки.
Аргентум позицируется как язык прикладного уровня: Он гарантирует отсутствие утечек памяти для любого кода, который успешно скомпилировался. Аргентум управляет временем жизни объектов и в стеке и в хипе, он делает это не с помощью явных деклараций lifetimes, а в соответсвии с принципами агрегации-компизиции-ассоциации, что гораздо ближе к предметной области приложений и привычно по UML. Аргентум сам генерирует сервисный код обслуживающий иерархии объектов. Аргентум не позволяет модифицировать шареные объекты, взламывать систему типов кастами, не имеет unsafe режима, не позволяет обращаться по null-pointer и т.д. Он более ограниченный чем Rust но более безопасный.
Он гарантирует отсутствие утечек памяти для любого кода, который успешно скомпилировался.
Гм. А разве проблема обнаружения утечки не является эквивалентной проблеме останова? Хотя тут нужно определить, что является утечкой. А то, если пихать в хешмапу объекты и ключи забывать, то формально утечки вроде нет, а фактически — есть. И говоря про гарантии утечек Rust скорее всего эту ситуацию и имеет ввиду.
Rust не умеет автоматически копировать/клонировать иерархии объектов, соединеных умными указателями
Опять же непонятно.
std::rc::Rc
реализуетClone
.std::rc::Weak
реализуетClone
.std::sync::Arc
реализуетClone
.std::sync::Weak
реализуетClone
.
Что именно недостает Rust-у для копирования? Зато к вашему примеру есть вопросы. Что будет, если focused
будет указывать не на Label
своего объекта, а соседнего? По вашему описанию, ссылка любая. Куда после копирования она будет указывать? А если владелец focused
будет не Scene
, а промежуточный объект внутри сцены и его копируют в ту же сцену?:
class Scene {
// Поле `elements` владеет объектом `Array`,
// который владеет множеством `SceneItem`s
// Это композиция
elements = Array(SceneItem);
// Поле `focusedArray` владеет объектом `Array`,
// который владеет множеством `FocusedHolder`s
// Это композиция
focusedArray = Array(FocusedHolder);
}
class FocusedHolder {
// Поле `focused` ссылается на произвольный `SceneItem`. Без владения
// Это ассоциация
focused = &SceneItem;
}
Копируем scene.focusedArray[0]
в scene.focusedArray[1]
. Куда укажет ссылка scene.focusedArray[1].focused
? Если туда же, куда scene.focusedArray[0].focused
, то почему при копировании всей сцены целиком она будет указывать в другое место (судя по вашему примеру)?
Мне кажется, сложно утверждать, что язык более безопасный, если в нем такие умолчания, которые не поймешь, как работают и не сломается ли чего после небольшого рефакторинга (перенесли focused
во вложенный объект). Причем, заметьте, судя по вашему описанию все должно компилироваться, то есть это непредсказуемое поведение рантайма.
А то, если пихать в хешмапу объекты и ключи забывать, то формально утечки вроде нет, а фактически — есть. И говоря про гарантии утечек Rust скорее всего эту ситуацию и имеет ввиду.
Непонятно, что означает фраза "ключи забывать". Определение утечки не должно зависить от внутреннего убеждения программиста о нужности или ненужности данных. Оно должно быть формальным. Все современные программные системы - основанные на ручном управлении, подсчете ссылок и сборке мусора - считают утечкой не освобожденный объект недоступный приложению по владеющим ссылкам.
При таком определении в расте есть утечки, вызванные циклическими ссылками. И архитекторы Раста перекладывают борьбу с этими утечками на плечи программиста. Подробности тут: https://doc.rust-lang.org/book/ch15-06-reference-cycles.html
Забывать — это значит, вы кладете в хешпаму что-то по ключу, но никогда не достаете из нее по этому ключу и не удаляете по этому ключу. А также никогда не обходите все значения, то есть мапа у вас служит кешем — только положить по ключу, достать по ключу и удалить по ключу.
Вот с логической точки зрения тут самая настоящая утечка — мапа пухнет, пожирая память почем зря. А с формальной никакой утечки нет — ведь внутренние структуры мапы этой памятью владеют, это внутренние структуры вашего приложения и формально оно доступ к этой памяти имеет. Только не пользуется ей. И мне кажется нет, и не может быть способа с этим бороться, это принципиально неразрешимая проблема. Никаким алгоритмами вы не определите, что то, что в мапу положили уже никогда в будущем не достанется из нее.
Опять же непонятно.
std::rc::Rc
реализуетClone
.Что именно недостает Rust-у для копирования?
Реализация std::rc::Rc
Clone
(как и все прочие из приведенного списка) не копирует иерархию объектов. Она копирует ссылку на существующий объект, нарушая инвариант UML-композиции, который требует наличия единственного вдалельца. Поэтому Rc ссылка непригодна для организации композитов - а ведь на композитах построены все модели данных в современных приложениях - HTML DOM, Compiler AST, XML-JSON, базы данных, офисные документы, файловые системы, сцены GUI и 3D приложений - это всё композиты. И копирование композитов в Расте приходится писать вручную заново для каждой структуры данных.
Так для композитов ссылки не нужны — вы просто включаете один объект в другой. Зачем тут ссылки? Это только на схеме они нужны, чтобы не мельчить в одной ограниченной области, а раскидать классы по листу.
Глубокое копирование в расте таких композитов имплементируется одной строчкой — #[derive(Clone)]
на структуре-владельце.
Проблема вообще мало понятная. Когда пишешь, нужно думать. Хоть малечко. Большие проекты. Все на C++. Один раз в жизни посмотрел на память. Ничего интересного не увидел. Обычные sbared, вкупе с UI weak , иногда. Чего сложного?
Можно пойти ещё дальше и использовать реактивное программирование для понимания какие объекты ещё нужны, а какие уже не нужны, хотя и доступны по владеющим ссылкам. Во фреймворке $mol это используется для управления жизненным циклом объектов. Там объекты получаются через ленивые владеющие фабрики. Как только этот объект где-то понадобился - он создаётся налету. Как только перестал везде использоваться - уничтожается.
Кто о чём, а ниндзя о шмали.
Есть у нас в проекте такое, вик поинтер в кэше, если объект уже сдох то пересоздаем если нет отдаем, работает нормально только либо для иммутабл объектов либо в одном потоке. И знаю я это потому, что как раз очень серьезные проблемы у нас с этим с мутабельными объектами в многопотоке. Одно спасает, случается редко и краши пока не преодолели порог что бы их фиксить.
Короче, подход рабочий, но с ограничениями, если добавить копи он райт будет почти универсальным, но не будет покрывать случая когда надо поменять поле шаредного объекта для всех пользователей объекта. Можно и это решить через внутренний лок объекта конечно, но в любом случае накладные расходы в рантайме на лок вик принтера не бесплатны, но вполне приемлемы для многих задач.
Интересно, а как оператор глубокого копирования будет копировать большой связанный клубок объектов с thread safety? Все объекты должны быть заблокированы?
Каждая композитная (изменяемая) объектная иерархия всегда принадлежит какому-то одному треду. (Хотя ее и можно целиком или по частям передавать между тредами, разбирая и снова собирая в одном треде). Агрегатные иерархии объектов шарятся между всеми тредами, но это не создает проблем, т.к. они не изменяемые.
Можно сказать, что копирование, как и любой доступ к данным в треде с точки зрения других тредов выполняется в транзакции, а с точки зрения кода треда - однопоточен. Поэтому оператор глубокого копирования легко справится с такой иерархией.
спасибо, а есть ссылка на язык? гитхаб, мейллист или просто спека?
Замечу, что в Swift-е вводят возможность создавать некопируемые структуры / enum-ы, у которых в каждый конкретный момент есть строго один владелец (можно закончить жизненный цикл оператором consume
, можно передать в другую функцию временно через borrowing
параметр или окончательно через consuming
параметр, но в любом случае в каждый момент времени владелец один). Константность и раньше была через let
. Кажется, как минимум значительная часть фич Argentum-а этим закрывается?
В аргентуме нет некопируемых структур. Единственность владения обеспечивается не запретом копирования, а удобной встроенной операцией копирования. Let в Swift-e не делает объект константным тогда как оператор заморозки Аргентума - делает.
Спасибо за фидбэк, он поможет мне написать продолжение про встроенные операции и главное, сделать акцент на отличиях от Раста и Свифта.
Если столько проблем с объектами, то не проще ли избавится от объектов вообще? И сразу же с утечками памяти проблем не будет.
![](https://habrastorage.org/getpro/habr/upload_files/6d0/2cc/65a/6d02cc65a1a6424a47d9460585831ecc.png)
Просто оставлю реализацию lifetime-менеджмента дримберда тут
root.focused? _.hide();
// Защитить объект по ассоциативной ссылке от удаления,
// и если он не пуст, вызвать его метод.
// после чего снять с объекта защиту.
А зачем защищать объект и вводить лишние синтаксические конструкции?
Если компилятор языка знает, что объект - это не владеющая ссылка (ассоциация), то что мешает делать захват объекта и его проверку без участия пользователя, т.е. внутри root.focused.hide();
без всяких root.focused ? _ .hide();
Если делать захват объекта и его проверку без участия пользователя, мы сэкономим два символа в исходном тексте программы, но получим несколько проблем:
Из текста будет не ясно, что метод hide может вызваться, а может нет
Программисту некуда будет добавить реакцию на оборванную ссылку (else - часть)
Если после единственной проверки на оборванность ссылки нужно будет сделать несколько взимосвязанных действий не отпуская объекта, без явного оператора "?" это будет проблематично. А сейчас это делается так:
focused ? {
scrollTo(_);
flash(_);
_.underline()
}
Кстати, "?" это не лишняя конструкция - это бинарный оператор if-then - половинка тернарного оператора if-then-else. Вот про что надо написать! Третий пост будет про управляющие конструкции на основе optional. И про то что в Аргентуме нет bool, а есть optional<void>. Спасибо за коммент.
Из текста будет не ясно, что метод hide может вызваться, а может нет
Это замечание может относится практически к любому объекту в программе, т.к. ошибки могут возникать где угодно.
Программисту некуда будет добавить реакцию на оборванную ссылку (else - часть)
Классический try ... exept ... else ?
Если после единственной проверки на оборванность ссылки нужно будет сделать несколько взимосвязанных действий не отпуская объекта, без явного оператора "?" это будет проблематично. А сейчас это делается так:
Если ? это тернарный оператор, тогда я не понял, какой оператор выполняет захват ссылки.
Осталось дело за малым, описать собственно каким образом заявленная семантика, различного рода ссылок будет осуществляться? Доступ, по не владеющей ссылки(ассоциации в вашей терминологии) не будет протухать? Когда объект доступный по "разделяемым" ссылкам все таки будет уничтожаться? И прочие не удобные вопросы. А то что управлять деревом из владеющих ссылок просто, это давно известно, вы лишь изобрели unique_ptr (да и Qt есть похожая древовидная модель владения ресурсами/виджетами)
Просто вы в своих построениях практически в точности воспроизвели, набор "умных" указателей из плюсов (unique_ptr/shared_ptr/weak_ptr), и поэтому хотелось бы понять, зачем все это нужно, тем более чтобы претендовать на статус "нового языка".
Кажется, у автора не совсем правильные представления о менеджменте памяти в современном C++. Там по сути применяется та же парадигма с наличием одного владельца. Когда надо, можно заиспользовать observer_ptr, а если и его не хватает - shared_ptr/weak_ptr. Никакого "ручного" управления памятью там давно уже нету, никто руками malloc/free new/delete не зовёт, управление памятью происходит через контейнеры стандартной библиотеки, которые это всё внутри по необходимости делают.
В языке Argentum просто это просто формализовано, хоть это и тоже достижение.
На счёт разделяемых указателей, встроенных в язык: стоит предусмотреть два вида подобных указателей - однопоточные и многопоточные. В однопоточных внутри просто счётчик ссылок, в многопоточных - атомарный счётчик ссылок плюс какой-нибудь мьютекс или rwlock для доступа к самому объекту. Такое разделение есть, к примеру, в Rust. Смысл в двух разных механизмах - использовать однопоточные типы там, где это возможно, т. к. это не так накладно, как использование многопоточных типов.
Где можно найти компилятор данного языка? Или язык пока только проектируется "на бумаге"?
Если честно, то я так и не понял, зачем специально выделять ссылки на константные объекты (аггрегация)? Ведь они вроде как повторяют поведение обычный (не владеющих) ссылок?
Агрегация - это владение, совместное (шаренное) владение. А аргентуме агрегатная ссылка может ссылаться только на неизменяемый объект. По такой ссылке можно только читать поля объекта и вызывать константные методы.
Это понял, что в аргентуме сделано именно так, но не понял для чего это было сделано.
Поэтому у меня и возник вопрос, для какой цели нужно выделять именно константные ссылки, если их функциональность полностью покрывается обычными не владеющими ссылками и не дается никаких очевидных преимуществ? Может быть я этого просто не вижу, поэтому и задал этот вопрос.
Насколько я понял:
Невладеющая ссылка никак не влияет на сборку мусора. Объект на который она ссылается может быть удален и тогда невладеющая ссылка окажется "висячей" (аналог WeakReference из Java)
Агрегатная ссылка - учитывается сборщиком мусора, объект на который ссылается агрегатная ссылка не будет удален. Но и изменять его нельзя т.к. к нему возможен доступ по агрегатным ссылкам из разных потоков.
Владеющая ссылка - учитывается сборщиком мусора и она монопольная, то есть на такой объект не может быть других владеющих (или агрегатных) ссылок.
Но так как в аргентум сборщик мусора отсутствует, то и подсчет ссылок для этих целей не актуален, поэтому мой вопрос все еще остается актуальным.
В роли сборщика мусора - компилятор. Это я по джавовской привычке так написал. Насколько я понимаю, там тот же принцип работы что и в C++ с его умными указателями: когда объект ссылки уничтожается (например по выходу из области видимости или по любой другой причине), то его деструктор уменьшает значение счетчика ссылок. Если счетчик ссылок уменьшается до нуля - память освобождается. Собственно, "сборщика мусора" как отдельной подсистемы нет, но эффект освобождения памяти без необходимости заботиться об этом со стороны программиста - аналогичен
Я понимаю как работает подсчет ссылок и сборщик мусора.
И его работа никак не отличается как для не владеющих ссылок, так и для агрегатных. А раз это работает одинаково для всех типов ссылок, поэтому мне и стало интересно, зачем выделять агрегатные ссылки в отдельную сущность.
Но так мы можем долго гадать, но судя по всему без ответа ТК мы причину не узнаем :-(
Невладеющие ссылки - могут "провиснуть", агрегатные - не могут. В этом и отличие, насколько я его понял.
Мне почему-то кажется, что автор немного перемудрил со ссылками на константные объекты. Все это имело бы смысл, если бы речь шла о совместном доступе к объектам только для чтения и для чтения/записи.
Но в этому случае сборка мусора тут не причем, как и ссылки только константные объекты. Достаточно невозможности изменить данные по такой ссылке (т.е. ссылка только для чтения), но сами данные могут быть какими угодно.
В The Argentum Book приводится такой пример:
predicate(expression()) ? use(_);
Но на практике зачастую требуется нечто другое. Например, в Python 3.8 можно написать так:
if (line := input()) != '':
use(line)
Что соответствует такой записи в C++17:
if (auto line = input(); line != "")
use(line);
Как такое сделать в Аргентум [без вспомогательной функции-предиката]?
Извиняюсь за долгую задержку. Увольнялся из Гугла, устраивался на работу в Meta. Сейчас немножко появилось время.
Обсудить можно в https://www.reddit.com/r/ArgentumLanguage/, на сайте https://aglang.org/, теперь еще и в https://github.com/karol11/argentum/discussions
Включил Discussions, спасибо за наводку.
По вашим примерам:
if (line := input()) != '':
use(line)
В этом примере input возвращает или введенную строку или магическое значение пустая строка "", если ничего не ввели. При этом теряется возможность различить не веденную строку и пустую введенную строку.
В идиоматическом Аргентуме функция input() будет возвращать ?String, чтобы различить существующую (возможно и пустую) строку и несуществующую строку. И обрабатываться она будет так:
input() ? use(_)
Или со специальной обработкой введенных пустых строк:
input() ? _ == ""
? log("Error, empty input")
: use(_)
Кстати, вспомогательная функция-предикат - это не так уж плохо, она поможет сделать код самодокументирующимся даже если input() будет кодировать отсутствие строки пустой строкой:
fn nonEmpty(s String) ?String { s!="" ? s }
...
nonEmpty(input()) ? use(_)
Если надо прямо отдельно обработать именно не пустую строку и проигнорировать пустую и без фукнций:
{
line = input();
line != "" ? use(line)
}
Или с использованием нового пайп-оператора:
input() -> _ != "" ? use(_)
Извиняюсь за долгую задержку.
А я за задержку не извиняюсь. Просто думаю долго. :)(:
В этом примере input возвращает или введенную строку или магическое значение пустая строка "", если ничего не ввели.
Постойте-постойте, мы наверное [думаем/]говорим о каком-то разном input.
Когда в программе на Python вызывается функция input()
, ожидается ввод строки от пользователя [обычно через консоль]. Пользователь вводит строку и нажимает Enter.
Если был набран только один символ до нажатия Enter, то возвращаемая функцией input()
строка будет состоять из одного символа. Если не было набрано ни одного [символа] (т.е. пользователь сразу нажал Enter) — то возвращаемая строка будет пустая. Т.е. пустая строка — это не какое-то магическое значение. Оно вообще никак особым образом не обрабатывается, а получается как бы само собой [ну, так вышло, что символов перед нажатием Enter набрано не было, либо они все были стёрты клавишей Backspace].
При этом теряется возможность различить не веденную строку и пустую введенную строку.
Тут, похоже, вы имеете в виду нечто другое: «не введённая строка» — это ошибка, возникшая во время вызова input()
?
Вообще, я считаю, что ошибки во время конкретного вызова input()
обрабатывать явно программисту не требуется в >99% случаев. Когда в коде написано password = input('Enter password:')
, программист ожидает, что после выполнения этой строки кода можно спокойно обращаться к password
как к строке и это вполне нормально. [Возможно, программист захочет проверить, что эта строка не пустая или ещё что-то, но он уж точно не ожидает, что password
может оказаться установленным в None
.]
В идиоматическом Аргентуме функция input() будет возвращать ?String
А как в «идиоматическом Аргентуме» обработать реальные ошибки, которые могут возникнуть во время вызова input()
?
В Python это решается исключениями [упоминания исключений в описании Аргентума я не нашёл]. Конкретно при выполнении input()
могут возникнуть следующие исключения:
KeyboardInterrupt
— возникает при нажатии Ctrl+C;EOFError
— возникает, если на момент вызоваinput()
уже достигнут конец файла (в случае, когда поток стандартного ввода был перенаправлен, т.е. когда ввод читается из файла, например, а не с клавиатуры);UnicodeDecodeError
— возникает, когда перенаправленный стандартный ввод содержит данные, которые не удаётся преобразовать в Unicode строку (т.е. опять же когда ввод читается из файла, т.к. при вводе с клавиатуры такого исключения возникнуть не может).
Ещё раз повторю своё утверждение "я считаю, что ошибки во время конкретного вызова input()
обрабатывать явно программисту не требуется в >99% случаев". Потому-то я и привёл в своём примере именно функцию input()
, никак не ожидая с вашей стороны дикую мысль "input() будет возвращать ?String". Но для большей ясности моего вопроса "Как такое сделать в Аргентум [без вспомогательной функции-предиката]?" сделаю два уточнения:
подставьте на место
input()
любую другую функцию, возвращающую только строку, а на место!= ""
— любую другую const-операцию со строкой;на месте
use(line)
может быть не просто одиночный вызов функции, а блок кода, причём довольно большой, поэтому использование_
тут нежелательно, и я имел в виду именно создание/объявление новой полноценной переменной (line
в ‘моём примере’/‘данном случае’).
Вообще, мой вопрос был навеян вашей фразой в The Argentum Book:
That's why the new C++17 if got its new convenient syntax:
if (auto v = expression(); predicate(v)) use (v);
...
The AG binary "?"-operator with optional operand we can achieve the same functionality without extra syntax:
predicate(extression()) ? use(_);
Т.е. вы утверждаете, что данный функционал C++17 поддерживается в Аргентуме, хотя, похоже, имеете в виду лишь частный случай if (...; predicate(v))
. Поэтому я и сделал замечание, что данная запись в C++17 зачастую требуется для другого, и привёл соответствующий код.
Если ваш язык это не поддерживает — ничего страшного (это всего лишь щепотка синтаксического сахара, и в том же C++ это появилось только в C++17 — раньше как-то программисты C++ жили без этого и ничего). Просто так и скажите об этом прямо.
"Поэтому я и сделал замечание, что данная запись в C++17 зачастую требуется для другого, и привёл соответствующий код." Для какого другого?
Если в этой вашей записи:
if (line := input()) != '':
use(line)
.. пустая строка не означает отсутствие строки, а означает именно пустую введенную человеком строку, то молча игнорировать ее будет как минимум невежливо. Она должна проверяться на соответствие какой-то грамматике с выводом осмысленных сообщений об ошибках.
Например так:
input() ->
_ == "" ? log("Error, empty input")
: use(_)
Если введенная строка обрабатывается большим куском кода, который по каким-то причнам не захотели оформить в виде фукнции, ок:
input() -> {
// большой кусок кода, использующий _
}
// или, если в большом куске кода тоже есть потребность в своих внутренних _-переменных
input() -> `name {
// большой кусок кода, использующий name
}
// или, если нет желания локализовать результат input в этом выражении:
name = input();
use(name)
....
Рантайм-исключения в Аргентуме есть. Они реализованы по-другому - с возможностью перезапуска, с обязательным включением в прототип функций и методов, очень легковесно и используя только уже существующие в языке конструкции.
В двух словах - в метод передается лямбда-параметр который вызывается при определенных проблемах и который может или решить проблему в той точке, в которой она возникла или завершить приложение или вернуть управление из любого блока внешнего лексического скоупа, безопасно размотав стек до любой точки. Это как если бы цикл for
был функцией, а тело цикла было ее лямбда-парамером и в нем бы раболал break
передающий управление за пределы цикла for
. Аргентум именно так работает и с циклами и с исключениями.
"Поэтому я и сделал замечание, что данная запись в C++17 зачастую требуется для другого, и привёл соответствующий код." Для какого другого?
Ну фича эта довольно новая, и сейчас непросто найти хорошие примеры из реальных проектов на C++, где она бы использовалась. Но предполагаемые варианты использования можно посмотреть в proposal и в статьях.
пустую введенную человеком строку, то молча игнорировать ее будет как минимум невежливо.
Не соглашусь. Всё зависит от задачи. Вам разве никогда не встречалась фраза в скобках оставьте пустым, чтобы пропустить
?
Вот примеры приглашений для ввода из скрипта установки Arch Linux:
Напишите дополнительные пакеты для установки (разделите пробелами, оставьте пустым, чтобы пропустить):
Любые дополнительные пользователи для установки (оставьте пустым, если пользователей нет):
Введите IP-адрес вашего шлюза (маршрутизатора) или оставьте пустым, если его нет:
Введите ваши DNS-серверы (через пробел, пустой - нет):
Введите пароль шифрования диска (оставьте пустым для отсутствия шифрования):
Введите имя пользователя (оставьте пустым, чтобы пропустить):
По мотивам ваших идей я попробовал совместить управление памятью, ссылками и совместным доступом.
Управление временем жизни объектов: почему это важно и почему для этого пришлось создать новый язык «Аргентум»