๐Ÿ 

multiary fields

F vs Fs โ€” "give me one" vs "give me all."

the rule in one line

F.x  โ†’  a single value (errors on 0 or >1)
Fs.x  โ†’  a set of all values ({} if none)
the trailing underscore in entity.x_ is the same as Fs.x

why this even exists

Remember: in Zef's graph model, every field is physically many-valued โ€” a potentially-empty set of outgoing edges. Whether you treat a field as "exactly one" or "possibly many" is a logical decision you make at read time, not a schema decision.

mental model

When you access a field:

entity.name โ”€โ”€โ–ถ the single value (error if not exactly 1) entity | F.name โ”€โ”€โ–ถ same as above, as a ZefOp entity.email_ โ”€โ”€โ–ถ set of all email values (may be {}) entity | Fs.email โ”€โ”€โ–ถ same as above, as a ZefOp

You decide: "do I want to assert there's one?" or "do I want to handle zero-or-many gracefully?"

the pattern this kills: None-checks

Traditional Python:

if person.email is not None:
    send_notification(person.email)

Zef:

person.email_ | map(send_notification) | to_array | run

If email_ is empty, nothing happens. If it has 1 value, one notification. If it has 5 values, 5 notifications. Same code, no branches.

the deeper point

This is the zero-one-infinity rule showing up in your code. Because the storage is already "many," and your access syntax can treat it as "many," your code handles all three cases (0, 1, N) with no conditionals.

This also means: the day your "single email" becomes "multiple emails," you don't rewrite anything. Just start using Fs where you were using F.

every combination, side by side

situationF.emailFs.email
field has 1 value'a@x'{'a@x'}
field has 2 valuesโŒ ERROR{'a@x', 'a@y'}
field is absentโŒ ERROR{}

dot-access shorthand

You don't have to use F / Fs โ€” Python attribute access works the same way:

person.name        # F.name โ€” single
person.email_      # Fs.email โ€” set

# exactly equivalent to:
person | F.name
person | Fs.email

Use the dot form when reading directly. Use F/Fs when building pipelines to pass around.

works on plain dicts too

data = {'name': 'Alice', 'tags': ['python', 'rust']}

data.name_          # {'Alice'}
data.tags_          # {['python', 'rust']}
data.missing_       # {} โ€” no KeyError

in pipelines

Here's where it really shines. Say you have a list of people with optional emails:

people = [
    ET.Person(name='Alice', email_={'alice@x', 'alice@y'}),
    ET.Person(name='Bob'),                              # no email
    ET.Person(name='Carol', email_={'carol@x'}),
]

# every email across every person, flattened
all_emails = people | map(Fs.email) | flatten | collect
# {'alice@x', 'alice@y', 'carol@x'}

# people with at least one email
emailable = people | filter(Fs.email | length > 0) | collect

writing with the underscore

On the construction side, the underscore works the same way:

alice = ET.Person(
    name='Alice',                  # single value
    email_={'a@x', 'a@y'},          # set of emails
    visited_=[ET.City(name='Berlin'),
              ET.City(name='Paris')],   # ordered list
)

mismatched cardinality

If you construct with one rule and read with another, Zef will complain:

alice = ET.Person(email_={'a@x', 'a@y'})
alice.email                   # โŒ error โ€” 2 values
alice.email_                  # โœ… {'a@x', 'a@y'}

Use the _ on both sides when the field is plural.

summary card

The four things to remember:

  1. Every Zef field is physically multi-valued (0, 1, or many edges).
  2. Access with F.x / x asserts exactly one.
  3. Access with Fs.x / x_ gives you a set.
  4. Construct with x_ when the field can be plural.

spot the bug

What's wrong with this code?

person = ET.Person(name='Alice', tags=['py', 'rust'])
person.tags | map(to_upper_case) | collect
answer

Two bugs actually. The construction uses tags=[...] (singular), so Zef thinks tags is a single value (the list itself). Should be tags_=['py', 'rust']. And the read person.tags should be person.tags_ for multi-access.

person = ET.Person(name='Alice', tags_={'py', 'rust'})
person.tags_ | map(to_upper_case) | collect

Next up: the graph data model โ€” why Zef writes nested but stores flat. โ†’