Реализация ссылочной модели в языке программирования Аргентум

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Практический пример, сравнение с "конкурентами", формальное определение операций, особенности многопоточности, внутреннее устройство

Вводная статья про Аргентум: https://habr.com/ru/articles/749806/

  • Сайт проекта: https://aglang.org/

  • Демка для Windows: https://github.com/karol11/argentum/releases

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

Тестовый пример

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

Внешний вид наших карточем будет каким-то таким
Внешний вид наших карточем будет каким-то таким
Структура классов нашего приложения (для композиции использована нотация "включения" чайлд-объектов непосредственно во владеющий объект: Cards[], Rectangle, Point[] ... А для агрегации и ассоциации использованы привычные аналоги из C++ - weak и shared указатели).
Структура классов нашего приложения (для композиции использована нотация "включения" чайлд-объектов непосредственно во владеющий объект: Cards[], Rectangle, Point[] ... А для агрегации и ассоциации использованы привычные аналоги из C++ - weak и shared указатели).

Как мы видим, здесь присутствуют композитные, ассоциативные и агрегатные связи:

  • карточки принадлежат документу,

  • элементы принадлежат карточкам,

  • битмап принадлежит графическому блоку,

  • коннекторы связываются с произвольными блоками через анкерные точки,

  • карточки связываются с другими карточками через объекты Button,

  • текстовые блоки совместно используют стили.

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

Иерархия объектов построена из трех видов связей:

  • композиция

  • ассоциация

  • агрегация

Разберем их по порядку.

Композиция: "A владеет B"

Все современные приложения построены на  структурах данных с древовидным владением:

  • HTML/XML/Json DOM,

  • структуры реляционных и документно-ориентированных баз данных,

  • промежуточное представление кода в компиляторах,

  • сцены в 3D приложениях,

  • формы пользовательского интерфейса.

Все это то, что в UML называется композицией - владением с единственным владельцем.

Не является исключением и наш пример. Документ владеет карточками, карточки владеют элементами, которые владеют анкерными точками. Это все - композиция.

Инварианты композиции:

  • У объекта может быть ровно один владелец.

  • Объект не может быть прямым или косвенным владельцем самого себя.

  • Объект жив пока жив его владелец и владелец ссылается на него.

Было бы очень полезно если бы современные языки программирования позволяли явно декларировать такое отношение владения и проверяли инварианты композиции на этапе компиляции.

Композиция в С++/Rust/Swift

Почему не в Java/JS/Kotlin/Scala? В системах со сборщиком мусора все ссылки являются ссылками со множественным владением. То есть агрегатными, а не композитами. Поэтому поищем встроенную поддержку композиции в языках без GC.

Например, попробуем спроектировать структуру данных выше описанного приложения на C++.

Мы не можем включить объекты прямо в структуру по месту использования по трем причинам:

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

Во-вторых ассоциативные не владеющие ссылки. Они присутствуют в модели нашего приложения. Они связывают коннекторы с анкерами, и кнопки с карточками. Если представлять их в программе в виде weak_ptr, а это естественное представление ассоциаций в C++, то объекты на которые они ссылаются должны быть самостоятельными объектами а не полями в других объектах. Умные указатели в C++  вообще не любят указывать внутрь других объектов.

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

На следующей диаграмме показана эта ситуация в упрощенном виде:

  • У нас есть Card1, в котором Лежит TextBox1.

  • Где-то в нашей программе есть ссылка на текущий объект-в-фокусе.

  • Юзер нажимает кнопку del. И она отправляется сфокуссированному объекту.

  • Хендлер в текущем сфокусированном объекте просит сцену удалить все select-нутые объекты.

  • Среди них оказывается наш TextBox1.

  • Имеем русскую рулетку в виде нескольких мусорных указателей в стеке.

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

C++ предоставляет unique_ptr для случая уникального монопольного владения. Этот указатель решает проблему полиморфизма но не решает ни проблему русской рулетки, ни проблему weak_ptr, о которых говорилось выше. Объект, лежащий в unique_ptr не может быть таргетом weak_ptr. И этому есть логическое объяснение: weak_ptr может потерять ссылку в любой момент. Поэтому при его разыменовании нужно защитить объект от удаления. В C++ операция разыменования weak_ptr::lock() порождает shared_ptr. А он не совместим с unique_ptr по понятным причинам: первый удаляет объект по обнулению счетчика ссылок, тогда как второй - немедленно. Таким образом, ни weak_ptr ни shared_ptr не может использоваться на одном объекте совместно с unique_ptr.

Значит нам остается только хранить наши уникально-владеющие композитные ссылки в виде shared_ptr, который вообще-то реализует агрегацию, а не композицию, что заставляет нас признать, что композиция как понятие в стандартном C++ не поддержана.

Интересно что при переходе к Rust ситуация не меняется. Замените unique_ptr на Box, а shared_ptr на Rc. И мы получите точно такое же поведение.

Aналогично обстоят дела в Swift, ARC-strong полный аналог shared_ptr, встроенный в язык weak - это weak_ptr.

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

Композиция в Аргентуме

Вначале пара слов о синтаксисе:

a = expression;   // это определение новой переменной.
a := expression;  // это модификация существующей переменной - присваивание.
ClassName         // это создание экземпляра.

// Декларации типов требуются только в параметрах и результатах
// функций, делегатов и методов (но не лямбд).
// Во всех остальных выражениях типы выводятся автоматически.
fn myCoolFunction(param int) int { param * 5 }

// Тип переменной и тип поля класса задается инициализатором.
x = 42;  // Int64

Для предотвращения "русской рулетки" Аргентум вводит два отдельных типа ссылок:

  • Композитная ссылка. Живет в полях объектов и результатах функций. Тип декларируется как @T.

  • Временная стековая ссылка. Живет только в стеке. Декларируется как T.

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

// Примеры:

// Локальная переменная `a` ссылается на новый объект класса Image
a = Image;

// Локальная переменная `b` ссылается на тот же объект, что и `a`.
b = a;

// Локальная переменная `с` ссылается на копию объекта `a`
c = @a;

// То, ради чего все затевалось: нельзя присваивать композитной ссылке
// объект, хранящийся в стековой ссылке. Т.к. это может нарушить правила
// единственного владельца.
// Откуда тут стековая ссылка? Любое чтение композитной ссылки возвращает
// стековую ссылку.
a.resource := c.resource;   // Ошибка компиляции

// Присваивание любой ссылке отвязывает ее от старого объекта и привязывает
// к новому. При отвязывании старый объект может быть удален, если это была
// последняя ссылка.
a.resource := Bitmap;

Описанные в начале статьи инварианты композиции обеспечиваются в аргентуме автоматически:

  • У объекта может быть ровно один владелец. Владелец ссылается на объект композитной ссылкой. А ей можно присваивать только копии других объектов и новые созданные объекты. Поэтому не бывает двух композитных ссылок на один и тот же объект.

  • Объект жив, пока жив его владелец, и владелец ссылается на него. Плюс защита от "русской рулетки": объект жив, пока на него ссылается композитная ссылка или хотя бы одна стековая ссылка. Это обеспечивается счетчиком ссылок в объекте. Поскольку ссылки встроены в язык, компилятор может провести escape-анализ и убирать ненужные retain|release-операции со счетчиками. И еще, поскольку композитные ссылки работают с mutable-объектами, а mutable-объекты в Аргентуме всегда принадлежат какому-то одному потоку, этот счетчик не атомарен. Это также гарантирует, что разрушение объекта и освобождение его ресурсов произойдет в предсказуемый момент времени на правильном потоке. Часто это бывает важно.

  • Объект не может быть прямым или косвенным владельцем самого себя. При создании объекта компилятор уже видит в какое композитное поле какого объекта будет присвоен создаваемый объект. Это даёт гарантию того что владелец объекта всегда создаётся раньше объекта, и значит, объект никогда не будет владеть сам собой.

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

Ассоциации (weak pointers): "A знает о B"

Ассоциация aka связь без владения. В нашем примере в каждой кнопке есть поле target, которое ссылается на какую-то карточку. В коннекторе есть поля start и end, которые ссылаются на какие-то анкерные точки. Это всё примеры ассоциаций. Ассоциации очень распространены в моделях приложений: ссылки между UI-формами, foreign keys в базах данных, перекрестные ссылки между контроллерами, моделью данных и представлениями в MVC, делегаты и поддписчики на события, везде где в файловых форматах появляются атрибуты id и ref - это все ассоциации.

Ассоциации могут формировать произвольные графы, в том числе содержащие циклы.
Объекты, находящиеся по обе стороны ассоциации должны иметь владельцев в своих иерархиях.

Как и в случае композиции, ассоциативная связь имеет инварианты:

  • И ссылающийся объект и таргет-объект могут иметь независимые друг от друга времена жизни.

  • Связь разрывается при удалении любого из двух объектов.

Ассоциациативные ссылки в С++/Rust/Swift

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

Кстати, все перечисленные языки позволяют обратиться по ассоциативной ссылке без проверки на потерянность объекта.

Как ассоциация представлена в Аргентуме

Ассоциативные ссылки играют большую роль во встроенных операциях копирования иерархий объектов (ей посвящен отдельный раздел) и в многопоточных операциях (о которых будет отдельный пост), в прочих сценариях ассоциативные ссылки Аргентума почти идентичны weak-ссылкам C++/Rust/Swift двумя отличиями:

  • таргетом может быть любой объект, даже хранящийся в композитной ссылке,

  • обращение по ссылке невозможно без проверки на наличие таргета.

// В Аргентуме ассоциативная ссылка на класс T декларируется как &T.

// Функция принимающая ассоциативную ссылку на CardItem
// и возвращающая такую же ассоциативную ссылку на CardItem:
fn myCoolFunction(parameter &CardItem) &CardItem { parameter }

// В Аргентуме есть &-оператор, который создает &-ссылку на объект
a = TextBlock;  // a - Владеющая ссылка
w = &a;   // `w` - это &-ссылка на `a`

// `x` не привязанная ни к чему ссылка:
x = &TextBlock;

// Теперь `x` и `w` ссылаются на один и тот же объект:
x := w;

// &-ссылка может присутствовать в полях, переменных,
// параметрах, результатах и временных значениях.
class C {
   // `field` поле-ссылка на `C`, изначально не привязанная ни к какому объекту
   field = &C;
}

&-ссылка может потерять свой таргет в любой момент в результате любой операции которая удаляет объекты. Поэтому перед использованием &-ссылку надо заблокировать и проверить на наличие таргета. Эта операция порождает стековую ссылку. Но поскольку всегда существует вероятность, что таргет был потерян или ссылка изначально не указывала ни на какой объект, а стековые ссылки не-nullable, то результат такой конверсии - это всегда optional<стековая_ссылка>.

Краткое отступление про optional тип данных в Аргентуме:

  • Для любого типа (не только ссылок) может существовать optional-обертка

  • Для типа T, optional тип будет ?T. Например ?int, ?@Card, ?Connector, и т.д.

  • Переменная типа?T может содержать или "ничего" или значение типа T.

  • Бинарная операция A ? B работает с optionals. Она требует, чтобы операнд A имел тип ?T, а операнд B был преобразованием T->X. Результат операции - ?X. Бинарная операция A ? B работает как оператор if:

    • Вычисляется операнд A,

    • Если в нем "ничего", то результатом всей операции будет "ничего" типа ?X

    • Если в нем значение T, то оно вытаскивается из optional, связывается с именем _ и выполняется операнд B, чей результат упаковывается в ?X

Приведенная информация минимально-достаточна для иллюстрации работы &-ссылок. Остальное описание типа optional будет в следующем посте.

В Аргентуме &T-ссылка автоматически конвертируется в ?T (в optional стековую ссылку) всякий раз когда ожидается значение типа optional. Поэтому операция "?" примененная к &-ссылке автоматически выполняет блокировку таргета ссылки от удаления, проверку результата на "не потерянность" и исполнение кода при успехе:

// Если переменная `weak` (которая является &-ссылкой) не пустая,
// присвоить в поле объекта, на который она ссылается строку "Hello".
weak ? _.text := "Hello";

Поскольку переменная "_" существует все время исполнения правого операнда, результат разыменования &-ссылки будет защищен от удаления в течение всего времени его работы:

weak ? {
    _.myMethod();
    handleMyData(_);
    log(_.text)
};

Поскольку результат проверки виден только внутри правого операнда операции "?", в Аргентуме отсутствует синтаксическая возможность обратиться без проверки:

  • к внутреннему содержимому optional,

  • к null-ссылке,

  • к потенциально потерянной &-ссылке

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

Все это делает Аргентум немного занудным, но экстремально безопасным языком.

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

Краткий итог раздела:

  • Аргентум имеет встроенную поддержку ассоциативных ссылок.

  • В отличие от C++/Rust/Swift, таргеты &-ссылок могут оставаться композитами, т.к. защита объекта после разыменования выполняется стековой ссылкой, а не shared_ptr/arc/Rc.

  • Разыменование &-ссылки совмещено с проверкой на наличие таргета, поэтому разыменование без проверки невозможно чисто синтаксически, что делает эту операцию безопасной.

  • Если ссылка на объект не покидает границы потока, она не использует примитивы синхронизации.

  • Операции над &-ссылками имеют легковесный и очевидный синтаксис.

Агрегация: один объект, много владельцев

В нашем примере множество TextBox-объектов может ссылаться на один и тот же Style-объект. И каждый Style-объект жив, пока на него кто-то ссылается. В реальных приложениях на удивление мало сценариев, когда объекты могут безопасно шариться подобным образом. Коллективная мудрость сообщества программистов давно объявила шаринг изменяемых объектов анти-паттерном. Все примеры безопасного совместного владения сводятся к максиме "shared XOR mutable".

Например, можно безопасно ссылаться на один и тот же String-объект в Java. Или использовать одни и те же текстурные ресурсы в разных объектах 3D-сцены. Поскольку эти объекты не изменяемые.

Инварианты агрегации:

  • Все ссылки на шареный объект одинаковые и равноправные.

  • Шареный объект жив, пока на него ссылается хотя бы одна агрегатная ссылка.

Если дополнить этот список инвариантов правилом неизменяемости таргет-объекта, то появится одна важная гарантия: объект не может прямо или косвенно ссылаться на себя. Т.к. получить агрегатную ссылку можно только на неизменяемый объект, а присвоить ссылку не изменяемому объекту невозможно.

Как существующие мейнстрим-языки поддерживают агрегацию

Все современные языки полностью поддерживают агрегацию в неограниченном виде, когда шареный объект может быть изменяемым.

Это вызывает и трудно отлавливаемые проблемы в бизнес-логике приложения, и утечки памяти и состояние гонок в многопоточных средах. Исключением является Rust, который заставляет шареный объект быть неизменяемым, хоть и позволяет взламывать эту неизменяемость использованием Cell-обертки, что, учитывая его позицирование как языка системного уровня, полностью оправдано.

Как агрегация реализована в Aргентуме

В Аргентуме агрегатная ссылка может ссылаться только на неизменяемый объект. Более того, неизменяемость и шареность - это одно и то же понятие.

Агрегатная ссылка на класс T декларируется как *T (Читается frozen T).

В аргентуме есть оператор заморозки, который принимает стековую ссылку T на изменяемый объект и возвращает агрегатную шареную ссылку *T на неизменяемый объект. Оператор заморозки имеет синтаксис: *expression. Если есть возможность, объект замораживается на месте, но если на объект есть какие-то ссылки снаружи, то делается и замораживается копия объекта.

У замороженного объекта нельзя изменять значения полей и нельзя вызывать изменяющие методы. Методы в Аргентуме делятся на:

  • Изменяющие, которые можно вызвать только у изменяемых объектов,

  • Не изменяющие, которые можно вызывать у любых объектов,

  • Шареные - которые можно вызвать только у замороженных объектов, в которых this гарантированно является агрегатной ссылкой.

Пример неизменяемости:

a = Style;
a.size := 14;
f = *a;       // `f` имеет тип *Style и ссылается на замороженную копию `a`.
x = f.size;   // Можно обращаться к полям замороженных объектов.
f.size := 16; // Ошибка компиляции. Попытка изменить замороженный объект.

Пример шаринга:

s = *Style.setSize(18);  // Тут мы создаем Style-объект, заполняем его и замораживаем
t = TextBox.setStyle(s); // Создаем TextBox и добавляем в него ссылку на `s`.

// Это можно было сделать проще:
t = TextBox
    .setStyle(*Style.setSize(18));

// Второй  textBox будет ссылаться на тот же Style.
t1 = TextBox.setStyle(t.style);
Два TextBox-объекта разделяют общий Style.
Два TextBox-объекта разделяют общий Style.

Замороженный объект не может быть разморожен, но его можно скопировать уже знакомым оператором "@" и эта копия будет изменяемой.

s = @t1.style;  // `s` - изменяемая копия существующего стиля 
s.size := 24;   // изменяем size
t1.style := *s;  // замораживаем и сохраняем в t1.
Теперь каждый TextBox ссылается на свой Style.
Теперь каждый TextBox ссылается на свой Style.

Внутренняя реализация *T-ссылок в Аргентуме использует счетчик с дополнительным флагом многопоточности. Этот флаг позволяет шареным объектам живущим в одном потоке обходиться без атомарных операций и примитивов синхронизации.

Полный запрет на изменение шареных объектов в Аргентуме имеет три важных следствия:

  • Отсутствие закольцовок гарантировано, поэтому для управления временем жизни агрегатов достаточно простого ARC. GC не нужен.

  • Любой поток, имеющий агрегатную ссылку имеет 100% гарантию, что все объекты, доступные по этой ссылке и всем исходящим ссылкам продолжают быть доступными этому потоку независимо от действий других потоков. Это позволяет существенно сократить необходимость в retain/release операциях над счетчиками.

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

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

Неизменяемость шареных объектов - это требование best practices, направленное на улучшение надежности бизнес логики приложений, исключение гонок и ликвидации неопределенного поведения. А получившееся в результате кардинальное ускорение работы со счетчиками и исключение GC - это приятный бонус, а не самоцель.
Краткий итог раздела: Аргентум следует максиме "shared XOR mutable". И хотя каждый программист может сказать: "А я нарушал это неписанное правило несколько раз и до сих пор жив",- на больших масштабах приложений и длинных дистанциях эксплуатации и поддержки приложений следование этой максиме является выгодной стратегией.

Автоматическое копирование иерархий объектов

Оператор глубокого копирования объектов строго говоря не имеет отношения к ссылочной модели Аргентума. Можно было бы вместо этого:

  • запретить присваивание композитной ссылке чего либо кроме нового инстанса объекта,

  • или заставить программистов реализовывать трейт Clone вручную,

  • или скопировать поддерево, но ассоциации и агрегации скопировать по-простому - в виде значений указателей, как в Rust-e.

Без полноценной автоматической операций копирования объектная система Аргентума все равно бы была полной, безопасной и гарантированно защищенной от утечек памяти. Но не удобной. Некоторые операции или стал бы невозможными или были бы сопряжены с большим количеством рукописного кода. Список операции, зависящих от глубокого копирования небольшой но очень важный:

  • преобразование стековой ссылки T в композитную @T,

  • заморозка объекта, если на объект еще кто-то ссылается, и он не может быть заморожен на месте,

  • разморозка объекта.

Операция копирования строится на следующих принципах (в порядке убывания важности):

  1. Не должны нарушаться инварианты ссылочночной модели.

  2. Должны поддерживаться non-null ссылки (то есть если в оригинальном объекте поле не-null, в копии оно не может становиться null).

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

  4. Данные не должны теряться.

  5. Копирование должно работать в многопоточном окружении.

  6. Оверхед операции копирования по времени и памяти должен быть минимальным.

Первый и второй принципы требуют, чтобы при копировании корня дерева объектов было скопировано все поддерево по композитным ссылкам.

Копирование агрегатной ссылки может выполняться простым копированием значения ссылки, с тем, чтобы оригинал и копия совместно шарили общий неизменяемый под-объект. В конце концов это и является смыслом агрегации.

С ассоциативными ссылками есть два случая:

Случай 1. Если копируемая ссылка ссылается за пределы копируемой иерархии объектов, единственное значение, которое она может иметь - это значение оригинала. Поэтому копия такой ссылки будет ссылаться на оригинальный объект.

Рассмотрим пример: скопируем карточку, которая ссылается на другую карточку:

doc.cards[0] ? doc.cards.append(@_);
// Если документ содержит card[0], скопировать _его_ в конец списка cards
Копируемое поддерево изображено черным цветом.Результат копирования - синим, внешняя ссылка, красным.
Копируемое поддерево изображено черным цветом.
Результат копирования - синим, внешняя ссылка, красным.

Легко заметить, что это ожидаемое поведение. Именно так бы работал код копирования, написанный программистом.

Случай 2. Если копируемая ссылка ссылается на объект, который участвует в той же операции копирования, значит эта ссылка относится к внутренней топологии объекта у нас есть выбор, ссылаться ей на оригинальный объект или на копию. Если она ссылается на оригинал, то мы теряем информацию о внутренней топологии, а мы договорились не терять информацию. Поэтому внутренние ссылки копируются так, чтобы сохранялась топология оригинала. Рассмотрим пример копирования объектов имеющих внутренние перекрестные ссылки:

doc.cards[1] ? doc.cards.append(@_);
Копируемое поддерево изображено черным цветом.результат копирования - синим,внутренние ссылки, сохраняющие топологию - красным.
Копируемое поддерево изображено черным цветом.
результат копирования - синим,
внутренние ссылки, сохраняющие топологию - красным.

Такое копирование ссылок это также ожидаемое поведение. Именно так бы работал написанный вручную код копирования.

Такая операция копирования универсальна. Точно такие же принципы применяются:

  • При обработке моделей документов, как в приведенном выше примере.

  • При преобразованиях AST в компиляторах, например если нужно сделать inline функции или инстанцирование шаблона, мы копируем поддерево этой функции с сохранением всех внутренних связей между узлами (например ссылок на локальные переменные и параметры).

  • В 3D движках этот паттерн используется для изготовления префабов, и операция копирования там должна выполняется точно по таким же правилам.

  • В паттерне проектирование "прототип". Например при разработке графического пользовательского пользовательского интерфейса мы можем создать элемент списка состоящий из иконок, текста, checkbox-а, хитрым образом связанных друг с другом ссылками и с прикрепленными хендлерами обеспечивающими нужное поведение, и потом копировать и вставлять этот элемент в контролы списков для каждого элемента модели данных. Именно такой алгоритм копирования обеспечит ожидаемые внутренние связи и ожидаемое поведение.

Сложно найти сценарий, в котором такая операция копирования окажется не применимой. 

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

Если объекты управляют какими-то системными ресурсами, для них можно определить специальные функции afterCopy и dispose, которые будут вызываться при копировании и удалении объектов. Они вызываются в тот момент, когда иерархии объектов находятся уже/еще в валидном состоянии. Они могут использоваться для закрытия файлов, освобождения ресурсов, копирования хендлов, сброса кешей и прочих действий по управлению системными ресурсами.

Краткий итог раздела: Автоматизированная операция копирования - как автоматическая трансмиссия в машине. Ее можно ругать, но жизнь она упрощает изрядно.

Влияние многопоточности на ссылочную модель

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

Потоковая модель Аргентума напоминает модель web-workers, в которой потоки - это такие легковесные процессы, которые живут в общем адресном пространстве и имеют общий доступ ко всем неизменяемым объектам приложения по агрегатным ссылкам (если у них есть ссылка). Кроме того, у каждого потока есть собственная иерархия изменяемых объектов. Примеры потоков с собственным состоянием: поток графической сцены, поток http клиента, поток документной модели. Конечно могут существовать и простые потоки-воркеры без состояния, которые просто молотят задачи, оперируя объектами лежащими в парметрах задач.

Каждый поток имеет входящую очередь асинхронных задач. Задача - состоит из:

  • ассоциативной ссылки &T на объект-получатель задачи, это один из объектов, живущих в этом протоке,

  • функции, которую надо исполнить,

  • и списка параметров, которые надо передать этой функции.

Таким образом, задачи исполняются в потоках, но посылаются объектам, хранящимся в этих потоках. Чтобы послать задачу, коду не нужно знать ничего про потоки и очереди, ему нужно иметь ассоциативную ссылку на объект-получатель.

Роль и поведение ссылок в многопоточном окружении

  • Стековые ссылки T и композитные ссылки @T - это локальные внутрипоточные ссылки, не видимые за пределами своего потока. Они всегда показывают на объект своего потока. Они могут быть переданы в качестве параметров задач в другой поток. При этом они передают и переподчиняют свой таргет-объект другому потоку. Реальная посылка задачи в очередь происходит, когда в текущем потоке не остается ссылок на объекты этой задачи. Это гарантия того, что изменяемый объект не будет одновременно доступен двум потокам.

  • Агрегатные ссылки *T могут свободно шарится между потоками, объект по ссылке всегда доступен любому количеству любых потоков.

  • Ассоциативные ссылки &T позволяют хранить связи между объектами разных потоков. При попытке синхронного обращения к объекту другого потока, &-ссылка возвращает null. Но зато &-ссылки позволяют асинхронно посылать задачи своим таргет-объектам независимо от того, в каком потоке эти объекты находятся. Таким образом ассоциативная ссылка в многопоточном окружении играет роль универсального локатора объекта.

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

Заключение

Ссылочная модель Аргентума позволяет ему избегать data races, утечек памяти, обеспечивает memory safety, null safety на уровне синтаксиса. Встроенные типы ссылок позволяют строить модели данных и иерархии объектов, которые сами поддерживают свою структуру, проверяя инварианты владения на этапе компиляции. Синтаксис операций над ссылками краток и мнемоничен.

В следующей части будет описан control flow, построенный на базе optional-типа данных. Он немного необычен но очень практичен.

Источник: https://habr.com/ru/articles/751630/


Интересные статьи

Интересные статьи

Всем привет! Меня зовут Евгений Торчинский, практически всю свою жизнь я работаю с технологиями. Сейчас я руковожу Movix Lab — мы уже много лет занимаемся железом, софтом и работаем с искусственн...
Рим пал. Но культура его живет. Люди до сих пор любуются величественным Колизеем, читают истории про борьбу галлов, цитируют Цезаря. Но, к сожалению, в Старом Свете совсем перестали говорить на латинс...
В DataStax работают над созданием производительной модели данных для Apache Cassandra. В чём заключается эта работа и как её делать правильно, на конференции Cassandra Day Russia 2021 рас...
Продолжаем рассказ о создании мульти-парадигменного языка программирования, сочетающего декларативный стиль с объектно-ориентированным и функциональным, который был бы удобен при работе с...
В этой статье описывается библиотека optlib, предназначенная для решения задач глобальной оптимизации на языке Rust. На момент написания этой статьи в этой библиотеке реализован генетический алго...