Baton

Why it is shaped this way

A fragment is a lens

A fragment compiles to a typed, read-only view over one record: a reference and one accessor per declared field. Nothing is decoded to hand it to a view. The value a parent passes to a child is the fragment itself: a record reference and a context.

A fragment compiles to a typed, read-only view over one record: a reference and one accessor per declared field. Nothing is decoded to hand it to a view. The value a parent passes to a child is the fragment itself: a record reference and a context.

Why#

Every native GraphQL client before this one decoded responses into models. Apollo's generated structs wrap a dictionary and cast on every access; its Kotlin sibling parses into models, re-serializes them into records, and rebuilds the model tree on every read. Meta measured the consequence in 2015: tokenizing a response was cheap, building objects from it was the frame budget. The models also leak. A parent that holds a child's fragment as a struct can read its fields, so components grow implicit dependencies, and the generated code grows with every field merged into every type that mentions it: twenty megabytes of app size from one codegen option, in one reported case.

The opposite failure is a client that hands views the raw records. Then masking is a convention, types are strings, and the first refactor of a fragment breaks a view nobody knew depended on it.

The idea#

The lens of IssueList_repository, a fragment, reads a record of type Repository:

accessorreadsas
issuesissuesa lens of IssueConnection
  edgesedgesa lens of IssueEdge, a list, optional
    nodenodea lens of Issue, optional
      idida scalar
      issueRowthe parent's recordthe lens of IssueRow_issue
    cursorcursora scalar
  pageInfopageInfoa lens of PageInfo
    endCursorendCursora scalar, optional
    hasNextPagehasNextPagea scalar
idida scalar
Plate 1. A lens over a connection, as reported: a reference and one accessor per declared field, nested lenses for links, and nothing else.

The compiler knows every fragment and every field it selects. For each fragment it emits a struct holding a record reference (and a context when the fragment uses variables) and a one-line accessor per field. Reading character.name loads a slot. A linked field returns another lens; a plural field returns a lazy collection of lenses, identifiable by record. A spread into another fragment becomes a named accessor returning that fragment's lens, optional exactly when the spread is conditional or deferred.

Masking falls out of the shape: the lens has no accessor for a field the fragment did not declare, and the compiler is the only thing that can add one. Observation falls out of the shape too: the accessor is where a read is registered, so a view body depends on the fields it read and nothing else.

Consequences#

  • There is no model to copy, mutate, persist or keep in sync. The store is the only data.
  • Generated code for a lens is one line per field plus a few data tables. No Codable, no Hashable beyond identity, no initializers except opt-in test builders. The explicit snapshot is generated too, as a fragment's other reading: an @inline fragment compiles to a value, not a lens (the decision).
  • A lens is valid only on the main actor, where the store lives. Code that needs a value elsewhere hops there or takes an explicit snapshot.
  • Equality of lenses is identity: same record, same context. SwiftUI's diffing of a view holding a lens compares references.
  • A fragment's name appears once in Swift, as the type of the property that holds it.

Not this#

  • Decoding responses into Codable models and normalizing from the models.
  • A parent reading a child's fields through a shared struct.
  • Fragment references as protocols a generic view is constrained by.
  • Materializing a snapshot of a fragment to decide what re-renders; Observation decides that.

See The store is the UI's state for what a read registers, and The compiler decides for who emits the lens.

Spelled today#

A fragment CharacterRow_character compiles to struct CharacterRow_character: Baton.Lens, which holds its anchor and has one accessor per field it declares; an operation root compiles to CharactersScreenQuery.Data, and a linked field to a nested struct such as Data.Characters. A spread is an accessor such as characterRow that returns the child's lens, and a plural link is a Baton.List, a RandomAccessCollection of lenses. A lens is Equatable by its anchor, so a row view may be too. A view declares @Fragment("…") var character: CharacterRow_character. The anchor a lens holds, the initializer that takes one and every reader behind the accessors are @_spi(Generated), which the generated files import and an app's files do not, so another module cannot make one fragment's lens from another's. Swift does not check SPI inside the module that declares it, so in the module the lenses were generated in, Other(anchor: lens.anchor) still compiles, while the anchor's readers stay out of reach. This section may rot; the rest must not.