Skip to content

interlens.functional

interlens.functional

Copy-on-write functional-update support shared by Participant and Conversation.

Both are dataclasses that the user builds up incrementally (conv.turns(6).data(ds).analyzer(grade)) and replicates cheaply (one recipe → N per-row copies in a rollout). The Functional mixin gives them a single uniform update primitive — obj.set(**changes) -> new obj — plus optional per-field dot-modifier sugar (conv.turns(6)) that reads with no argument and returns a modified copy with one.

Why copy.copy + an explicit _after_set hook rather than dataclasses.replace: a live participant carries heavy shared state (the loaded _model/_tokenizer) and volatile per-conversation state (KV cache, a pending capture). We want the copy to share the heavy objects by reference (zero GPU cost — the whole point of copy-on-write here) but start with fresh volatile state. copy.copy shallow-copies the instance dict (so heavy refs are shared automatically) and, crucially, does NOT re-run __init__/__post_init__ (which for a participant would try to re-derive device from the model, and for a conversation would re-seed/re-validate), leaving us to reset exactly the volatile fields in _after_set. The result is precise control over what is shared vs. what is reset — which replace cannot express when the sharing/reset split cuts across init and non-init state.

Functional

Mixin giving a dataclass copy-on-write updates via set(**changes).

set shallow-copies the instance (heavy refs like a loaded model are shared, not reloaded), applies the changes, then calls _after_set to reset volatile per-instance state on the copy. Sugared field names (declared via sugar_fields) are accepted under their public spelling and routed to their _-prefixed storage. Unknown fields raise TypeError. The original is never mutated.

set

set(**changes)

Return a modified shallow copy: the same object with changes applied and volatile state reset. The receiver is untouched (copy-on-write); heavy state (a loaded model, an API client) is shared by reference. Unknown field names raise TypeError.

The copy is a raw instance-dict clone (NOT copy.copy), so it deliberately bypasses __getstate__ — which exists to DROP the loaded model/client on pickle (spawn boundary). A .set() clone, by contrast, is in-process and must KEEP those shared references (that is the whole point of copy-on-write here).

Source code in src/interlens/functional.py
def set(self, **changes):
	"""Return a modified shallow copy: the same object with ``changes`` applied and volatile state reset. The
	receiver is untouched (copy-on-write); heavy state (a loaded model, an API client) is shared by reference.
	Unknown field names raise ``TypeError``.

	The copy is a raw instance-dict clone (NOT ``copy.copy``), so it deliberately bypasses ``__getstate__`` —
	which exists to DROP the loaded model/client on *pickle* (spawn boundary). A ``.set()`` clone, by contrast,
	is in-process and must KEEP those shared references (that is the whole point of copy-on-write here)."""
	allowed = self._settable_names()
	new = self.__class__.__new__(self.__class__)
	new.__dict__.update(self.__dict__)
	for key, value in changes.items():
		if key not in allowed:
			raise TypeError(f"{type(self).__name__}.set() got an unexpected field {key!r}; "
			                f"settable fields are {sorted(allowed)}")
		setattr(new, f"_{key}" if key in self._SUGARED else key, value)
	new._after_set(self)
	return new

sugar_fields

sugar_fields(*names: str)

Class decorator declaring copy-on-write dot-modifier sugar for names. Each name becomes an accessor (obj.name() reads, obj.name(v) returns a modified copy) backed by the dataclass field _name. The public names are also accepted by set(**changes); the underscored storage names are hidden from it.

Source code in src/interlens/functional.py
def sugar_fields(*names: str):
	"""Class decorator declaring copy-on-write dot-modifier sugar for ``names``. Each ``name`` becomes an accessor
	(``obj.name()`` reads, ``obj.name(v)`` returns a modified copy) backed by the dataclass field ``_name``. The
	public names are also accepted by ``set(**changes)``; the underscored storage names are hidden from it."""

	def decorate(cls):
		sugared = set(getattr(cls, "_SUGARED", frozenset())) | set(names)
		cls._SUGARED = frozenset(sugared)
		for name in names:
			setattr(cls, name, _SugarAccessor(name))
		return cls

	return decorate