git clone https://github.com/plurigrid/asi
T=$(mktemp -d) && git clone --depth=1 https://github.com/plurigrid/asi "$T" && mkdir -p ~/.claude/skills && cp -r "$T/ies/music-topos/.agents/skills/tuple-nav-composition" ~/.claude/skills/plurigrid-asi-tuple-nav-composition && rm -rf "$T"
ies/music-topos/.agents/skills/tuple-nav-composition/SKILL.mdTuple Nav Composition
Core Concept
Navigation through product structures (tuples, named tuples, records) where multiple fields must be accessed and composed in parallel.
A TupleNav is a bundled Navigator that operates on all fields of a product simultaneously, maintaining structural integrity through composition.
Why Tuple Navigation?
Standard Specter navigators work on sequences (ALL, each element). But what about simultaneous access to multiple fields?
# Without TupleNav: Access fields separately, type mismatch risk person = (name="Alice", age=30, email="alice@example.com") name_nav = @late_nav([INDEX(1)]) # But tuples aren't indexed this way! age_nav = @late_nav([INDEX(2)]) # Fragile, error-prone # With TupleNav: Access structurally, type-safe composition record_nav = @tuple_nav([:name, :age]) # Access both fields as unit
Architecture
TupleNav Structure
struct TupleNav fields::Vector{Symbol} # Field names to navigate navigators::Dict{Symbol, Navigator} # Navigator for each field composition_strategy::Symbol # :parallel, :sequential, :reduce output_type::Type # (Field1Type, Field2Type, ...) end
Product Types
| Type | Navigation |
|---|---|
| Positional tuple |
| Named access |
| Struct-like |
| Key-based dictionary |
| Record/DataClass | Field-based |
Composition Strategies
- Access all fields simultaneously:parallel
nav = @tuple_nav([:x, :y, :z], strategy=:parallel) result = nav_select(nav, (x=1, y=2, z=3), id) # => ((x=1), (y=2), (z=3)) # All accessed at once
- Access fields in order, threading results:sequential
nav = @tuple_nav([:x, :y, :z], strategy=:sequential) # Field y receives result from x, z receives from y, etc.
- Fold/reduce across fields:reduce
nav = @tuple_nav([:x, :y, :z], strategy=:reduce) # Combine results across fields: x + y + z
API
TupleNav Creation
Creates a TupleNav for navigating product types.@tuple_nav(fields, strategy=:parallel)
# Access name and email from person record nav = @tuple_nav([:name, :email]) person = (name="Alice", email="alice@example.com", age=30) result = nav_select(nav, person, identity) # => (name="Alice", email="alice@example.com")
Explicitly typed TupleNav.tuple_nav(fields::Vector{Symbol}, structure_type::Type)
PersonType = NamedTuple{(:name, :age, :email)} nav = tuple_nav([:name, :email], PersonType) # Type-checked at compile time
Composition
Combines two product navigators.compose_tuple_navs(nav1::TupleNav, nav2::TupleNav) :: TupleNav
nav_identity = @tuple_nav([:id, :name]) # Extract identity nav_contact = @tuple_nav([:email, :phone]) # Extract contact nav_combined = compose_tuple_navs(nav_identity, nav_contact) # Result has all 4 fields: id, name, email, phone
Nested Products
Navigate to a product, then select fields.nested_tuple_nav(path::Vector, fields::Vector{Symbol})
# Navigate to users list, then select name/email from each nav = nested_tuple_nav([keypath("users"), ALL], [:name, :email]) data = Dict( "users" => [ (name="Alice", age=30, email="alice@example.com"), (name="Bob", age=25, email="bob@example.com") ] ) result = nav_select(nav, data, identity) # => [ # (name="Alice", email="alice@example.com"), # (name="Bob", email="bob@example.com") # ]
Field Projection
Extract subset of fields from a TupleNav.project_tuple_nav(nav::TupleNav, fields::Vector{Symbol})
nav_full = @tuple_nav([:id, :name, :age, :email]) nav_subset = project_tuple_nav(nav_full, [:name, :email]) # New navigator accesses only name and email
Structural Composition
Key insight: TupleNav maintains type safety across composition.
# Type 1: Person Person = NamedTuple{(:name, :age)} # Type 2: Contact Contact = NamedTuple{(:email, :phone)} # Type 3: User (combines both) User = NamedTuple{(:name, :age, :email, :phone)} # Composition is type-safe nav_person = @tuple_nav([:name, :age], Person) nav_contact = @tuple_nav([:email, :phone], Contact) nav_user = compose_tuple_navs(nav_person, nav_contact) # => TupleNav for User type ✓
Parallel Semantics
With
:parallel strategy, all fields are accessed in parallel (logically):
nav = @tuple_nav([:x, :y, :z], strategy=:parallel) # Execution order doesn't matter—all fields see the same input result1 = nav_select(nav, structure, f) result2 = nav_select(nav, structure, f) # => Always identical (deterministic parallel access)
Under the hood, this uses color-aware SplitMix64 seeding to ensure parallel reads remain deterministic (see
color-envelope-preserving skill).
Dictionary Projection
TupleNav also works on dictionaries with structured keys:
nav = @tuple_nav([:user_id, :user_name, :user_email]) data = Dict( :user_id => 42, :user_name => "Alice", :user_email => "alice@example.com", :user_age => 30 ) result = nav_select(nav, data, identity) # => Dict(:user_id => 42, :user_name => "Alice", :user_email => "alice@example.com")
Integration with Type Inference
TupleNav output types are automatically inferred by the type system:
nav = @tuple_nav([:x, :y, :z]) # Type: (T1, T2, T3) → (T1', T2', T3') # where T_i' is the refined type of field i sig = navigator_signature(nav) # => TypeSignature( # input: NamedTuple{(:x,:y,:z)}, # output: NamedTuple{(:x,:y,:z)}, # same structure # preserves: field types # )
Composition with Constraints
TupleNav can apply navigators to each field:
# Each field goes through its own Navigator field_navs = Dict( :x => @late_nav([pred(ispositive)]), :y => @late_nav([pred(ispositive)]), :z => @late_nav([pred(ispositive)]) ) nav = @tuple_nav([:x, :y, :z], field_navigators=field_navs) # Each field must pass its predicate
Error Handling
Missing field:
nav = @tuple_nav([:name, :email, :phone]) person = (name="Alice", email="alice@example.com") # missing :phone nav_select(nav, person, id) # => FieldMissingError("Field :phone not found in tuple")
Type mismatch:
nav = @tuple_nav([:age]) # Expects numeric field person = (name="Alice", age="thirty") # age is string nav_select(nav, person, id) # => TypeError("Expected Int, got String for field :age")
Performance
| Operation | Complexity | Notes |
|---|---|---|
| Create TupleNav | O(n) | n = number of fields |
| Parallel access | O(n) | All fields accessed once |
| Sequential access | O(n²) | Each step threads to next |
| Reduce | O(n) | Single fold over fields |
| Caching | O(1) | Cache key includes field list |
Related Skills
- type-inference-validation - Validates field types at compile time
- constraint-generalization - Combines field constraints across products
- specter-navigator-gadget - Base Navigator system that TupleNav builds on
- möbius-path-filtering - Ensures product navigation is topologically valid
Use Cases
Example 1: Extract contact info from user records
nav = @tuple_nav([:name, :email, :phone]) users = [ (id=1, name="Alice", email="alice@ex.com", phone="555-1234"), (id=2, name="Bob", email="bob@ex.com", phone="555-5678") ] contacts = map(u -> nav_select(nav, u, identity), users) # All users' contact triples extracted
Example 2: Parallel aggregation
nav = @tuple_nav([:revenue, :costs, :profit], strategy=:parallel) quarterly_data = ( revenue=100_000, costs=60_000, profit=40_000 ) result = nav_select(nav, quarterly_data, x -> x * 1.1) # 10% increase # => (revenue=110_000, costs=66_000, profit=44_000)
Example 3: Nested product navigation
nav = nested_tuple_nav( [keypath("employees"), ALL], [:name, :salary] ) company = Dict( "employees" => [ (name="Alice", salary=80_000, dept="Engineering"), (name="Bob", salary=75_000, dept="Sales") ] ) result = nav_select(nav, company, identity) # => Extract name/salary for all employees
References
- Product types: "Types and Programming Languages" - Pierce
- Cartesian logic: "Basic Intuitionistic Linear Logic" - Girard
- Parallel semantics: "Parallel Haskell" - Peyton Jones et al.