Skip to content

203. Collections and Groups

github-actions[bot] edited this page Jul 23, 2026 · 2 revisions

Overview

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

Collections are indexed structures where each entity has a unique name. They're ideal when you need to access specific entities by their identifiers.

Characteristics

  • 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

Where Collections Are Used

Type Description
component.Collection Set of components accessed by name
port.Collection Set of ports accessed by name

Basic Operations

// 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()

Filtering Collections

// 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, use AnyMatch().

Iterating Collections

// 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
})

Adding and Removing

// 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

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.

Characteristics

  • 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

Where Groups Are Used

Type Description
signal.Group Ordered set of signals
port.Group Ordered set of ports

Basic Operations

// 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()

Creating Signal Groups

// Create a group from multiple payloads
group := signal.NewGroup(1, "hello", 3.14, true)

// Add signals to existing group
_ = port.PutSignals(sig1, sig2, sig3)

Filtering Groups

// 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"))

Iterating Groups

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)
    },
)

Working with Payloads

// 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()

Metadata on Groups and Collections

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 batch operations

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.)


Error Handling

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
}

Common Error Scenarios

// 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 */ }

Common Patterns

Check if All Ports Have Signals

component.WithActivationFunc(func(this *component.Component) error {
    if !this.Inputs().ByNames("in1", "in2").AllHaveSignals() {
        return component.ErrWaitingForInputsKeep
    }
    // Process inputs
    return nil
})

Filter and Process Components

_ = 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
    })

Transform Signal Payloads

// 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())
    },
)

Count Matching Entities

activeCount := fm.Components().Count(func(c *component.Component) bool {
    return c.Labels().ValueIs("status", "active")
})

Collection vs Group: When to Use Which

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

Examples

// 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),
)

API Consistency

One of F-Mesh's strengths is API consistency. The same methods work across different entity types:

Filtering

fm.Components().Filter(predicate)
c.Inputs().Filter(predicate)
port.Signals().Filter(predicate)

Iteration

fm.Components().ForEach(func(c *component.Component) error { ... })
c.Inputs().ForEach(func(p *port.Port) error { ... })
port.Signals().ForEach(func(s *signal.Signal) error { ... })

Checking Conditions

fm.Components().AnyMatch(predicate)
c.Inputs().AnyMatch(predicate)
port.Signals().Any(predicate)

Summary

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 return nil
  • API consistency across different entity types makes the framework easy to learn

Understanding these patterns is fundamental to writing clean, idiomatic F-Mesh code.

Clone this wiki locally