-
-
Notifications
You must be signed in to change notification settings - Fork 1
203. Collections and Groups
F-Mesh provides two fundamental abstractions for working with multiple entities: Collections and Groups. These patterns are used consistently throughout the framework for signals, components, and ports, providing a unified API for managing sets of entities.
Understanding the distinction between Collections and Groups is key to working effectively with F-Mesh:
- Collection - Indexed structure where entities are accessed by name (map-based)
- Group - Ordered structure where entities are accessed by iteration (slice-based)
Both abstractions support fluent APIs with powerful filtering, mapping, and iteration capabilities.
Collections are indexed structures where each entity has a unique name. They're ideal when you need to access specific entities by their identifiers.
- Map-based - Internally uses a map for storage
- Named access - Retrieve entities by name
- Unique names - Cannot contain multiple entities with the same name
- Unordered - No guaranteed iteration order
| Type | Description |
|---|---|
component.Collection |
Set of components accessed by name |
port.Collection |
Set of ports accessed by name |
// Get component by name (returns nil if not found)
c := fm.Components().ByName("processor")
// Get port by name (returns nil if not found)
p := c.Inputs().ByName("input1")
// Get multiple ports by names
ports := c.Inputs().ByNames("in1", "in2", "in3")
// Check if collection is empty
if fm.Components().IsEmpty() {
// No components
}
// Get collection size
count := fm.Components().Len()// Filter components by predicate
activeComponents := fm.Components().Filter(func(c *component.Component) bool {
return c.Labels().ValueIs("status", "active")
})
// Find first component matching predicate
first := fm.Components().FindAny(func(c *component.Component) bool {
return c.Labels().Has("critical")
})
// Check if any component matches
hasActive := fm.Components().AnyMatch(func(c *component.Component) bool {
return c.Labels().ValueIs("status", "active")
})
// Check if all components match
allActive := fm.Components().Every(func(c *component.Component) bool {
return c.Labels().ValueIs("status", "active")
})Note:
Any()with no arguments returns an arbitrary component from the collection (or nil if empty). To test a predicate, useAnyMatch().
// Iterate over all components
err := fm.Components().ForEach(func(c *component.Component) error {
fmt.Printf("Component: %s\n", c.Name())
return nil
})
// Map/transform collection (returns error if the mapper produces an invalid entry)
transformed, err := fm.Components().Map(func(c *component.Component) *component.Component {
c.AddLabel("processed", "true")
return c
})// Add components to collection (returns error on duplicate)
err := fm.Components().Add(c1, c2, c3)
// Remove components by name
fm.Components().Without("old-component", "deprecated")
// Create new collection with filtered entities
active := fm.Components().Filter(func(c *component.Component) bool {
return c.Labels().ValueIs("status", "active")
})Groups are ordered structures that maintain a sequence of entities. They're ideal when you need to work with multiple entities without caring about their individual names.
- Slice-based - Internally uses a slice for storage
- Sequential access - Access by iteration or index
- Ordered - Maintains insertion order
- Can contain duplicates - No uniqueness constraint
| Type | Description |
|---|---|
signal.Group |
Ordered set of signals |
port.Group |
Ordered set of ports |
// Get all signals from a port
signals := port.Signals().All()
// Get first signal (nil if empty)
first := port.Signals().First()
// Get last signal (nil if empty)
last := port.Signals().Last()
// Get signal at index (via All slice)
signals := port.Signals().All()
if len(signals) > 0 {
sig := signals[0]
}
// Find first matching signal
found := port.Signals().Find(signal.LabelEquals("priority", "high"))
// Check if group is empty
if port.Signals().IsEmpty() {
// No signals
}
// Get group size
count := port.Signals().Len()// Create a group from multiple payloads
group := signal.NewGroup(1, "hello", 3.14, true)
// Add signals to existing group
_ = port.PutSignals(sig1, sig2, sig3)// Filter signals by predicate
highPriority := port.Signals().Filter(signal.LabelEquals("priority", "high"))
// Check if any signal matches
hasUrgent := port.Signals().Any(signal.HasLabel("urgent"))
// Check if all signals match
allProcessed := port.Signals().Every(signal.HasLabel("processed"))There are two main approaches for iterating over signals:
// Approach 1: Using ForEach() — recommended
_, err := port.Signals().ForEach(func(s *signal.Signal) error {
fmt.Printf("Signal payload: %v\n", s.PayloadOrNil())
return nil
})
// Approach 2: Using All() — when you need the slice
for _, sig := range port.Signals().All() {
fmt.Printf("Signal payload: %v\n", sig.PayloadOrNil())
}Recommendation: Use ForEach() when iterating — it stops on the first error. Use All() when you need the actual slice.
// Map/transform signals
doubled := port.Signals().Map(func(s *signal.Signal) *signal.Signal {
if num, ok := s.PayloadOrNil().(int); ok {
return signal.New(num * 2)
}
return s
})
// Map only matching signals
doubledInts := port.Signals().MapIf(
func(s *signal.Signal) bool { _, ok := s.PayloadOrNil().(int); return ok },
func(s *signal.Signal) *signal.Signal {
return signal.New(s.PayloadOrNil().(int) * 2)
},
)// Get first payload
payload := port.Signals().FirstPayloadOrNil()
// Get first payload with default
payload := port.Signals().FirstPayloadOrDefault(0)
// Get all payloads
payloads, err := port.Signals().AllPayloads()Every group and collection carries its own labels and scalars, plus batch operations on its contents:
| Tier | Methods | Description |
|---|---|---|
| Entity's own |
WithLabel, WithScalar
|
Metadata on the group/collection itself |
| Batch on contents |
WithLabelOnEach, WithScalarOnEach, RemoveLabelOnEach, RemoveScalarOnEach
|
Metadata on each contained element |
| Cross-entity aggregation |
MinScalar, MaxScalar, AvgScalar, SumScalar
|
Aggregate scalars across elements (signal.Group only) |
For signal.Group (copy-on-write), batch methods return a new group. For other types, they mutate in place.
port.Collection supports operating on all ports at once:
// Check readiness across ports
this.Inputs().AllHaveSignals()
this.Inputs().AnyHasSignals()
// Put signals on every port in the collection
_ = c.Inputs().PutSignals(signal.New(1))
// Flush all output ports
_ = c.Outputs().Flush()
// Pipe all outputs to the same destination
_ = c.Outputs().PipeTo(destPort)
// Aggregate all signals from all ports into one group
allSignals := c.Inputs().Signals()WithParentComponent links ports back to their owning component. (Indexed ports are created on components via WithIndexedInputs / AddIndexedInputs — see Component.)
Methods that can fail return error directly. Lookup methods return nil when an entity is not found.
// ByName returns nil if not found
c := fm.Components().ByName("missing")
if c == nil {
return fmt.Errorf("component not found")
}
// ForEach stops on first error
err := fm.Components().ForEach(func(c *component.Component) error {
return c.OutputByName("out").PutSignals(signal.New("done"))
})
if err != nil {
return fmt.Errorf("operation failed: %w", err)
}
// Add returns error on duplicate
if err := fm.Components().Add(c); err != nil {
return err
}// Component not found — nil check
c := fm.ComponentByName("missing")
if c == nil { /* handle */ }
// Port not found — nil check
p := c.InputByName("missing")
if p == nil { /* handle */ }
// Empty group — First() returns nil
first := port.Signals().First()
if first == nil { /* no signals */ }component.WithActivationFunc(func(this *component.Component) error {
if !this.Inputs().ByNames("in1", "in2").AllHaveSignals() {
return component.ErrWaitingForInputsKeep
}
// Process inputs
return nil
})_ = fm.Components().
Filter(func(c *component.Component) bool {
return c.Labels().ValueIs("criticality", "high")
}).
ForEach(func(c *component.Component) error {
log.Printf("Critical component: %s\n", c.Name())
return nil
})// Double all numeric signals, preserving labels
doubled := port.Signals().MapIf(
func(s *signal.Signal) bool { _, ok := s.PayloadOrNil().(int); return ok },
func(s *signal.Signal) *signal.Signal {
num := s.PayloadOrNil().(int)
return signal.New(num * 2).WithLabels(s.Labels().All())
},
)activeCount := fm.Components().Count(func(c *component.Component) bool {
return c.Labels().ValueIs("status", "active")
})| Scenario | Use Collection | Use Group |
|---|---|---|
| Need to access by name | ✅ Yes | ❌ No |
| Need guaranteed order | ❌ No | ✅ Yes |
| Names must be unique | ✅ Yes | ❌ No |
| Simple iteration | Both work | ✅ Simpler |
| Known entity names | ✅ Yes | ❌ No |
| Dynamic entity sets | Both work | ✅ More flexible |
// Use Collection when you have named entities
_, _ = component.New("processor",
component.WithInputs("data", "config", "control"),
)
// Use Group when working with anonymous entities
_ = port.PutSignals(
signal.New(1),
signal.New(2),
signal.New(3),
)One of F-Mesh's strengths is API consistency. The same methods work across different entity types:
fm.Components().Filter(predicate)
c.Inputs().Filter(predicate)
port.Signals().Filter(predicate)fm.Components().ForEach(func(c *component.Component) error { ... })
c.Inputs().ForEach(func(p *port.Port) error { ... })
port.Signals().ForEach(func(s *signal.Signal) error { ... })fm.Components().AnyMatch(predicate)
c.Inputs().AnyMatch(predicate)
port.Signals().Any(predicate)Collections and Groups provide a consistent, powerful API for working with multiple entities in F-Mesh:
- Collections are for named, indexed access (components, ports)
- Groups are for ordered, sequential access (signals, port groups, cycles)
- Both support filtering, mapping, and iteration with the same API
-
Direct error returns — methods that can fail return
error; lookups returnnil - API consistency across different entity types makes the framework easy to learn
Understanding these patterns is fundamental to writing clean, idiomatic F-Mesh code.