Tagged records (and variants)

I'd be a bit worried about putting private fields in R/T. As mentioned on github, all this can be done explicitly in user land by wrapping the target in a record, and including an opaque identity carrying primitive (Tag) to identify the brand.

A Tag is basically like a symbol which can be used to convey identity and be used in Weak collections:

function Tag(description) {
  const tagString = `Tag(${description})`;
  const tagObject = Object.assign(Object.create(null), {
    description: description.valueOf(),
    toString: Object.freeze(() => tagString),
    valueOf: Object.freeze(() => tagBox),
  });
  Object.defineProperty(tagObject, "toString", { enumerable: false });
  Object.defineProperty(tagObject, "valueOf", { enumerable: false });
  Object.freeze(tagObject);
  const tagBox = Box(tagObject);
  return tagBox;
}

Regarding tagging, there are 2 use cases I see:

  • The tagged data should be visible and participate in the structure checks of R/T that may contain it. In that case the tagging primitive would be a Boxed unique object, and the "public marker" would keep track of these tagged records:
class PublicMarker() {
  #marked = new WeakSet();
  #tag;

  constructor(description) {
    this.#tag = new Tag(description);
  }

  get mark() {
    return this.#tag;
  }
  
  wrap(record) {
    const marked = #{mark: this.#tag, wrapped: record};
    this.#marked.add(marked);
    return marked;
  }

  unwrap(marked) {
    if (!this.isWrapped(marked)) throw new TypeError();
    return marked.wrapped;
  }

  isWrapped(record) {
    return record.mark === this.#tag && this.#marked.has(record);
  }
}
  • The tagged data should be opaque and privately wrapped so that only the wrapper can unwrap it. The wrapped data would be contained in a Box to exclude it from participating in sameStructure checks.
class PrivateMarker {
  #Wrapper = new WrapperRegistry();
  #tag;

  constructor(description) {
    this.#tag = new Tag(description);
  }

  get mark() {
    return this.#tag;
  }
  
  wrap(record) {
    const wrapped = Box(this.#Wrapper.wrap(value));
    return #{mark: this.#tag, wrapped};
  }

  unwrap(marked) {
    if (!this.isWrapped(marked)) throw new TypeError();
    return this.#Wrapper.unwrap(Box.unbox(marked.wrapped));
  }

  isWrapped(record) {
    try {
      const correctShape =
        Record.isRecord(marked) &&
        #[...Object.keys(marked)] === #['mark', 'wrapped'] &&
        marked.mark === this.#tag;

      return correctShape && Box.unbox(marked.wrapped) instanceof this.#Wrapper;
    } catch {
      return false;
    }
  }
}

The WrapperRegistry would be this one: Wrap any value into a registered object ยท GitHub

1 Like