Ownership и Traits в JS (вдохновлено Rust)
Концепции Rust на JS: Box-владение (using + Symbol.dispose), move вместо clone, трейты через implement (без наследования и mixin). WeakMap для автоочистки. Symbol.for-подобный реестр трейтов.
Box: владение значением
class Box {
#value;
constructor(value) { this.#value = value; }
get { return this.#value; }
move { const v = this.#value; this.#value = null; return v; }
[Symbol.dispose] { this.#value = null; }
}
get— распаковывает (значение остаётся в box)move— распаковывает и опустошает (передача владения)[Symbol.dispose]— опустошает (вызывается автоматически при выходе из using-блока)
using и автоматический dispose
{
using b = new Box(point);
// b alive в этом блоке
}
// здесь b уже [Symbol.dispose] вызван
Когда заканчивается контекст, у объекта box вызывается dispose, и он забывает о всем своем внутреннем содержании. Если хотим сохранить — нужно явно
point.getи переложить.
point.get переносит состояние
function createPoint() {
using b = new Box(internalPoint);
return new Box(b.get); // создаём новый Box, состояние перенесли
}
Если не хотим, чтобы box опустошился, нужно явно взять из него состояние и переложить в другой контейнер.
Трейты — контракты на чём угодно
Трейты — это что-то типа методов, типа интерфейсов, которые можно цеплять к чему угодно. Вы отдельно их создаёте, такие контракты.
const Clonable = Trait.for('Clonable');
const Movable = Trait.for('Movable');
const Serializable = Trait.for('Serializable');
Clonable.implement(point, () => new Point(point.x, point.y));
Movable.implement(point, (target, dx, dy) => target.move(dx, dy));
implement, не миксины
Мы вместо того, чтобы написать self.clone() = function {...} (примесью), хотим контракт при помощи implement добавить. Уход от mixins.
Трейт хранит реализации в WeakMap:
class Trait {
#implementations = new WeakMap();
implement(target, callable) {
if (typeof target !== 'object') throw new Error('target must be object');
if (typeof callable !== 'function') throw new Error('callable must be function');
this.#implementations.set(target, callable);
}
invoke(boxed, ...args) {
const target = boxed.get;
const callable = this.#implementations.get(target);
if (!callable) throw new Error('not implemented');
return callable(target, ...args);
}
}
WeakMap для автоочистки
Сама приватная коллекция implementations — WeakMap. Если кто-то добавил значение, а потом у человека пропала ссылка на target — из коллекции оно стёрлось само.
Реализации трейта живут пока жив target. Это решает проблему утечек памяти при динамическом расширении.
Реестр трейтов через Symbol.for-подобный механизм
const Clonable = Trait.for('Clonable');
// в другом модуле:
const same = Trait.for('Clonable');
// same === Clonable
Что-то типа Multitone. Очень похоже на Symbol.for. Tradeoff — глобальное состояние, плюс — единый трейт между модулями.
invoke: распаковка + диспетчеризация
invoke(boxed, ...args) {
const target = boxed.get;
const callable = this.#implementations.get(target);
if (!callable) throw new Error();
return callable(target, ...args);
}
Вход — boxed-значение, выход — результат реализации трейта для конкретного target.
Чего нет: проверка сигнатур
В Rust трейты ещё типизируются. Тут — нет. Я пока не придумал, как это сделать красиво. Здесь решаются две задачи — владение данными и диспетчеризация методов.
В TypeScript можно добавить интерфейсы для каждого трейта.
Зачем это в JS
- Ownership: явный контроль времени жизни ресурса (соединения, файлы, lock-и)
- Traits без наследования: behavior-composition без mixin-проблем (collision, this binding)
- Pattern matching на типах: разные реализации одного трейта для разных типов
Связанные темы
- using, Symbol.dispose
- Композиция функций (pipe, compose)
- Functors, Monads и Applicatives
- WeakRef и FinalizationRegistry
Ресурсы
- Лекция: lnUfBHQAxw4