From 2ca19e593c8e030b0bb828b76d534209b61b1b65 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Mon, 23 Jun 2025 17:49:21 -0700 Subject: [PATCH 01/11] more text up front --- docs/rfcs/functional-composition-pattern.md | 337 ++++++++++++++++++++ 1 file changed, 337 insertions(+) create mode 100644 docs/rfcs/functional-composition-pattern.md diff --git a/docs/rfcs/functional-composition-pattern.md b/docs/rfcs/functional-composition-pattern.md new file mode 100644 index 00000000000..9912fc96d7c --- /dev/null +++ b/docs/rfcs/functional-composition-pattern.md @@ -0,0 +1,337 @@ +# Functional Composition Pattern + +## Overview + +This codebase uses a **Functional Composition** pattern for its core +interfaces. This pattern decomposes individual methods from interface +types into corresponding function types, then recomposes them into a +concrete implementation type. This approach provides flexibility, +testability, and safe interface evolution for the OpenTelemetry +Collector. + +When an interface type is exported for users outside of this +repository, the type MUST follow these guidelines. Interface types +exposed in internal packages may decide to export internal interfaces, +of course. + +For every method in the public interface, a corresponding `type +Func func(...) ...` declaration in the same package will +exist, having the matching signature. + +For every interface type, there is a corresponding functional +constructor `func New(Func, Func, ...) Type` +in the same package for constructing a functional composition of +interface methods. + +Interface stability for exported interface types is our primary +objective. The Functional Composition pattern supports safe interface +evolution, first by "sealing" the type with an unexported interface +method. This means all implementations of an interface must use +constructors provided in the package. + +These "sealed concrete" implementation objects support adding new +methods in future releases, _without changing the major version +number_, because public interface types are always provided through a +package-provided implementation. As a key requirement, every function +must have a simple "no-op" implementation correspodning with the zero +value of the `Func`. + +Additional methods may be provided using the widely-known [Functional +Option pattern](https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html). + +## Key concepts + +### 1. Decompose Interfaces into Function Types + +Instead of implementing interfaces directly on structs, we create +function types for each method: + +```go +// Interface definition +type RateReservation interface { + WaitTime() time.Duration + Cancel() +} + +// Function types for each method +type WaitTimeFunc func() time.Duration +type CancelFunc func() + +// Function types implement their corresponding methods +func (f WaitTimeFunc) WaitTime() time.Duration { + if f == nil { + return 0 + } + return f() +} + +func (f CancelFunc) Cancel() { + if f == nil { + return + } + f() +} +``` + +Users of the `net/http` package have seen this pattern before. The +`http.HandlerFunc` type can be seen as the prototype for functional +composition, in this case for HTTP handlers. Interestingly, the +single-method `http.RoundTripper` interface, which represents the same +interaction on the client-side, does not have a `RoundTripperFunc`. In +this codebase, the pattern is applied exstensively. + +### 2. Compose Functions into Interface Implementations + +Create concrete implementations embed the corresponding function type: + +```go +type rateReservationImpl struct { + WaitTimeFunc + CancelFunc +} + +// Constructor for an instance +func NewRateReservation(wf WaitTimeFunc, cf CancelFunc) RateReservation { + return rateReservationImpl{ + WaitTimeFunc: wf, + CancelFunc: cf, + } +} +``` + +[The Go language automatically converts function literal +expressions](https://go.dev/doc/effective_go#conversions) into the +correct named type (i.e., `Func`), so we can write: + +```go + return NewRateReservation( + // Wait time 1 second + func() time.Duration { return time.Second }, + // Cancel is a no-op. + nil, + ) +``` + +### 3. Use Constructors for Interface Values + +Provide constructor functions rather than exposing concrete types: + +```go +// Good: Constructor function +func NewRateLimiter(f ReserveRateFunc) RateLimiter { + return rateLimiterImpl{ReserveRateFunc: f} +} + +// Avoid: Direct struct instantiation inside the package +// rateLimiterImpl{ReserveRateFunc: f} // Don't do this, use the constructor +``` + +This will help maintainers upgrade your callsites, should the +interface gain a new method. For more complicated interfaces, this +pattern can be combined with the Functional Option pattern in Golang, +shown in the next example. + +Taken from `receiver/receiver.go`, here we setup signal-specific +functions using a functional-option argument passed to +`receiver.NewFactory`: + +```go +// Setup optional signal-specific functions (e.g., logs) +func WithLogs(createLogs CreateLogsFunc, sl component.StabilityLevel) FactoryOption { + return factoryOptionFunc(func(o *factoryImpl, cfgType component.Type) { + o.CreateLogsFunc = createLogs + o.LogsStabilityFunc = sl.Self + }) +} + +// Accept options to configure various aspects of the interface +func NewFactory(cfgType component.Type, createDefaultConfig component.CreateDefaultConfigFunc, options ...FactoryOption) Factory { + f := factoryImpl{ + Factory: component.NewFactoryImpl(cfgType.Self, createDefaultConfig), + } + for _, opt := range options { + opt.applyOption(&f, cfgType) + } + return f +} +``` + +### 4. Constant-value Function Implementations + +For types defined by simple values, especially for enumerated types, +define a `Self()` method to act as the corresponding functional +constant: + +```go +// Self returns itself. +func (t Type) Self() Type { + return t +} + +// TypeFunc is ... +type TypeFunc func() Type + +// Type gets the type of the component created by this factory. +func (f TypeFunc) Type() Type { + if f == nil { + } + return f() +} +``` + +For example, we can decompose, modify, and recompose a +`component.Factory`: + +```go + // Construct a factory with a new default Config: + factory := sometype.NewFactory() + cfg := factory.CreateDefaultConfig() + // ... Modify the config object + // Pass cfg.Self as the default config function. + return NewFactoryImpl(factory.Type().Self, cfg.Self) +``` + +## Rationale + +### Flexibility and Composition + +This pattern enables composition scenarios by making it easy to +compose and decompose interface values. For example, to wrap a +`receiver.Factory` with a limiter of some sort: + +```go +// Transform existing factories with cross-cutting concerns +func NewLimitedFactory(fact receiver.Factory, cfgf LimiterConfigurator) receiver.Factory { + return receiver.NewFactoryImpl( + fact.Type, + fact.CreateDefaultConfig, + receiver.CreateTracesFunc(limitReceiver(fact.CreateTraces, traceTraits{}, cfgf)), + fact.TracesStability, + receiver.CreateMetricsFunc(limitReceiver(fact.CreateMetrics, metricTraits{}, cfgf)), + fact.MetricsStability, + receiver.CreateLogsFunc(limitReceiver(fact.CreateLogs, logTraits{}, cfgf)), + fact.LogsStability, + ) +} +``` + +This is sometimes called aspect-oriented programming, for example it +is easy to add logging to an existing function: + +```go +func addLogging(f ReserveRateFunc) ReserveRateFunc { + return func(ctx context.Context, n int) (RateReservation, error) { + log.Printf("Reserving rate for %d", n) + return f(ctx, n) + } +} +``` + +### Safe Interface Evolution + +Using a private method allows sealing the interface type, which forces +external users to use functional constructor methods. This allows +interfaces to evolve safely because users are forced to use +constructor functions, and instead of breaking stability for function +definitions, we can add alternative constructors. + +```go +type RateLimiter interface { + ReserveRate(context.Context, int) (RateReservation, error) + + // Can add new methods without breaking existing code + private() // Prevents external implementations +} +``` + +### Enhanced Testability + +Individual methods can be tested independently: + +```go +func TestWaitTimeFunction(t *testing.T) { + waitFn := WaitTimeFunc(func() time.Duration { return time.Second }) + assert.Equal(t, time.Second, waitFn.WaitTime()) +} +``` + +## Implementation Guidelines + +### 1. Interface Design + +- Define interfaces with clear method signatures +- Include a `private()` method if you need to control implementations +- Keep interfaces focused and cohesive + +### 2. Function Type Naming + +- Use `Func` naming convention +- Ensure function signatures match the interface method exactly +- Always implement the corresponding interface method on the function type + +### 3. Nil Handling + +Always handle nil function types gracefully to act as a no-op implementation. + +```go +func (f ReserveRateFunc) ReserveRate(ctx context.Context, value int) (RateReservation, error) { + if f == nil { + return NewRateReservationImpl(nil, nil), nil // No-op implementation + } + return f(ctx, value) +} +``` + +### 4. Constructor Patterns + +Follow consistent constructor naming for building an implementation +of each interface: + +```go +// For interfaces: NewImpl +func NewRateLimiterImpl(f ReserveRateFunc) RateLimiter +``` + +Follow consistent type naming for each method-function type: + +```go +// For function types: Func +type ReserveRateFunc func(context.Context, int) (RateReservation, error) +``` + +### 5. Implementation Structs + +- Use unexported names for implementation structs +- Embed function types directly +- Implement private methods for sealing the interface + +```go +type RateLimiter interface { + // ... + + // Must use functional constructors outside this package + private() +} + +type rateLimiterImpl struct { + ReserveRateFunc +} + +func (rateLimiterImpl) private() {} +``` + +## When to Use This Pattern + +### Appropriate Use Cases + +- **Interfaces with single or multiple methods** that benefit from composition +- **Cross-cutting concerns** that need to be applied selectively +- **Interface evolution** where you need to add methods over time +- **Factory patterns** where you're assembling behavior from components +- **Middleware/decorator scenarios** where you're transforming behavior + +### When to Avoid + +- **Stateful objects** where methods need to share significant state +- **Performance-critical code** where function call overhead matters +- **Simple implementations** where the pattern adds unnecessary complexity From b34fb4d8315b8836b5fab6d6218d3d4c8201a1f1 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Tue, 24 Jun 2025 11:46:28 -0700 Subject: [PATCH 02/11] human --- docs/rfcs/functional-composition-pattern.md | 277 ++++++++------------ 1 file changed, 115 insertions(+), 162 deletions(-) diff --git a/docs/rfcs/functional-composition-pattern.md b/docs/rfcs/functional-composition-pattern.md index 9912fc96d7c..fa210d05ce4 100644 --- a/docs/rfcs/functional-composition-pattern.md +++ b/docs/rfcs/functional-composition-pattern.md @@ -3,16 +3,16 @@ ## Overview This codebase uses a **Functional Composition** pattern for its core -interfaces. This pattern decomposes individual methods from interface -types into corresponding function types, then recomposes them into a -concrete implementation type. This approach provides flexibility, -testability, and safe interface evolution for the OpenTelemetry -Collector. +interfaces. This pattern decomposes the individual methods from +interface types into corresponding function types, then recomposes +them into a concrete implementation type. This approach provides +flexibility, testability, and safe interface evolution for the +OpenTelemetry Collector. When an interface type is exported for users outside of this -repository, the type MUST follow these guidelines. Interface types -exposed in internal packages may decide to export internal interfaces, -of course. +repository, the type MUST follow these guidelines, including the use +of an unexported method to "seal" the interface. Interface types +exposed from internal packages may opt-out of this recommendation. For every method in the public interface, a corresponding `type Func func(...) ...` declaration in the same package will @@ -33,12 +33,9 @@ These "sealed concrete" implementation objects support adding new methods in future releases, _without changing the major version number_, because public interface types are always provided through a package-provided implementation. As a key requirement, every function -must have a simple "no-op" implementation correspodning with the zero +must have a simple "no-op" implementation corresponding with the zero value of the `Func`. -Additional methods may be provided using the widely-known [Functional -Option pattern](https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html). - ## Key concepts ### 1. Decompose Interfaces into Function Types @@ -60,29 +57,31 @@ type CancelFunc func() // Function types implement their corresponding methods func (f WaitTimeFunc) WaitTime() time.Duration { if f == nil { - return 0 + return 0 // No-op behavior } return f() } func (f CancelFunc) Cancel() { if f == nil { - return + return // No-op behavior } f() } ``` -Users of the `net/http` package have seen this pattern before. The -`http.HandlerFunc` type can be seen as the prototype for functional -composition, in this case for HTTP handlers. Interestingly, the -single-method `http.RoundTripper` interface, which represents the same -interaction on the client-side, does not have a `RoundTripperFunc`. In -this codebase, the pattern is applied exstensively. +Users of the [`net/http` package have seen this +pattern](https://pkg.go.dev/net/http#HandlerFunc). `http.HandlerFunc` +can be seen as a prototype for the Functional Composition pattern, in +this case for HTTP servers. Interestingly, the single-method +`http.RoundTripper` interface, representing the same interaction for +HTTP clients, does not have a `RoundTripperFunc`. In this codebase, +the pattern is applied exstensively. -### 2. Compose Functions into Interface Implementations +### 2. Compose Function Types into Interface Implementations -Create concrete implementations embed the corresponding function type: +Create concrete implementations embedding the function type +corresponding with each interface method: ```go type rateReservationImpl struct { @@ -90,18 +89,53 @@ type rateReservationImpl struct { CancelFunc } -// Constructor for an instance +This pattern applies even for single-method interfaces, where the +`Func` is capable of implementing the interaface. + +type RateLimiter interface { + ReserveRate(context.Context, int) RateReservation +} + +type ReserveRateFunc func(context.Context, int) RateReservation + +func (f ReserveRateFunc) ReserveRate(ctx context.Context, value int) RateReservation { + if f == nil { + return rateReservationImpl{} // Composite no-op behavior + } + f(ctx, value) +} + +// Implement the interface and use the struct via its constructor, not +// the function type, to implement a single-method interface. +type rateLimiterImpl struct { + ReserveRateFunc +} +``` + +### 3. Use Constructors for Interface Values + +Provide constructor functions rather than exposing concrete types. By +default, each interface should provide a `func +New(Func, Func, ...) Type` for all methods, +using the concrete implementation struct. For example: + +```go func NewRateReservation(wf WaitTimeFunc, cf CancelFunc) RateReservation { return rateReservationImpl{ WaitTimeFunc: wf, CancelFunc: cf, } } + +func NewRateLimiter(f ReserveRateFunc) RateLimiter { + return rateLimiterImpl{ReserveRateFunc: f} +} ``` [The Go language automatically converts function literal expressions](https://go.dev/doc/effective_go#conversions) into the -correct named type (i.e., `Func`), so we can write: +correct named type (i.e., `Func`), so we can pass function +literals to these constructors without an explicit conversion: ```go return NewRateReservation( @@ -112,24 +146,10 @@ correct named type (i.e., `Func`), so we can write: ) ``` -### 3. Use Constructors for Interface Values - -Provide constructor functions rather than exposing concrete types: - -```go -// Good: Constructor function -func NewRateLimiter(f ReserveRateFunc) RateLimiter { - return rateLimiterImpl{ReserveRateFunc: f} -} - -// Avoid: Direct struct instantiation inside the package -// rateLimiterImpl{ReserveRateFunc: f} // Don't do this, use the constructor -``` - -This will help maintainers upgrade your callsites, should the -interface gain a new method. For more complicated interfaces, this -pattern can be combined with the Functional Option pattern in Golang, -shown in the next example. +For more complicated interfaces, this pattern can be combined with the +[Functional Option +pattern](https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html) +in Golang, shown in the next example. Taken from `receiver/receiver.go`, here we setup signal-specific functions using a functional-option argument passed to @@ -147,7 +167,7 @@ func WithLogs(createLogs CreateLogsFunc, sl component.StabilityLevel) FactoryOpt // Accept options to configure various aspects of the interface func NewFactory(cfgType component.Type, createDefaultConfig component.CreateDefaultConfigFunc, options ...FactoryOption) Factory { f := factoryImpl{ - Factory: component.NewFactoryImpl(cfgType.Self, createDefaultConfig), + Factory: component.NewFactory(cfgType.Self, createDefaultConfig), } for _, opt := range options { opt.applyOption(&f, cfgType) @@ -156,7 +176,46 @@ func NewFactory(cfgType component.Type, createDefaultConfig component.CreateDefa } ``` -### 4. Constant-value Function Implementations +### 4. Seal Public Interface Types + +Using an unexported method "seals" the interface type so external +packages can only use, not implement the interface. This allows +interfaces to evolve safely because users are forced to use +constructor functions. + +```go +type RateLimiter interface { + ReserveRate(context.Context, int) (RateReservation, error) + + private() // Prevents external implementations +} +``` + +This practice enables safely evolving interfaces. A new method can be +added to a public interface type because public constructor functions +force the user to obtain the new type and the new type is guaranteed +to implement the old interface. If the functional option pattern is +already being used, then new interface methods will need no new +constructors, otherwise backwards compatibility can be maintained by +adding new constructors, for example: + +```go +type RateLimiter interface { + // Original method + ReserveRate(context.Context, int) RateReservation + + // New method (optional support) + ExtraFeature() +} + +// Original constructor +func NewRateLimiter(f ReserveRateFunc) RateLimiter { ... } + +// New constructor +func NewRateLimiterWithExtraFeature(rf ReserveRateFunc, ef ExtraFeatureFunc) RateLimiter { ... } +``` + +### 5. Constant-value Function Implementations For types defined by simple values, especially for enumerated types, define a `Self()` method to act as the corresponding functional @@ -180,18 +239,22 @@ func (f TypeFunc) Type() Type { ``` For example, we can decompose, modify, and recompose a -`component.Factory`: +`component.Factory` easily using Self methods to capture the +constant-valued Type and Config functions: ```go - // Construct a factory with a new default Config: - factory := sometype.NewFactory() - cfg := factory.CreateDefaultConfig() - // ... Modify the config object - // Pass cfg.Self as the default config function. + // Construct a factory, get its default default Config: + originalFactory := somepackage.NewFactory() + cfg := originalFactory.CreateDefaultConfig() + + // ... Modify the config object somehow + + // Pass cfg.Self as the default config function, + // return a new factory using the modified config. return NewFactoryImpl(factory.Type().Self, cfg.Self) ``` -## Rationale +## Examples ### Flexibility and Composition @@ -215,8 +278,7 @@ func NewLimitedFactory(fact receiver.Factory, cfgf LimiterConfigurator) receiver } ``` -This is sometimes called aspect-oriented programming, for example it -is easy to add logging to an existing function: +For example, it is easy to add logging to an existing function: ```go func addLogging(f ReserveRateFunc) ReserveRateFunc { @@ -226,112 +288,3 @@ func addLogging(f ReserveRateFunc) ReserveRateFunc { } } ``` - -### Safe Interface Evolution - -Using a private method allows sealing the interface type, which forces -external users to use functional constructor methods. This allows -interfaces to evolve safely because users are forced to use -constructor functions, and instead of breaking stability for function -definitions, we can add alternative constructors. - -```go -type RateLimiter interface { - ReserveRate(context.Context, int) (RateReservation, error) - - // Can add new methods without breaking existing code - private() // Prevents external implementations -} -``` - -### Enhanced Testability - -Individual methods can be tested independently: - -```go -func TestWaitTimeFunction(t *testing.T) { - waitFn := WaitTimeFunc(func() time.Duration { return time.Second }) - assert.Equal(t, time.Second, waitFn.WaitTime()) -} -``` - -## Implementation Guidelines - -### 1. Interface Design - -- Define interfaces with clear method signatures -- Include a `private()` method if you need to control implementations -- Keep interfaces focused and cohesive - -### 2. Function Type Naming - -- Use `Func` naming convention -- Ensure function signatures match the interface method exactly -- Always implement the corresponding interface method on the function type - -### 3. Nil Handling - -Always handle nil function types gracefully to act as a no-op implementation. - -```go -func (f ReserveRateFunc) ReserveRate(ctx context.Context, value int) (RateReservation, error) { - if f == nil { - return NewRateReservationImpl(nil, nil), nil // No-op implementation - } - return f(ctx, value) -} -``` - -### 4. Constructor Patterns - -Follow consistent constructor naming for building an implementation -of each interface: - -```go -// For interfaces: NewImpl -func NewRateLimiterImpl(f ReserveRateFunc) RateLimiter -``` - -Follow consistent type naming for each method-function type: - -```go -// For function types: Func -type ReserveRateFunc func(context.Context, int) (RateReservation, error) -``` - -### 5. Implementation Structs - -- Use unexported names for implementation structs -- Embed function types directly -- Implement private methods for sealing the interface - -```go -type RateLimiter interface { - // ... - - // Must use functional constructors outside this package - private() -} - -type rateLimiterImpl struct { - ReserveRateFunc -} - -func (rateLimiterImpl) private() {} -``` - -## When to Use This Pattern - -### Appropriate Use Cases - -- **Interfaces with single or multiple methods** that benefit from composition -- **Cross-cutting concerns** that need to be applied selectively -- **Interface evolution** where you need to add methods over time -- **Factory patterns** where you're assembling behavior from components -- **Middleware/decorator scenarios** where you're transforming behavior - -### When to Avoid - -- **Stateful objects** where methods need to share significant state -- **Performance-critical code** where function call overhead matters -- **Simple implementations** where the pattern adds unnecessary complexity From b9fc80112a14ae5bdcbc56e71ce0cd7e9c9da77f Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Tue, 24 Jun 2025 12:47:47 -0700 Subject: [PATCH 03/11] lint --- docs/rfcs/functional-composition-pattern.md | 41 ++++++++++----------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/docs/rfcs/functional-composition-pattern.md b/docs/rfcs/functional-composition-pattern.md index fa210d05ce4..5a907c77d5f 100644 --- a/docs/rfcs/functional-composition-pattern.md +++ b/docs/rfcs/functional-composition-pattern.md @@ -160,7 +160,7 @@ functions using a functional-option argument passed to func WithLogs(createLogs CreateLogsFunc, sl component.StabilityLevel) FactoryOption { return factoryOptionFunc(func(o *factoryImpl, cfgType component.Type) { o.CreateLogsFunc = createLogs - o.LogsStabilityFunc = sl.Self + o.LogsStabilityFunc = sl.Self // See (5) below }) } @@ -218,20 +218,19 @@ func NewRateLimiterWithExtraFeature(rf ReserveRateFunc, ef ExtraFeatureFunc) Rat ### 5. Constant-value Function Implementations For types defined by simple values, especially for enumerated types, -define a `Self()` method to act as the corresponding functional -constant: +define a `Self()` method to act as the corresponding value: ```go // Self returns itself. -func (t Type) Self() Type { +func (t Config) Self() Config { return t } -// TypeFunc is ... -type TypeFunc func() Type +// ConfigFunc is ... +type ConfigFunc func() Config -// Type gets the type of the component created by this factory. -func (f TypeFunc) Type() Type { +// Config gets the type of the component created by this factory. +func (f ConfigFunc) Config() Config { if f == nil { } return f() @@ -243,24 +242,22 @@ For example, we can decompose, modify, and recompose a constant-valued Type and Config functions: ```go - // Construct a factory, get its default default Config: - originalFactory := somepackage.NewFactory() - cfg := originalFactory.CreateDefaultConfig() - - // ... Modify the config object somehow - - // Pass cfg.Self as the default config function, - // return a new factory using the modified config. - return NewFactoryImpl(factory.Type().Self, cfg.Self) +// Copy a factory from somepackage, modify its default config. +func modifiedFactory() Factory { + original := somepackage.NewFactory() + cfg := original.CreateDefaultConfig() + + // ... Modify the config object somehow, + // pass cfg.Self as the default config function. + return component.NewFactory(original.Type, cfg.Self) +} ``` ## Examples -### Flexibility and Composition - -This pattern enables composition scenarios by making it easy to -compose and decompose interface values. For example, to wrap a -`receiver.Factory` with a limiter of some sort: +This pattern enables composition by making it easy to compose and +decompose interface values. For example, to wrap a `receiver.Factory` +with a limiter of some sort: ```go // Transform existing factories with cross-cutting concerns From eb2c7d626f45d9bb76f308cd813176a17e5681e5 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Tue, 24 Jun 2025 12:57:14 -0700 Subject: [PATCH 04/11] spell --- docs/rfcs/functional-composition-pattern.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/rfcs/functional-composition-pattern.md b/docs/rfcs/functional-composition-pattern.md index 5a907c77d5f..498b6c0ccdb 100644 --- a/docs/rfcs/functional-composition-pattern.md +++ b/docs/rfcs/functional-composition-pattern.md @@ -76,7 +76,7 @@ can be seen as a prototype for the Functional Composition pattern, in this case for HTTP servers. Interestingly, the single-method `http.RoundTripper` interface, representing the same interaction for HTTP clients, does not have a `RoundTripperFunc`. In this codebase, -the pattern is applied exstensively. +the pattern is applied extensively. ### 2. Compose Function Types into Interface Implementations @@ -90,7 +90,7 @@ type rateReservationImpl struct { } This pattern applies even for single-method interfaces, where the -`Func` is capable of implementing the interaface. +`Func` is capable of implementing the interface. type RateLimiter interface { ReserveRate(context.Context, int) RateReservation @@ -261,15 +261,15 @@ with a limiter of some sort: ```go // Transform existing factories with cross-cutting concerns -func NewLimitedFactory(fact receiver.Factory, cfgf LimiterConfigurator) receiver.Factory { +func NewLimitedFactory(fact receiver.Factory, cfg LimiterConfigurator) receiver.Factory { return receiver.NewFactoryImpl( fact.Type, fact.CreateDefaultConfig, - receiver.CreateTracesFunc(limitReceiver(fact.CreateTraces, traceTraits{}, cfgf)), + receiver.CreateTracesFunc(limitReceiver(fact.CreateTraces, traceTraits{}, cfg)), fact.TracesStability, - receiver.CreateMetricsFunc(limitReceiver(fact.CreateMetrics, metricTraits{}, cfgf)), + receiver.CreateMetricsFunc(limitReceiver(fact.CreateMetrics, metricTraits{}, cfg)), fact.MetricsStability, - receiver.CreateLogsFunc(limitReceiver(fact.CreateLogs, logTraits{}, cfgf)), + receiver.CreateLogsFunc(limitReceiver(fact.CreateLogs, logTraits{}, cfg)), fact.LogsStability, ) } From 4022a1db82eb433448c3e6bfb17f14156df50424 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Tue, 24 Jun 2025 13:10:43 -0700 Subject: [PATCH 05/11] chlog --- .chloggen/functional-composition.yaml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .chloggen/functional-composition.yaml diff --git a/.chloggen/functional-composition.yaml b/.chloggen/functional-composition.yaml new file mode 100644 index 00000000000..56d4d9c589c --- /dev/null +++ b/.chloggen/functional-composition.yaml @@ -0,0 +1,25 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver) +component: Document Functional Composition pattern + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: This pattern already exists, this RFC reinforces what we're doing. + +# One or more tracking issues or pull requests related to the change +issues: [13263] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [api] From 3ae70a326a7e9f85ab11239cb381773781f1cc79 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 25 Jun 2025 09:07:23 -0700 Subject: [PATCH 06/11] edits, remove chlog --- .chloggen/functional-composition.yaml | 25 --------- docs/rfcs/functional-composition-pattern.md | 59 ++++++++++++++------- 2 files changed, 40 insertions(+), 44 deletions(-) delete mode 100644 .chloggen/functional-composition.yaml diff --git a/.chloggen/functional-composition.yaml b/.chloggen/functional-composition.yaml deleted file mode 100644 index 56d4d9c589c..00000000000 --- a/.chloggen/functional-composition.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# Use this changelog template to create an entry for release notes. - -# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' -change_type: enhancement - -# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver) -component: Document Functional Composition pattern - -# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). -note: This pattern already exists, this RFC reinforces what we're doing. - -# One or more tracking issues or pull requests related to the change -issues: [13263] - -# (Optional) One or more lines of additional information to render under the primary note. -# These lines will be padded with 2 spaces and then inserted directly into the document. -# Use pipe (|) for multiline entries. -subtext: - -# Optional: The change log or logs in which this entry should be included. -# e.g. '[user]' or '[user, api]' -# Include 'user' if the change is relevant to end users. -# Include 'api' if there is a change to a library API. -# Default: '[user]' -change_logs: [api] diff --git a/docs/rfcs/functional-composition-pattern.md b/docs/rfcs/functional-composition-pattern.md index 498b6c0ccdb..742361121bf 100644 --- a/docs/rfcs/functional-composition-pattern.md +++ b/docs/rfcs/functional-composition-pattern.md @@ -10,8 +10,7 @@ flexibility, testability, and safe interface evolution for the OpenTelemetry Collector. When an interface type is exported for users outside of this -repository, the type MUST follow these guidelines, including the use -of an unexported method to "seal" the interface. Interface types +repository, the type MUST follow these guidelines. Interface types exposed from internal packages may opt-out of this recommendation. For every method in the public interface, a corresponding `type @@ -26,15 +25,18 @@ interface methods. Interface stability for exported interface types is our primary objective. The Functional Composition pattern supports safe interface evolution, first by "sealing" the type with an unexported interface -method. This means all implementations of an interface must use -constructors provided in the package. +method. This means all implementations of an interface must embed or +use constructors provided in the package. These "sealed concrete" implementation objects support adding new methods in future releases, _without changing the major version number_, because public interface types are always provided through a -package-provided implementation. As a key requirement, every function -must have a simple "no-op" implementation corresponding with the zero -value of the `Func`. +package-provided implementation. + +As a key requirement, every function must have a simple "no-op" +implementation corresponding with the zero value of the +`Func`. The expression `New(nil, nil, ...)` is the empty +implementation for each type. ## Key concepts @@ -75,28 +77,35 @@ pattern](https://pkg.go.dev/net/http#HandlerFunc). `http.HandlerFunc` can be seen as a prototype for the Functional Composition pattern, in this case for HTTP servers. Interestingly, the single-method `http.RoundTripper` interface, representing the same interaction for -HTTP clients, does not have a `RoundTripperFunc`. In this codebase, -the pattern is applied extensively. +HTTP clients, does not have a `RoundTripperFunc` in the base library +(consequently, this codebase defines [it for testing middleware +extensions](`../../extension/extensionmiddleware/extensionmiddlewaretest/nop.go`)). In +this codebase, the pattern is applied extensively. ### 2. Compose Function Types into Interface Implementations Create concrete implementations embedding the function type corresponding with each interface method: -```go +```go +// A struct embedding a Func for each method. type rateReservationImpl struct { WaitTimeFunc CancelFunc } +``` This pattern applies even for single-method interfaces, where the -`Func` is capable of implementing the interface. +`Func` would implement the interface, were it not sealed. +```go +// Single-method interface type type RateLimiter interface { ReserveRate(context.Context, int) RateReservation } -type ReserveRateFunc func(context.Context, int) RateReservation +// Single function type +ReserveRateFunc func(context.Context, int) RateReservation func (f ReserveRateFunc) ReserveRate(ctx context.Context, value int) RateReservation { if f == nil { @@ -105,8 +114,7 @@ func (f ReserveRateFunc) ReserveRate(ctx context.Context, value int) RateReserva f(ctx, value) } -// Implement the interface and use the struct via its constructor, not -// the function type, to implement a single-method interface. +// The matching concrete type. type rateLimiterImpl struct { ReserveRateFunc } @@ -184,20 +192,30 @@ interfaces to evolve safely because users are forced to use constructor functions. ```go +// Public interfaces must include at least one private method type RateLimiter interface { ReserveRate(context.Context, int) (RateReservation, error) - private() // Prevents external implementations + // Prevents external implementations + private() +} + +// Concrete implementations are sealed with this method. +type rateLimiterImpl struct { + ReserveRateFunc } + +func (rateLimiterImpl) private() {} ``` This practice enables safely evolving interfaces. A new method can be added to a public interface type because public constructor functions force the user to obtain the new type and the new type is guaranteed to implement the old interface. If the functional option pattern is -already being used, then new interface methods will need no new -constructors, otherwise backwards compatibility can be maintained by -adding new constructors, for example: +already being used, then new interface methods will not require new +constructors, only new options. If the functional option pattern is +not in use, backwards compatibility can be maintained by adding new +constructors, for example: ```go type RateLimiter interface { @@ -212,7 +230,10 @@ type RateLimiter interface { func NewRateLimiter(f ReserveRateFunc) RateLimiter { ... } // New constructor -func NewRateLimiterWithExtraFeature(rf ReserveRateFunc, ef ExtraFeatureFunc) RateLimiter { ... } +func NewRateLimiterWithOptions(rf ReserveRateFunc, opts ...Option) RateLimiter { ... } + +// New option +func WithExtraFeature(...) Option { ... } ``` ### 5. Constant-value Function Implementations From fb3faf1801da4f6d32da8a7bc97b01d782c2781b Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 25 Jun 2025 09:13:11 -0700 Subject: [PATCH 07/11] again --- docs/rfcs/functional-composition-pattern.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/rfcs/functional-composition-pattern.md b/docs/rfcs/functional-composition-pattern.md index 742361121bf..0d4ac6d15eb 100644 --- a/docs/rfcs/functional-composition-pattern.md +++ b/docs/rfcs/functional-composition-pattern.md @@ -250,7 +250,7 @@ func (t Config) Self() Config { // ConfigFunc is ... type ConfigFunc func() Config -// Config gets the type of the component created by this factory. +// Config gets the default configuration for this factory. func (f ConfigFunc) Config() Config { if f == nil { } @@ -259,18 +259,21 @@ func (f ConfigFunc) Config() Config { ``` For example, we can decompose, modify, and recompose a -`component.Factory` easily using Self methods to capture the -constant-valued Type and Config functions: +`component.Factory` easily using Self instead of the inline `func() +Config { return cfg }` to capture the constant-valued Config function: ```go // Copy a factory from somepackage, modify its default config. func modifiedFactory() Factory { original := somepackage.NewFactory() + otype := original.Type() cfg := original.CreateDefaultConfig() - // ... Modify the config object somehow, - // pass cfg.Self as the default config function. - return component.NewFactory(original.Type, cfg.Self) + // Modify the config object + ... + + // Here, otype.Self equals original.Type. + return component.NewFactory(otype.Self, cfg.Self) } ``` From 21224a0f09557d4a297bc48f13419fc940ed572a Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 25 Jun 2025 09:14:20 -0700 Subject: [PATCH 08/11] whitespace --- docs/rfcs/functional-composition-pattern.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/rfcs/functional-composition-pattern.md b/docs/rfcs/functional-composition-pattern.md index 0d4ac6d15eb..f68039f1122 100644 --- a/docs/rfcs/functional-composition-pattern.md +++ b/docs/rfcs/functional-composition-pattern.md @@ -5,7 +5,7 @@ This codebase uses a **Functional Composition** pattern for its core interfaces. This pattern decomposes the individual methods from interface types into corresponding function types, then recomposes -them into a concrete implementation type. This approach provides +them into a concrete implementation type. This approach provides flexibility, testability, and safe interface evolution for the OpenTelemetry Collector. @@ -73,7 +73,7 @@ func (f CancelFunc) Cancel() { ``` Users of the [`net/http` package have seen this -pattern](https://pkg.go.dev/net/http#HandlerFunc). `http.HandlerFunc` +pattern](https://pkg.go.dev/net/http#HandlerFunc). `http.HandlerFunc` can be seen as a prototype for the Functional Composition pattern, in this case for HTTP servers. Interestingly, the single-method `http.RoundTripper` interface, representing the same interaction for @@ -122,10 +122,10 @@ type rateLimiterImpl struct { ### 3. Use Constructors for Interface Values -Provide constructor functions rather than exposing concrete types. By +Provide constructor functions rather than exposing concrete types. By default, each interface should provide a `func New(Func, Func, ...) Type` for all methods, -using the concrete implementation struct. For example: +using the concrete implementation struct. For example: ```go func NewRateReservation(wf WaitTimeFunc, cf CancelFunc) RateReservation { @@ -187,7 +187,7 @@ func NewFactory(cfgType component.Type, createDefaultConfig component.CreateDefa ### 4. Seal Public Interface Types Using an unexported method "seals" the interface type so external -packages can only use, not implement the interface. This allows +packages can only use, not implement the interface. This allows interfaces to evolve safely because users are forced to use constructor functions. From bc4cec8327ceaf1018f26fdc2f000411b2b5a9ad Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Tue, 1 Jul 2025 10:48:08 -0700 Subject: [PATCH 09/11] Update docs/rfcs/functional-composition-pattern.md --- docs/rfcs/functional-composition-pattern.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rfcs/functional-composition-pattern.md b/docs/rfcs/functional-composition-pattern.md index f68039f1122..abd4e295e85 100644 --- a/docs/rfcs/functional-composition-pattern.md +++ b/docs/rfcs/functional-composition-pattern.md @@ -79,7 +79,7 @@ this case for HTTP servers. Interestingly, the single-method `http.RoundTripper` interface, representing the same interaction for HTTP clients, does not have a `RoundTripperFunc` in the base library (consequently, this codebase defines [it for testing middleware -extensions](`../../extension/extensionmiddleware/extensionmiddlewaretest/nop.go`)). In +extensions](https://github.com/open-telemetry/opentelemetry-collector/blob/64088871efb1b873c3d53ed3b7f0ce7140c0d7e2/extension/extensionmiddleware/extensionmiddlewaretest/nop.go#L23)). In this codebase, the pattern is applied extensively. ### 2. Compose Function Types into Interface Implementations From a7abf9f0bc0cf3b6447e42499765e5dfb4bb94dd Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Tue, 1 Jul 2025 11:58:48 -0700 Subject: [PATCH 10/11] draft option --- docs/rfcs/functional-composition-pattern.md | 32 ++++++++++++++------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/docs/rfcs/functional-composition-pattern.md b/docs/rfcs/functional-composition-pattern.md index abd4e295e85..da51558c8d9 100644 --- a/docs/rfcs/functional-composition-pattern.md +++ b/docs/rfcs/functional-composition-pattern.md @@ -18,9 +18,11 @@ For every method in the public interface, a corresponding `type exist, having the matching signature. For every interface type, there is a corresponding functional -constructor `func New(Func, Func, ...) Type` -in the same package for constructing a functional composition of -interface methods. +constructor to enable a functional composition of interface methods, +`func New(Func, {...}, ...Option) Type` which accepts +the initially-required method set and uses a Functional Option pattern +for forwards compatibility, even when there are no options at the +start. Interface stability for exported interface types is our primary objective. The Functional Composition pattern supports safe interface @@ -34,8 +36,8 @@ number_, because public interface types are always provided through a package-provided implementation. As a key requirement, every function must have a simple "no-op" -implementation corresponding with the zero value of the -`Func`. The expression `New(nil, nil, ...)` is the empty +behavior corresponding with the zero value of the `Func`. The +expression `New(nil, nil, ...)` is the "empty" do-nothing implementation for each type. ## Key concepts @@ -123,19 +125,24 @@ type rateLimiterImpl struct { ### 3. Use Constructors for Interface Values Provide constructor functions rather than exposing concrete types. By -default, each interface should provide a `func -New(Func, Func, ...) Type` for all methods, -using the concrete implementation struct. For example: +default, each interface should provide a `New` constructor for +all which returns the corresponding concrete implementation +struct. Methods are "required" when they are explicitly listed as +parameters methods are arguments in the constructor. In addition, a +Functional Optional pattern is provided for use by future optional +methods. + +For example: ```go -func NewRateReservation(wf WaitTimeFunc, cf CancelFunc) RateReservation { +func NewRateReservation(wf WaitTimeFunc, cf CancelFunc, _ ...RateReservationOption) RateReservation { return rateReservationImpl{ WaitTimeFunc: wf, CancelFunc: cf, } } -func NewRateLimiter(f ReserveRateFunc) RateLimiter { +func NewRateLimiter(f ReserveRateFunc, _ ...RateLimiterOption) RateLimiter { return rateLimiterImpl{ReserveRateFunc: f} } ``` @@ -277,6 +284,11 @@ func modifiedFactory() Factory { } ``` +### 6. How to apply the Functional Option pattern + +The functional option pattern is well known. For the Functional +Composition pattern, we require the use of Functional Option arguments + ## Examples This pattern enables composition by making it easy to compose and From 4b8a6250eddb5295ce8ba44d2d9e0de831f2cc4f Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 24 Sep 2025 10:44:51 -0700 Subject: [PATCH 11/11] update --- ...ern.md => functional-interface-pattern.md} | 130 +++++++++++------- 1 file changed, 84 insertions(+), 46 deletions(-) rename docs/rfcs/{functional-composition-pattern.md => functional-interface-pattern.md} (66%) diff --git a/docs/rfcs/functional-composition-pattern.md b/docs/rfcs/functional-interface-pattern.md similarity index 66% rename from docs/rfcs/functional-composition-pattern.md rename to docs/rfcs/functional-interface-pattern.md index da51558c8d9..1accfb35ed7 100644 --- a/docs/rfcs/functional-composition-pattern.md +++ b/docs/rfcs/functional-interface-pattern.md @@ -1,8 +1,8 @@ -# Functional Composition Pattern +# Functional Interface Pattern ## Overview -This codebase uses a **Functional Composition** pattern for its core +This codebase uses a **Functional Interface** pattern for its core interfaces. This pattern decomposes the individual methods from interface types into corresponding function types, then recomposes them into a concrete implementation type. This approach provides @@ -18,19 +18,19 @@ For every method in the public interface, a corresponding `type exist, having the matching signature. For every interface type, there is a corresponding functional -constructor to enable a functional composition of interface methods, -`func New(Func, {...}, ...Option) Type` which accepts -the initially-required method set and uses a Functional Option pattern -for forwards compatibility, even when there are no options at the -start. +constructor to enable an easy composition of interface methods, `func +New(Func, {...}, ...Option) Type` which accepts the +initially-required method set and also applies the well-known +Functional Option pattern for forwards compatibility, even in cases +where there are no options at the start. Interface stability for exported interface types is our primary -objective. The Functional Composition pattern supports safe interface +objective. The Functional Interface pattern supports safe interface evolution, first by "sealing" the type with an unexported interface method. This means all implementations of an interface must embed or use constructors provided in the package. -These "sealed concrete" implementation objects support adding new +These sealed, concrete implementation objects support adding new methods in future releases, _without changing the major version number_, because public interface types are always provided through a package-provided implementation. @@ -45,7 +45,11 @@ implementation for each type. ### 1. Decompose Interfaces into Function Types Instead of implementing interfaces directly on structs, we create -function types for each method: +function types for each method. In the examples developed below, we +describe a hypothetical rate limiter interface that returns a +non-block reservation. The reservation object is a simple pair of two +interface methods saying how long to wait before proceeding and +allowing the caller to cancel their request. ```go // Interface definition @@ -76,13 +80,14 @@ func (f CancelFunc) Cancel() { Users of the [`net/http` package have seen this pattern](https://pkg.go.dev/net/http#HandlerFunc). `http.HandlerFunc` -can be seen as a prototype for the Functional Composition pattern, in +can be seen as a prototype for the Functional Interface pattern, in this case for HTTP servers. Interestingly, the single-method `http.RoundTripper` interface, representing the same interaction for HTTP clients, does not have a `RoundTripperFunc` in the base library -(consequently, this codebase defines [it for testing middleware -extensions](https://github.com/open-telemetry/opentelemetry-collector/blob/64088871efb1b873c3d53ed3b7f0ce7140c0d7e2/extension/extensionmiddleware/extensionmiddlewaretest/nop.go#L23)). In -this codebase, the pattern is applied extensively. +(consequently, this codebase defines [its own for testing middleware +extensions](https://github.com/open-telemetry/opentelemetry-collector/blob/64088871efb1b873c3d53ed3b7f0ce7140c0d7e2/extension/extensionmiddleware/extensionmiddlewaretest/nop.go#L23)). + +In this codebase, the Functional Interface pattern is applied extensively. ### 2. Compose Function Types into Interface Implementations @@ -97,21 +102,22 @@ type rateReservationImpl struct { } ``` -This pattern applies even for single-method interfaces, where the -`Func` would implement the interface, were it not sealed. +This pattern applies even for single-method interfaces, where a single +`Func` provides the complete interface surface. Still, an +interface is used so that it can be sealed, as in: ```go // Single-method interface type type RateLimiter interface { - ReserveRate(context.Context, int) RateReservation + ReserveRate(context.Context, int) (RateReservation, error) } // Single function type -ReserveRateFunc func(context.Context, int) RateReservation +type ReserveRateFunc func(context.Context, int) (RateReservation, error) -func (f ReserveRateFunc) ReserveRate(ctx context.Context, value int) RateReservation { +func (f ReserveRateFunc) ReserveRate(ctx context.Context, value int) (RateReservation, error) { if f == nil { - return rateReservationImpl{} // Composite no-op behavior + return rateReservationImpl{} // No-op behavior } f(ctx, value) } @@ -132,16 +138,20 @@ parameters methods are arguments in the constructor. In addition, a Functional Optional pattern is provided for use by future optional methods. +The Golang "functional option" pattern [first introduced in a Rob Pike +blog +post](https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html) +is by now widely known, spelled out in in Golang blog post ["working +with +interfaces"](https://go.dev/blog/module-compatibility#working-with-interfaces). For +complex interfaces, including all top-level extension types, this +additional pattern is REQUIRED, even at first when there are no +options. + For example: ```go -func NewRateReservation(wf WaitTimeFunc, cf CancelFunc, _ ...RateReservationOption) RateReservation { - return rateReservationImpl{ - WaitTimeFunc: wf, - CancelFunc: cf, - } -} - +// NewRateLimiter func NewRateLimiter(f ReserveRateFunc, _ ...RateLimiterOption) RateLimiter { return rateLimiterImpl{ReserveRateFunc: f} } @@ -153,19 +163,12 @@ correct named type (i.e., `Func`), so we can pass function literals to these constructors without an explicit conversion: ```go - return NewRateReservation( - // Wait time 1 second - func() time.Duration { return time.Second }, - // Cancel is a no-op. - nil, - ) + // construct a rate limiter from a bare function (without options) + return NewRateLimiter(func(ctx context.Context, weight int) RateReservation { + // rate-limiter logic + }) ``` -For more complicated interfaces, this pattern can be combined with the -[Functional Option -pattern](https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html) -in Golang, shown in the next example. - Taken from `receiver/receiver.go`, here we setup signal-specific functions using a functional-option argument passed to `receiver.NewFactory`: @@ -193,7 +196,9 @@ func NewFactory(cfgType component.Type, createDefaultConfig component.CreateDefa ### 4. Seal Public Interface Types -Using an unexported method "seals" the interface type so external +Following the [guidance in "working with +interfaces"](https://go.dev/blog/module-compatibility#working-with-interfaces), +we use an unexported method "seals" the interface type so external packages can only use, not implement the interface. This allows interfaces to evolve safely because users are forced to use constructor functions. @@ -284,20 +289,53 @@ func modifiedFactory() Factory { } ``` -### 6. How to apply the Functional Option pattern +### 6. When to apply the Functional Option pattern + +[The functional option pattern is not always considered +helpful](https://rednafi.com/go/dysfunctional_options_pattern/), since +it adds code complexity and runtime cost when it is applied +unconditionally. + +For the Functional Interface pattern, we require the use of Functional +Option arguments as follows: -The functional option pattern is well known. For the Functional -Composition pattern, we require the use of Functional Option arguments +- Major interfaces, including all top-level extension types and those + with substantial design complexity, are REQUIRED to use the + Functional Option pattern in their constructor. +- Performance interfaces SHOULD NOT use the Functional Option pattern; + top-interfaces are expected to pre-compute the result of functional + options during initialization and obtain simpler interfaces to use + at runtime. +- Interfaces MAY use the Functional Option pattern when they are not + simple, not top-level, and do not require high-performance. + +As an example of a type which does not require functional options, +consider a lazily-evaluated key-value object. The interface allows the +user selective evaluation, knowing that the function call is +expensive. + +```go +// Construct a new rate reservation. Simple API, not a top-level interface, +// and performance sensitive, therefore do not use functional options. +func NewRateReservation(wf WaitTimeFunc, cf CancelFunc) RateReservation { + return rateReservationImpl{ + WaitTimeFunc: wf, + CancelFunc: cf, + } +} +``` ## Examples -This pattern enables composition by making it easy to compose and -decompose interface values. For example, to wrap a `receiver.Factory` -with a limiter of some sort: +The Functional Interface pattern enables code composition through +making it easy to compose and decompose interface values. For example, +to wrap a `receiver.Factory` with a limiter of some sort: ```go // Transform existing factories with cross-cutting concerns -func NewLimitedFactory(fact receiver.Factory, cfg LimiterConfigurator) receiver.Factory { +// Note interface method values (e.g., fact.Type) have the correct +// argument type to pass-through in this constructor +func NewLimitedFactory(fact receiver.Factory, cfg LimiterConfigurator, _ ...Option) receiver.Factory { return receiver.NewFactoryImpl( fact.Type, fact.CreateDefaultConfig,