ET, UIDs, and the π prefix family.
alice = ET.Person(name='Alice', age=30)
type(alice) # Entity_
ET.Person is an entity type. You invent them on the fly: ET.Car, ET.Invoice, ET.Tamagotchi. The first time Zef sees a new type name, it assigns it a compact binary encoding.
Correct. There's no schema declaration, no field list, no constructor you
have to implement. ET.X is just "a new kind of thing" β and Zef
figures out the fields from the kwargs you pass.
This is the "zero-one-infinity rule" applied to schemas: if constraints are optional, don't force them. You can add refinement types later if you want validation.
An entity itself carries no data. Its fields are edges pointing
to other values. Think of ET.Person(...) as a little empty box
labeled "person" β the contents come from the outgoing edges.
Every entity has an identity. But how you identify it β there are three levels:
alice_a = ET.Person(name='Alice')
alice_b = ET.Person(name='Alice')
# these are two distinct entities, even though they look the same
Bare entities are "value-like" β useful when you don't care about tracking them across operations.
ET.Person(137) # "the 137th Person in this graph"
Integer indexes are graph-local and deterministic: the Nth entity of this type as counted from 0. Useful for scripting, test fixtures, bulk loads. But the number means nothing outside this one graph.
ET.Person('π-97421467198ef6d64520', name='Alice')
A UID is globally unique. π prefix + 20 hex characters. Generated randomly, guaranteed unique without coordination.
Generating a UID calls rand(), which is impure. Every UID is an
act of tagging a conceptual thing from the real world and pulling it
into your data universe. That's a modeling decision β not something you want
happening silently.
So in Zef, UID generation is explicit: either you pass one in, or you call
generate_uid() or FX.Random(tp=UID) | run.
Zef uses different prefix emojis to signal different kinds of identity. Seeing the prefix, you know what kind of thing you're dealing with:
| prefix | meaning | example |
|---|---|---|
π- | platonic UID β a global identity | π-97421467198ef6d64520 |
π§- | snapshot UID β a specific DB state at a point in time | π§-6dc2ec6a470d33ec919b-... |
πΈοΈ- | graph-local ref β position inside one graph | πΈοΈ-1-... |
πΏ- | content-hashed value β identifies by content | πΏ-abc... |
Yes, real Zef code has emojis in UIDs. You get used to it quickly. They're visual handles that make logs and error messages instantly categorizable.
# manually, if you need a specific one
ET.Person('π-97421467198ef6d64520', name='Alice')
# generated at runtime
uid = generate_uid() # 'π-...20 hex...'
ET.Person(uid, name='Alice')
# via the FX system (when you want it logged/replayable)
uid = FX.Random(tp=UID) | run
ET.Person(uid, name='Alice')
The most important syntactic rule about entities:
use bare field name
ET.Person(name='Alice')
"name is one thing"
add trailing _
ET.Person(likes_={'π', 'πΊ'})
"likes is a SET"
And for ordered many:
ET.Person(visited_=[
ET.City(name='Berlin'),
ET.City(name='Paris'),
ET.City(name='Tokyo'),
])
Square brackets (list) = ordered. Curly braces (set) = unordered.
Dropping the trailing underscore switches semantics entirely. likes
means "one like" (Zef will error if you give a set). likes_
means "the set of likes" (Zef expects a collection). Always add the underscore
when the field can have more than one value.
company = ET.Company(
'π-01abcdef01234567890a', # global identity
name='Green Widgets Inc',
founded=2020,
ceo=ET.Person(
'π-02cafebabe...000b',
name='Alice',
),
employees_=[ # ordered list
ET.Person(name='Bob', role='Engineer'),
ET.Person(name='Carol', role='Designer'),
],
tags_={'b-corp', 'startup'}, # unordered set
)
Reaching for an analogy? An entity is like a Python dict, plus:
ET.Something)When you persist it, it becomes graph nodes + edges. When you read it back, you get the same shape.
a = ET.Person('π-abc...', name='Alice')
b = ET.Person('π-abc...', name='Alice')
a == b # True β same UID, same fields
c = ET.Person('π-abc...', name='Bob')
a == c # False β same UID, different fields
a.same_entity_as(c) # True β same UID (identity match)
Equality considers both identity and fields. If you want "are these the same thing, regardless of their current state?", use same_entity_as or compare UIDs.
Define an entity ET.Book with:
ET.Person)ET.Book(
'π-deadbeefdeadbeef1234',
title='The Zef Zine',
authors_=[
ET.Person(name='Ada'),
ET.Person(name='Bea'),
],
tags_={'functional', 'python', 'zines'},
)
Next up: the critical F vs Fs distinction β one-or-many field access. β