Skip to content

Modern type specification

Lucid modernizes how user-defined types are specified. Python spreads this work across classes, dataclasses, ABCs, protocols, mixins, descriptors, properties, constructors, metaclasses, and special methods. Lucid replaces that scattered model with two kinds of type specification: traits and classes.

Lucid separates user-defined type specification into two kinds.

Definitions are visible everywhere in the project by default; a leading _ makes one private instead — see Module-private names.

Each kind gets its own document: Traits specify obligations, reusable method bodies, or both, without owning state. Classes own concrete state, construction, and identity. The rest of this page shows why Python mixes the two together, and how they work together once they're kept apart.

Traits and inheritance

A trait and a class work together when a small required core can support rich reusable behavior on top of it. A cache only has to say how to fetch, store, and report freshness; the same declaration can build higher-level behavior directly on those obligations:

trait Cache[K, V]:
    def get(self: ~Self, key: K) -> V | none
    def put(self, key: K, value: V) -> none
    def is_fresh(self: ~Self, key: K) -> bool

    def get_or_put(self, key: K, build: () -> V) -> V:
        cached = self.get(key)
        if cached is not none and self.is_fresh(key):
            return cached
        value = build()
        self.put(key, value)
        return value

trait Sized:
    def __len__(self: ~Self) -> int

    getter empty(self) -> bool:
        return self.__len__() == 0

class MemoryCache[K: !Hashable, V](Cache[K, V], Sized):
    entries: dict[K, V] = {:}
    fresh: set[K] = {}

    def get(self: ~Self, key: K) -> V | none:
        return self.entries.get(key)

    def put(self, key: K, value: V) -> none:
        self.entries[key] = value
        self.fresh.add(key)

    def is_fresh(self: ~Self, key: K) -> bool:
        return key in self.fresh

    def __len__(self: ~Self) -> int:
        return len(self.entries)

cache = MemoryCache[str, User]()
user = cache.get_or_put("ada", def(): load_user("ada"))