diff --git a/Examples/Sources/Examples/Composition/ActorLessComposition.swift b/Examples/Sources/Examples/Composition/ActorLessComposition.swift new file mode 100644 index 0000000..1cf810d --- /dev/null +++ b/Examples/Sources/Examples/Composition/ActorLessComposition.swift @@ -0,0 +1,238 @@ +// Example: Actor-less composition of two transducers using sum types and composed proxy +// Oak Example - ActorLessComposition.swift +// +// This example demonstrates how to compose multiple transducers without using actors. +// It uses the BaseTransducer protocol which only requires type definitions without +// requiring an update function, making it ideal for composition. +// +// Key composition patterns demonstrated: +// - Using sum types for events and outputs +// - Composing state from sub-transducer states +// - Creating a custom proxy that delegates to sub-proxies +// - Implementing a run function that delegates to sub-transducer run functions +// - Forwarding outputs from sub-transducers to the parent transducer's output + +import Foundation +import Oak + + +// MARK: - Transducer Definitions +enum A: Transducer { + struct State: NonTerminal { var count: Int } + enum Event { case increment } + typealias Output = Int + + static func update(_ state: inout State, event: Event) -> Int { + switch event { + case .increment: + state.count += 1 + return state.count + } + } +} + +enum B: Transducer { + struct State: NonTerminal { var count: Int } + enum Event { case increment } + typealias Output = Int + + static func update(_ state: inout State, event: Event) -> Int { + switch event { + case .increment: + state.count += 1 + return state.count + } + } +} + +// MARK: - Composition Example +/// `TransducerC` demonstrates composition of two transducers (`A` and `B`) without using actors. +/// It conforms to `BaseTransducer` rather than `Transducer` since it doesn't need to implement +/// an `update` function - it delegates to the component transducers instead. +/// +/// This pattern allows for: +/// - Independent evolution of component transducers +/// - Reuse of existing transducer logic +/// - Separation of concerns between state management and composition +/// - Building complex state machines from simpler building blocks +struct TransducerC: BaseTransducer { + struct State: NonTerminal { + var stateA: A.State + var stateB: B.State + } + + enum Event { + case eventA(A.Event) + case eventB(B.Event) + } + + enum Output { + case outputA(A.Output) + case outputB(B.Output) + } + + struct Proxy: TransducerProxy { + + typealias Event = TransducerC.Event + + let proxyA: A.Proxy + let proxyB: B.Proxy + + init() { + self.proxyA = A.Proxy() + self.proxyB = B.Proxy() + } + + init(proxyA: A.Proxy, proxyB: B.Proxy) { + self.proxyA = proxyA + self.proxyB = proxyB + } + + typealias Stream = AsyncThrowingStream + + var stream: Stream { + fatalError("not implemented") // Implement stream logic if needed + } + + func checkInUse() throws(TransducerError) { + try proxyA.checkInUse() + try proxyB.checkInUse() + } + + func cancel(with error: Swift.Error?) { + proxyA.cancel(with: error) + proxyB.cancel(with: error) + } + + func finish() { + proxyA.finish() + proxyB.finish() + } + + // Unique identifier for the proxy + public let id: UUID = UUID() + + struct Input { + let inputA: A.Proxy.Input + let inputB: B.Proxy.Input + } + + var input: Input { + Input(inputA: proxyA.input, inputB: proxyB.input) + } + + public final class AutoCancellation: Sendable, Equatable { + public static func == (lhs: AutoCancellation, rhs: AutoCancellation) -> Bool { + lhs.id == rhs.id + } + + let autoCancellationA: A.Proxy.AutoCancellation + let autoCancellationB: B.Proxy.AutoCancellation + let id: Proxy.ID + + init(proxy: Proxy) { + autoCancellationA = proxy.proxyA.autoCancellation + autoCancellationB = proxy.proxyB.autoCancellation + id = proxy.id + } + } + + public var autoCancellation: AutoCancellation { + AutoCancellation(proxy: self) + } + + } + + static func run( + initalState: State, + proxy: Proxy, + output: some Subject & Sendable + ) async throws -> Output { + // Create output subjects for A and B that forward to the main output + let subjectA = Oak.Callback { value in + // Forward A's output to the main output subject + // In a real implementation, handle the try/await properly + } + + let subjectB = Oak.Callback { value in + // Forward B's output to the main output subject + // In a real implementation, handle the try/await properly + } + + // Run A and B concurrently + async let resultA = A.run( + initialState: initalState.stateA, + proxy: proxy.proxyA, + output: subjectA + ) + async let resultB = B.run( + initialState: initalState.stateB, + proxy: proxy.proxyB, + output: subjectB + ) + + // Wait for both to finish + let (finalA, _) = try await (resultA, resultB) + + // Compose final output (choose how to represent termination) + // Here, just return the last output from A as an example + return .outputA(finalA) + } + +} + +// MARK: - Example Usage +func example() async { + // Create initial state for TransducerC + let initialState = TransducerC.State( + stateA: A.State(count: 0), + stateB: B.State(count: 0) + ) + + // Create proxy for TransducerC + let proxy = TransducerC.Proxy() + + // Create a callback to handle outputs + let outputCallback = Oak.Callback { output in + switch output { + case .outputA(let value): + print("Output from A: \(value)") + case .outputB(let value): + print("Output from B: \(value)") + } + } + + // Create a task to run the transducer + Task { + do { + let finalOutput = try await TransducerC.run( + initalState: initialState, + proxy: proxy, + output: outputCallback + ) + + print("Final output: \(finalOutput)") + } catch { + print("Error: \(error)") + } + } + + // Send some events to the composed transducer + Task { + // Simulate some interaction with the transducer + try? await Task.sleep(for: .milliseconds(100)) + + // Send an event to A (adjust according to your actual API) + try? proxy.proxyA.input.send(.increment) + + // Send an event to B (adjust according to your actual API) + try? proxy.proxyB.input.send(.increment) + + // Let them run for a bit + try? await Task.sleep(for: .milliseconds(500)) + + // Signal completion + proxy.finish() + } +} + diff --git a/Examples/Sources/Examples/Composition/README.md b/Examples/Sources/Examples/Composition/README.md new file mode 100644 index 0000000..dcf1a00 --- /dev/null +++ b/Examples/Sources/Examples/Composition/README.md @@ -0,0 +1,100 @@ +# Oak Composition Examples + +This folder contains examples of compositional patterns for Oak transducers. + +## Actor-less Composition + +The `ActorLessComposition.swift` example demonstrates how to compose multiple transducers without using actors, leveraging the `BaseTransducer` protocol as a type container for composition. + +### Key Concepts + +1. **Type Composition Patterns** + - **State**: Product type (struct) combining component states + - **Event**: Sum type (enum) for routing events to appropriate sub-transducers + - **Output**: Sum type (enum) for preserving the source of outputs + +2. **Proxy Composition** + - Delegates operations to sub-proxies + - Composes input and auto-cancellation types + - Preserves isolation and error handling + +3. **Run Function Pattern** + - Creates output subjects for each sub-transducer + - Runs sub-transducers concurrently + - Waits for completion and composes final output + +4. **Benefits of This Approach** + - Independent evolution of component transducers + - Reuse of existing transducer logic + - Separation of concerns between state management and composition + - Type-safe composition + +### Implementation Observations + +1. **Leaf Transducers** (A and B in the example) + - Simple and focused on specific state management + - Conform to `Transducer` protocol with `update` function + +2. **Composite Transducer** (TransducerC in the example) + - Conforms to `BaseTransducer` which only requires type definitions + - No need to implement an `update` function + - Delegates to component transducers through `run` + +3. **Proxy Implementation** + - Currently requires manual implementation + - Could benefit from a generic helper for common composition patterns + +4. **Output Handling** + - Sum types work well for preserving the source of outputs + - Forwarding outputs requires careful handling of isolation + +## Potential Improvements + +Based on our exploration, we've identified several potential improvements for Oak's composition capabilities: + +1. **Generic Composition Helpers** + ```swift + // Conceptual example of what could be added to Oak + struct ComposedProxy: TransducerProxy { + // Generic implementation for any two proxies + } + + func compose( + _ a: A.Type, + _ b: B.Type + ) -> some BaseTransducer { + // Return a composed transducer + } + ``` + +2. **Swift Macros** + - Could generate the boilerplate for composition + - Would reduce the risk of errors in manual composition + +3. **Special Composition Types** + - Sequential composition (chaining transducers) + - Parallel composition (as demonstrated) + - Hierarchical composition (state machines within state machines) + +4. **Stream Handling** + - Current example doesn't implement the `stream` property of the proxy + - A generic implementation would need to merge streams from sub-proxies + +## Usage Recommendations + +1. **When to Use Composition** + - When you have reusable transducer components + - When different parts of your state machine have different concerns + - When you want to break down complex state management into simpler pieces + +2. **Composition vs. Single Transducer** + - Use composition when parts of your state are logically separate + - Use a single transducer when state updates are tightly coupled + +3. **Testing Composed Transducers** + - Test each component transducer independently + - Test the composition with integration tests + +## Conclusion + +Oak's design principles support composition well, even without relying on actors. The type system and protocol-based approach create a solid foundation for building complex, composable state machines. With some additional helper utilities, Oak could make composition even more straightforward and less error-prone. diff --git a/Examples/Sources/Examples/Composition/TransducerComposition/README.md b/Examples/Sources/Examples/Composition/TransducerComposition/README.md new file mode 100644 index 0000000..e12066a --- /dev/null +++ b/Examples/Sources/Examples/Composition/TransducerComposition/README.md @@ -0,0 +1,84 @@ +# Transducer Composition in Oak + +This directory contains examples of how to compose transducers in the Oak framework using a generic protocol extension approach. + +## Overview + +Composition is a powerful technique for building complex systems from smaller, simpler parts. The `ComposableTransducer` implementation demonstrates how to compose two transducers into a new composite transducer, allowing you to build more complex state machines by combining simpler ones. + +## Types of Composition + +The implementation supports several composition strategies: + +1. **Parallel Composition**: Both transducers run concurrently, processing events independently +2. **Sequential Composition**: The second transducer starts after the first one completes, potentially using the output of the first as input +3. **Custom Composition**: Allows for specialized composition with user-defined behavior + +## Implementing Composition + +The implementation provides: + +1. A protocol extension for `BaseTransducer` that adds a `compose` method +2. A generic `CompositeTransducer` struct that handles the actual composition +3. Support for combining states, events, and outputs of the component transducers + +## Examples + +### Basic Usage + +```swift +// Define two simple transducers +enum CounterTransducer: Transducer { /* ... */ } +enum TextTransducer: Transducer { /* ... */ } + +// Compose them into a new transducer type +typealias ComposedTransducer = CompositeTransducer + +// Create initial state and proxy +let initialState = ComposedTransducer.State( + stateA: CounterTransducer.State(count: 0), + stateB: TextTransducer.State(text: "") +) +let proxy = ComposedTransducer.Proxy() + +// Run the composite transducer +let finalOutput = try await ComposedTransducer.run( + initialState: initialState, + proxy: proxy, + output: outputCallback +) +``` + +### Using the Extension Method + +```swift +// Create a composite transducer type using the extension method +let composedTransducerType = CounterTransducer.compose( + with: TextTransducer.self, + compositionType: .parallel +) +``` + +## Benefits of Composition + +1. **Reusability**: Compose existing transducers to create new functionality +2. **Separation of Concerns**: Each transducer can focus on its specific responsibility +3. **Incremental Development**: Build complex state machines by adding one transducer at a time +4. **Testability**: Test component transducers independently before testing the composition + +## Implementation Details + +The implementation leverages Swift's generics and protocol extensions to create a type-safe composition mechanism. It handles the delegation of events and the combination of outputs, while maintaining the proper state management for each component transducer. + +The composite transducer's proxy delegates to the proxies of the component transducers, forwarding events and managing cancellation appropriately. + +## Future Enhancements + +Potential enhancements to the composition mechanism could include: + +1. Support for composing more than two transducers +2. Enhanced sequential composition with better output-to-input mapping +3. Support for conditional composition based on runtime state +4. Performance optimizations for specific composition patterns + +See the example code in `TransducerCompositionExample.swift` for detailed usage examples. diff --git a/Examples/Sources/Examples/Composition/TransducerCompositionExample.swift b/Examples/Sources/Examples/Composition/TransducerCompositionExample.swift new file mode 100644 index 0000000..6ba2569 --- /dev/null +++ b/Examples/Sources/Examples/Composition/TransducerCompositionExample.swift @@ -0,0 +1,199 @@ +// Oak Example - TransducerCompositionExample.swift +// +// This example demonstrates how to compose transducers using both parallel and sequential composition +// strategies defined in the CompositeTransducerProtocol. It shows how to: +// - Create simple component transducers +// - Compose them using parallel composition +// - Compose them using sequential composition +// - Run the composed transducers + +import Foundation +import Oak + +// MARK: - Component Transducers + +/// A simple counter transducer that increments a count when it receives an increment event +enum CounterTransducer: Transducer { + struct State: NonTerminal { + var count: Int + } + + enum Event { + case increment + case reset + } + + typealias Output = Int + + static func update(_ state: inout State, event: Event) -> Output { + switch event { + case .increment: + state.count += 1 + case .reset: + state.count = 0 + } + return state.count + } + + static func initialOutput(initialState: State) -> Output? { + return initialState.count + } +} + +/// A transducer that takes numbers as input and doubles them +enum DoublerTransducer: Transducer { + struct State: NonTerminal { + // This transducer doesn't need internal state + } + + // The event for this transducer is an integer + typealias Event = Int + + // The output is also an integer, but doubled + typealias Output = Int + + static func update(_ state: inout State, event: Event) -> Output { + // Double the input value + return event * 2 + } +} + +// MARK: - Parallel Composition Example + +/// A transducer that combines two counters in parallel +enum ParallelCountersTransducer: ParallelCompositeTransducer { + typealias TransducerA = CounterTransducer + typealias TransducerB = CounterTransducer + typealias CompositionTypeMarker = DefaultParallelComposition + + // All other required types are defined by the ParallelCompositeTransducer protocol +} + +// MARK: - Sequential Composition Example + +/// A transducer that chains a counter and doubler in sequence +enum CounterThenDoublerTransducer: SequentialCompositeTransducer { + typealias TransducerA = CounterTransducer + typealias TransducerB = DoublerTransducer + typealias CompositionTypeMarker = DefaultSequentialComposition + + // This method defines how the counter's output gets converted to the doubler's input + static func convertOutput(_ output: TransducerA.Output) -> TransducerB.Event? { + // The output of CounterTransducer is an Int, which is exactly what DoublerTransducer expects + return output + } + + // All other required types are defined by the SequentialCompositeTransducer protocol +} + +// MARK: - Usage Examples + +/// Demonstrates how to use both types of transducer composition +struct TransducerCompositionExample { + /// Entry point function that demonstrates both composition types + static func runExample() async throws { + print("Starting Transducer Composition Example") + + try await demonstrateParallelComposition() + try await demonstrateSequentialComposition() + + print("Finished Transducer Composition Example") + } + + /// Demonstrates parallel composition of two counters + static func demonstrateParallelComposition() async throws { + print(" +=== Parallel Composition Example ===") + + // Create a proxy for sending events to the parallel counters + let proxy = ParallelCountersTransducer.Proxy() + + // Create a subject to observe outputs + let output = Subject() + + // Subscribe to outputs + output.subscribe { value in + switch value { + case .outputA(let countA): + print("Counter A: \(countA)") + case .outputB(let countB): + print("Counter B: \(countB)") + } + } + + // Initial state with both counters at 0 + let initialState = CompositeState( + stateA: CounterTransducer.State(count: 0), + stateB: CounterTransducer.State(count: 0) + ) + + // Start the transducer in a task + let task = Task { + try await ParallelCountersTransducer.run( + initialState: initialState, + proxy: proxy, + output: output + ) + } + + // Send events to both counters + proxy.send(.eventA(.increment)) + proxy.send(.eventB(.increment)) + proxy.send(.eventB(.increment)) + proxy.send(.eventA(.reset)) + proxy.send(.eventA(.increment)) + + // Cancel the task to stop the transducer + try await Task.sleep(nanoseconds: 1_000_000_000) + task.cancel() + try? await task.value + + print("Parallel composition demonstration completed") + } + + /// Demonstrates sequential composition of a counter followed by a doubler + static func demonstrateSequentialComposition() async throws { + print(" +=== Sequential Composition Example ===") + + // Create a proxy for sending events to the counter + let proxy = CounterThenDoublerTransducer.Proxy() + + // Create a subject to observe outputs + let output = Subject() + + // Subscribe to outputs + output.subscribe { value in + print("Doubled count: \(value)") + } + + // Initial state with counter at 0 + let initialState = CompositeState( + stateA: CounterTransducer.State(count: 0), + stateB: DoublerTransducer.State() + ) + + // Start the transducer in a task + let task = Task { + try await CounterThenDoublerTransducer.run( + initialState: initialState, + proxy: proxy, + output: output + ) + } + + // Send events to the counter, which will then be doubled + proxy.send(.increment) // Counter: 1 -> Doubler: 2 + proxy.send(.increment) // Counter: 2 -> Doubler: 4 + proxy.send(.increment) // Counter: 3 -> Doubler: 6 + proxy.send(.reset) // Counter: 0 -> Doubler: 0 + proxy.send(.increment) // Counter: 1 -> Doubler: 2 + + // Cancel the task to stop the transducer + try await Task.sleep(nanoseconds: 1_000_000_000) + task.cancel() + try? await task.value + + print("Sequential composition demonstration completed") + } +} diff --git a/README.md b/README.md index f48a708..e09455b 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,29 @@ enum CounterTransducer: EffectTransducer { - **Back pressure support**: In compositions, a connected output awaits readiness of the consumer - **Completion callbacks**: Handle transducer completion with type-safe callbacks - **Optional proxy parameters**: Simplified API with automatic proxy creation +- **Transducer composition**: Experimental support for composing transducers at the type level + +### Transducer Composition + +Oak includes experimental support for composing transducers at the type level. This advanced feature allows for the creation of composite state machines that maintain the type-safety guarantees of the component transducers. + +```swift +// Define transducer types +typealias NavigationTransducer = MyNavigationTransducer +typealias ContentTransducer = MyContentTransducer + +// Compose them at the type level +let AppTransducer = NavigationTransducer.compose(with: ContentTransducer.self) + +// Use the composed type +let initialState = AppTransducer.State( + stateA: NavigationTransducer.State.initial, + stateB: ContentTransducer.State.initial +) +let proxy = AppTransducer.Proxy() +``` + +This composition mechanism is still evolving, but it shows promise for building complex state management solutions with clean architectural boundaries. Various composition strategies (parallel, sequential, custom) are being explored to determine the most effective patterns for different use cases. ### TransducerView diff --git a/Sources/Oak/FSM/BaseTransducer.swift b/Sources/Oak/FSM/BaseTransducer.swift index 079d93d..587d762 100644 --- a/Sources/Oak/FSM/BaseTransducer.swift +++ b/Sources/Oak/FSM/BaseTransducer.swift @@ -2,24 +2,39 @@ /// state machine. It's the base type for all transducers that can be used /// in a finite state machine. It defines the types of events, states, outputs, /// and proxies that the transducer can use. +/// +/// BaseTransducer serves as a type container for composition without requiring an +/// implementation of `update` or `run` functions. This makes it ideal for: +/// +/// - Composing multiple transducers together where implementing an `update` function +/// wouldn't make sense for the composite +/// - Creating adapter or wrapper transducers that delegate to other transducers +/// - Building hierarchical state machines where leaf nodes implement full `Transducer` +/// conformance while parent nodes only compose and coordinate +/// +/// This separation between type definitions and behavior allows for more flexible +/// composition patterns, as composite transducers can focus on orchestration and delegation +/// rather than state mutation. public protocol BaseTransducer { - - /// The type of events that the transducer can process, aka the _Input_ of - /// the FSM. - associatedtype Event - + /// The type of the _State_ of the FSM. /// /// This is a type that conforms to the `Terminable` protocol, which means /// that it can be in a terminal state. The terminal state is a state in which /// the FSM cannot process any more events and cannot produce any more output. associatedtype State: Terminable - + + /// The type of events that the transducer can process, aka the _Input_ of + /// the FSM. + associatedtype Event + + // associatedtype TransducerOutput + /// The type of the input interface of the transducer proxy. /// /// This is used to send events to the transducer. typealias Input = Proxy.Input - + /// Part of the _Output_ of the FSM, which includes all non-effects. /// /// An output value will be produced by the transducer in every computation @@ -36,6 +51,18 @@ public protocol BaseTransducer { /// associatedtype Output + /// The type of the effect a transducer may return in its update function. + /// This is `Never` for non-effect transducers. + associatedtype Effect + + /// The type of the environment in which the transducer operates. + /// + /// The environment provides the necessary context for executing the transducer. + /// This allows the transducer to interact with the outside world in a controlled way. + /// + /// > Note: For non-effect transducers, its type is always `Void`. + associatedtype Env + /// The type of the transducer proxy. /// /// A proxy is required to execute a transducer to provide the input @@ -56,7 +83,7 @@ public protocol BaseTransducer { /// extremely fast, but if a transducer sends output to the subject, /// subscribers may block the processing of the event. associatedtype Proxy: TransducerProxy = Oak.Proxy - + /// This function needs to be defined and return a non-nil Output value /// to ensure correct behaviour of Moore type transducers. /// @@ -67,6 +94,91 @@ public protocol BaseTransducer { /// /// The default implementation returns `nil`. static func initialOutput(initialState: State) -> Output? + + /// Executes the Finite State Machine (FSM) by using the given storage as + /// as a reference to its state. The current value of the state is the + /// initial state of the FSM. + /// + /// The function `run(storage:proxy:env:output:systemActor:)` returns + /// when the transducer reaches a terminal state or when an error occurs. + /// + /// The proxy, or more specifically, the `Input` interface of the proxy, is used to + /// send events to the transducer. The output can be used to connect to other + /// components. This can also be another transducer. In this case, the output is + /// connected to the input interface of another transducer. + /// + /// - Parameter storage: A reference to a storage which is used by the transducer + /// to store its state. The storage must conform to the `Storage` protocol. + /// The storage is used to read and write the state of the transducer. + /// - Parameter proxy: The transducer proxy that provides the input interface + /// and an event buffer. + /// - Parameter env: The environment used for the transducer. + /// > Note: For non-effect transducers, its type is always `Void`. + /// - Parameter output: The subject to which the transducer's output will be + /// sent. + /// - Parameter systemActor: The isolation of the caller. + /// + /// > Note: State observation is not supported in this implementation of the + /// run function. + /// + /// - Returns: The final output produced by the transducer when the state + /// becomes terminal. + /// + /// - Throws: + /// - `TransducerError.noOutputProduced`: If no output is produced before reaching the terminal state. + /// - Other errors: If the transducer cannot execute its transition and output function as expected, + /// for example, when events could not be enqueued because of a full event buffer, + /// when the func `terminate()` is called on the proxy, or when the output value cannot be sent. + /// + @discardableResult + static func run( + storage: some Storage, + proxy: Proxy, + env: Env, + output: some Subject, + systemActor: isolated any Actor + ) async throws -> Output + + /// Executes the Finite State Machine (FSM) with the given initial state. + /// + /// The function `run(initialState:proxy:output:)` returns when the transducer + /// reaches a terminal state or when an error occurs. + /// + /// The proxy, or more specifically, the `Input` interface of the proxy, is used to + /// send events to the transducer. The output can be used to connect to other + /// components. This can also be another transducer. In this case, the output is + /// connected to the input interface of another transducer. + /// + /// - Parameter initialState: The initial state of the transducer. + /// - Parameter proxy: The transducer proxy that provides the input interface + /// and an event buffer. + /// - Parameter env: The environment used for the transducer. For non-effect + /// transducers, its type is always `Void`. This parameter exists for consistency + /// with `EffectTransducer` and to support composition patterns. + /// - Parameter output: The subject to which the transducer's output will be + /// sent. + /// - Parameter systemActor: The isolation of the caller. + /// + /// > Note: State observation is not supported in this implementation of the + /// run function. + /// + /// - Returns: The final output produced by the transducer when the state + /// becomes terminal. + /// + /// - Throws: + /// - `TransducerError.noOutputProduced`: If no output is produced before reaching the terminal state. + /// - Other errors: If the transducer cannot execute its transition and output function as expected, + /// for example, when events could not be enqueued because of a full event buffer, + /// when the func `terminate()` is called on the proxy, or when the output value cannot be sent. + /// + @discardableResult + static func run( + initialState: State, + proxy: Proxy, + env: Env, + output: some Subject, + systemActor: isolated any Actor + ) async throws -> Output } extension BaseTransducer { diff --git a/Sources/Oak/FSM/Composition/Binding.swift b/Sources/Oak/FSM/Composition/Binding.swift new file mode 100644 index 0000000..d3606ca --- /dev/null +++ b/Sources/Oak/FSM/Composition/Binding.swift @@ -0,0 +1,258 @@ +// +// Binding.swift +// Oak +// +// Created by Andreas Grosam on 08.08.25. +// See also: https://gist.github.com/AliSoftware/ecb5dfeaa7884fc0ce96178dfdd326f8 +#if false +// @propertyWrapper +@dynamicMemberLookup +public struct Binding { + + private let getValue: () -> Value + private let setValue: (Value) -> Void + + /// Creates a binding with closures that read and write the binding value. + /// + /// A binding conforms to Sendable only if its wrapped value type also + /// conforms to Sendable. It is always safe to pass a sendable binding + /// between different concurrency domains. However, reading from or writing + /// to a binding's wrapped value from a different concurrency domain may or + /// may not be safe, depending on how the binding was created. Oak will + /// issue a warning at runtime if it detects a binding being used in a way + /// that may compromise data safety. + /// + /// For a "computed" binding created using get and set closure parameters, + /// the safety of accessing its wrapped value from a different concurrency + /// domain depends on whether those closure arguments are isolated to + /// a specific actor. For example, a computed binding with closure arguments + /// that are known (or inferred) to be isolated to the main actor must only + /// ever access its wrapped value on the main actor as well, even if the + /// binding is also sendable. + /// + /// - Parameters: + /// - get: A closure that retrieves the binding value. The closure has no + /// parameters, and returns a value. + /// - set: A closure that sets the binding value. The closure has the + /// following parameter: + /// - newValue: The new value of the binding value. + public init( + get: @escaping () -> Value, + set: @escaping (Value) -> Void + ) { + self.setValue = set + self.getValue = get + } + + /// The underlying value referenced by the binding variable. + /// + /// This property provides primary access to the value's data. However, you + /// don't access `wrappedValue` directly. Instead, you use the property + /// variable created with the ``Binding`` attribute. + /// + /// When a mutable binding value changes, the new value is immediately + /// available. However, updates to a view displaying the value happens + /// asynchronously, so the view may not show the change immediately. + public var wrappedValue: Value { + get { self.getValue() } + nonmutating set { setValue(newValue) } + } + + /// A projection of the binding value that returns a binding. + /// + /// Use the projected value to pass a binding value down a hierarchy. + /// To get the `projectedValue`, prefix the property variable with `$`. + public var projectedValue: Binding { self } + + /// Creates a binding from the value of another binding. + public init(projectedValue: Binding) { + self.setValue = projectedValue.setValue + self.getValue = projectedValue.getValue + } + + /// Returns a binding to the resulting value of a given key path. + /// + /// - Parameter keyPath: A key path to a specific resulting value. + /// + /// - Returns: A new binding. + public subscript( + dynamicMember keyPath: WritableKeyPath + ) -> Binding { + Binding( + get: { self.wrappedValue[keyPath: keyPath] }, + set: { self.wrappedValue[keyPath: keyPath] = $0 } + ) + } +} + + +extension Binding { + func map(_ keyPath: WritableKeyPath) -> Binding { + Binding( + get: { self.wrappedValue[keyPath: keyPath] }, + set: { self.wrappedValue[keyPath: keyPath] = $0 } + ) + } +} + +/* +@dynamicMemberLookup protocol BindingConvertible { + associatedtype Value + + var binding: Binding { get } + + subscript(dynamicMember keyPath: WritableKeyPath) -> Binding { get } +} + +extension BindingConvertible { + public subscript(dynamicMember keyPath: WritableKeyPath) -> Binding { + return Binding( + get: { self.binding.wrappedValue[keyPath: keyPath] }, + set: { self.binding.wrappedValue[keyPath: keyPath] = $0 } + ) + } +} + +//: `XBinding` is one of those types on which we want that `@dynamicMemberLookup` feature: +extension Binding: BindingConvertible { + var binding: Binding { self } // well for something already a `Binding`, just use itself! +} +*/ + +#endif + + +#if false +extension Binding : Sequence where Value : MutableCollection { + + /// A type representing the sequence's elements. + public typealias Element = Binding + + /// A type that provides the sequence's iteration interface and + /// encapsulates its iteration state. + public typealias Iterator = IndexingIterator> + + /// A collection representing a contiguous subrange of this collection's + /// elements. The subsequence shares indices with the original collection. + /// + /// The default subsequence type for collections that don't define their own + /// is `Slice`. + public typealias SubSequence = Slice> +} + +extension Binding : Collection where Value : MutableCollection { + + /// A type that represents a position in the collection. + /// + /// Valid indices consist of the position of every element and a + /// "past the end" position that's not valid for use as a subscript + /// argument. + public typealias Index = Value.Index + + /// A type that represents the indices that are valid for subscripting the + /// collection, in ascending order. + public typealias Indices = Value.Indices + + /// The position of the first element in a nonempty collection. + /// + /// If the collection is empty, `startIndex` is equal to `endIndex`. + public var startIndex: Binding.Index { + } + + /// The collection's "past the end" position---that is, the position one + /// greater than the last valid subscript argument. + /// + /// When you need a range that includes the last element of a collection, use + /// the half-open range operator (`..<`) with `endIndex`. The `..<` operator + /// creates a range that doesn't include the upper bound, so it's always + /// safe to use with `endIndex`. For example: + /// + /// let numbers = [10, 20, 30, 40, 50] + /// if let index = numbers.firstIndex(of: 30) { + /// print(numbers[index ..< numbers.endIndex]) + /// } + /// // Prints "[30, 40, 50]" + /// + /// If the collection is empty, `endIndex` is equal to `startIndex`. + public var endIndex: Binding.Index { + } + + /// The indices that are valid for subscripting the collection, in ascending + /// order. + /// + /// A collection's `indices` property can hold a strong reference to the + /// collection itself, causing the collection to be nonuniquely referenced. + /// If you mutate the collection while iterating over its indices, a strong + /// reference can result in an unexpected copy of the collection. To avoid + /// the unexpected copy, use the `index(after:)` method starting with + /// `startIndex` to produce indices instead. + /// + /// var c = MyFancyCollection([10, 20, 30, 40, 50]) + /// var i = c.startIndex + /// while i != c.endIndex { + /// c[i] /= 5 + /// i = c.index(after: i) + /// } + /// // c == MyFancyCollection([2, 4, 6, 8, 10]) + public var indices: Value.Indices { + } + + /// Returns the position immediately after the given index. + /// + /// The successor of an index must be well defined. For an index `i` into a + /// collection `c`, calling `c.index(after: i)` returns the same index every + /// time. + /// + /// - Parameter i: A valid index of the collection. `i` must be less than + /// `endIndex`. + /// - Returns: The index value immediately after `i`. + public func index(after i: Binding.Index) -> Binding.Index + + /// Replaces the given index with its successor. + /// + /// - Parameter i: A valid index of the collection. `i` must be less than + /// `endIndex`. + public func formIndex(after i: inout Binding.Index) + + /// Accesses the element at the specified position. + /// + /// The following example accesses an element of an array through its + /// subscript to print its value: + /// + /// var streets = ["Adams", "Bryant", "Channing", "Douglas", "Evarts"] + /// print(streets[1]) + /// // Prints "Bryant" + /// + /// You can subscript a collection with any valid index other than the + /// collection's end index. The end index refers to the position one past + /// the last element of a collection, so it doesn't correspond with an + /// element. + /// + /// - Parameter position: The position of the element to access. `position` + /// must be a valid index of the collection that is not equal to the + /// `endIndex` property. + /// + /// - Complexity: O(1) + public subscript(position: Binding.Index) -> Binding.Element { + } +} + +extension Binding : BidirectionalCollection where Value : BidirectionalCollection, Value : MutableCollection { + + /// Returns the position immediately before the given index. + /// + /// - Parameter i: A valid index of the collection. `i` must be greater than + /// `startIndex`. + /// - Returns: The index value immediately before `i`. + public func index(before i: Binding.Index) -> Binding.Index + + /// Replaces the given index with its predecessor. + /// + /// - Parameter i: A valid index of the collection. `i` must be greater than + /// `startIndex`. + public func formIndex(before i: inout Binding.Index) +} + +extension Binding : RandomAccessCollection where Value : MutableCollection, Value : RandomAccessCollection { +} +#endif diff --git a/Sources/Oak/FSM/Composition/CompositeTransducerProtocol.swift b/Sources/Oak/FSM/Composition/CompositeTransducerProtocol.swift new file mode 100644 index 0000000..6b16ecc --- /dev/null +++ b/Sources/Oak/FSM/Composition/CompositeTransducerProtocol.swift @@ -0,0 +1,221 @@ +// Oak - CompositeTransducerProtocol.swift +// +// Defines the protocol for composite transducers that combine multiple transducers +// using different composition strategies. + +/// A type representing a composite event for parallel composition. +/// +/// This enum allows either component transducer to receive events independently. +public enum SumTypeEvent { + /// An event for the first component transducer + case eventA(EventA) + + /// An event for the second component transducer + case eventB(EventB) +} + +/// A type representing composite output from parallel composition. +/// +/// This enum allows tracking which component produced an output. +public enum SumTypeOutput { + /// Output from the first component transducer + case outputA(OutputA) + + /// Output from the second component transducer + case outputB(OutputB) + + public typealias Tuple = (OutputA, OutputB) +} + +/// A type representing the composite state of two transducers. +/// +/// This struct combines the states of both component transducers and +/// implements Terminable by considering both component states. +public struct ProductTypeState: Terminable { + /// The state of the first component transducer + public var stateA: StateA + + /// The state of the second component transducer + public var stateB: StateB + + /// Creates a new composite state + public init(stateA: StateA, stateB: StateB) { + self.stateA = stateA + self.stateB = stateB + } + + /// Returns true if either component state is terminal + public var isTerminal: Bool { + stateA.isTerminal || stateB.isTerminal + } +} + +/// A type representing the composite environment for two transducers. +/// +/// This struct combines the environments of both component transducers, +/// allowing each transducer to access its own environment. +public struct ProductTypeEnv { + /// The environment for the first component transducer + public var envA: EnvA + + /// The environment for the second component transducer + public var envB: EnvB + + /// Creates a new composite environment + public init(envA: EnvA, envB: EnvB) { + self.envA = envA + self.envB = envB + } +} + +/// Protocol for a composite proxy that manages proxies for multiple transducers +public protocol ProductTypeProxy { + /// The proxy type for the first component transducer + associatedtype ProxyA: TransducerProxy + + /// The proxy type for the second component transducer + associatedtype ProxyB: TransducerProxy + + /// Initialize a new composite proxy + init() + + /// Access to the proxy for the first component transducer + var proxyA: ProxyA { get } + + /// Access to the proxy for the second component transducer + var proxyB: ProxyB { get } +} + +/// A simple callback-based Subject implementation +struct SyncCallback: Subject { + let fn: (Value, isolated any Actor) async throws -> Void + + /// Initialises a `Callback` value with the given isolated throwing closure. + /// + /// - Parameter fn: An async throwing closure which will be called when `Self` + /// receives a value via its `send(_:)` function. + init(_ fn: @escaping (Value, isolated any Actor) async throws -> Void) { + self.fn = fn + } + + /// Send a value to `Self` which calls its callback clouser with the argument `value`. + /// - Parameter value: The value which is used as the argument to the callback closure. + /// - Parameter isolated: The "system actor" where this function is being called on. + func send( + _ value: Value, + isolated: isolated any Actor = #isolation + ) async throws { + try await fn(value, isolated) + } +} + +/// Protocol defining the requirements for a composite transducer. +/// +/// A composite transducer combines two transducers together using a specific +/// composition strategy (parallel or sequential). This protocol provides a minimal +/// interface, and the actual behavior is implemented in extensions specific to +/// each composition type. +public protocol CompositeTransducerProtocol: BaseTransducer { + /// The first component transducer + associatedtype TransducerA: BaseTransducer + + /// The second component transducer + associatedtype TransducerB: BaseTransducer + + /// The marker type for the composition strategy (parallel or sequential) + associatedtype CompositionTypeMarker: CompositionType + + /// The type of the environment used by the transducers. + /// By default, this is a CompositeEnv combining the environments of both component transducers. + /// Can be overridden to use a different environment type. + associatedtype Env = ProductTypeEnv +} + +/// Extension providing default implementation for parallel composition +extension CompositeTransducerProtocol where + CompositionTypeMarker: ParallelComposition, + State == ProductTypeState, + Event == SumTypeEvent, + Output == SumTypeOutput.Tuple, + Env == ProductTypeEnv, + TransducerA.Output: Sendable, + TransducerB.Output: Sendable, + TransducerA: BaseTransducer, + TransducerB: BaseTransducer { + + /// Run the parallel composite transducer + /// + /// In parallel composition, both component transducers run concurrently. + /// Events are dispatched to the appropriate component based on the SumTypeEvent type. + @discardableResult + public static func run

( + initialState: State, + proxy: P, + env: Env, + output: some Subject>, + systemActor: isolated any Actor = #isolation + ) async throws -> Output where P: ProductTypeProxy, P.ProxyA == TransducerA.Proxy, P.ProxyB == TransducerB.Proxy { + // Create output subjects that wrap the outputs from each component + let outputA = SyncCallback { valueA, actor in + guard actor === systemActor else { + return + } + nonisolated(unsafe) let output = output + try await output.send(.outputA(valueA), isolated: actor) + } + let outputB = SyncCallback { valueB, actor in + guard actor === systemActor else { + return + } + nonisolated(unsafe) let output = output + try await output.send(.outputB(valueB), isolated: actor) + } + + // Set up task to run transducer A + let transducerTaskA = Task { + return try await TransducerA.run( + initialState: initialState.stateA, + proxy: proxy.proxyA, + env: env.envA, + output: outputA, + systemActor: systemActor + ) + } + + // Set up task to run transducer B + let transducerTaskB = Task { + return try await TransducerB.run( + initialState: initialState.stateB, + proxy: proxy.proxyB, + env: env.envB, + output: outputB, + systemActor: systemActor + ) + } + + // Wait for both tasks to complete + do { + // We need to explicitly annotate these with 'let' to ensure proper task behavior + let outputAValue = try await transducerTaskA.value + let outputBValue = try await transducerTaskB.value + return (outputAValue, outputBValue) + } catch { + // Ensure all tasks are cancelled if there's an error + throw error + } + } + + + /// Provides the initial output for the composite transducer + /// + /// For parallel composition, if either component has an initial output, + /// we will return that (with preference to transducer A if both have initial outputs) + public static func initialOutput(initialState: State) -> SumTypeOutput? { + if let outputA = TransducerA.initialOutput(initialState: initialState.stateA) { + return .outputA(outputA) + } else if let outputB = TransducerB.initialOutput(initialState: initialState.stateB) { + return .outputB(outputB) + } + return nil + } +} diff --git a/Sources/Oak/FSM/Composition/CompositionType.swift b/Sources/Oak/FSM/Composition/CompositionType.swift new file mode 100644 index 0000000..e10b8a1 --- /dev/null +++ b/Sources/Oak/FSM/Composition/CompositionType.swift @@ -0,0 +1,45 @@ +// Oak - CompositionType.swift +// +// Defines marker protocols for different composition strategies. + +import Foundation + +/// Base protocol for all composition type markers. +/// +/// This protocol serves as a common base for all composition type markers, +/// allowing `CompositeTransducer` to constrain its `CompositionType` generic parameter. +public protocol CompositionType {} + +/// Marker protocol for parallel composition. +/// +/// In parallel composition: +/// - Both transducers receive their respective events independently +/// - Both produce outputs independently +/// - Outputs are combined when both transducers produce output simultaneously +/// - Either transducer can independently affect the composite output +public protocol ParallelComposition: CompositionType {} + +/// Marker protocol for sequential composition. +/// +/// In sequential composition: +/// - The first transducer processes events first +/// - Its output is transformed and fed as input to the second transducer +/// - The final output comes from the second transducer +public protocol SequentialComposition: CompositionType {} + +// Default implementations for the composition type markers +public struct DefaultParallelComposition: ParallelComposition {} +public struct DefaultSequentialComposition: SequentialComposition {} + +/// Factory struct to create composition types +public struct TransducerComposition { + /// Get the parallel composition type + public static var parallel: DefaultParallelComposition.Type { + DefaultParallelComposition.self + } + + /// Get the sequential composition type + public static var sequential: DefaultSequentialComposition.Type { + DefaultSequentialComposition.self + } +} \ No newline at end of file diff --git a/Sources/Oak/FSM/EffectTransducer.swift b/Sources/Oak/FSM/EffectTransducer.swift index 72fa010..f288d7f 100644 --- a/Sources/Oak/FSM/EffectTransducer.swift +++ b/Sources/Oak/FSM/EffectTransducer.swift @@ -71,22 +71,20 @@ /// to represent the transducer. The transducer is a pure function that can be /// executed in an asynchronous context, and it can be used to process events and /// produce output. -public protocol EffectTransducer: BaseTransducer { - +public protocol EffectTransducer: BaseTransducer where Effect == Oak.Effect { + + /// The _Output_ of the FSM, which may include an optional + /// effect and a value type, `Output`. Typically, for Effect- + /// Transducers it is either `Effect?` or the tuple `(Effect?, Output)`. + /// For non-effect transducers, it is simply `Output`. + associatedtype TransducerOutput + associatedtype Output = Void - + /// The type of the environment in which the transducer operates and which /// provides the necessary context for executing effects. associatedtype Env = Void - /// The _Output_ of the FSM, which may include an optional - /// effect and a value type, `Output`. Typically, it it is either - /// `Effect?` or the tuple `(Effect?, Output)`. - associatedtype TransducerOutput - - /// The concrete type of the effect which performs side effects. - typealias Effect = Oak.Effect - /// A pure function that combines the _transition_ and the _output_ function /// of the finite state machine (FSM) into a single function. /// @@ -101,15 +99,23 @@ public protocol EffectTransducer: BaseTransducer { static func update(_ state: inout State, event: Event) -> TransducerOutput } -extension EffectTransducer { +/// Required for protocol conformance +extension EffectTransducer where TransducerOutput == (Effect?, Output) { + + @inline(__always) + public static func compute(_ state: inout State, event: Event) -> (Effect?, Output) { + update(&state, event: event) + } - package static func run( + @_disfavoredOverload + @discardableResult + public static func run( storage: some Storage, - proxy: Proxy, + proxy: Proxy = Proxy(), env: Env, output: some Subject, systemActor: isolated any Actor = #isolation - ) async throws -> Output where TransducerOutput == (Effect?, Output) { + ) async throws -> Output { try proxy.checkInUse() try Task.checkCancellation() let stream = proxy.stream @@ -139,7 +145,7 @@ extension EffectTransducer { var nextEvent: Event? = event while let event = nextEvent { let effect: Effect? - (effect, outputValue) = Self.update(&storage.value, event: event) + (effect, outputValue) = Self.compute(&storage.value, event: event) try await output.send(outputValue!, isolated: systemActor) if let effect { let moreEvents = try await execute( @@ -225,16 +231,138 @@ extension EffectTransducer { nonisolated(unsafe) let res = result return res } + + // /// Executes the Finite State Machine (FSM) with the given initial state. + // /// + // /// This overload of `run` is specialized for transducers where + // /// `TransducerOutput == (Effect?, Output)`. + // /// + // /// The function `run(initialState:proxy:output:)` returns when the transducer + // /// reaches a terminal state or when an error occurs. + // /// + // /// The proxy, or more specifically, the `Input` interface of the proxy, is used to + // /// send events to the transducer. The output can be used to connect to other + // /// components. This can also be another transducer. In this case, the output is + // /// connected to the input interface of another transducer. + // /// + // /// - Parameter initialState: The initial state of the transducer. + // /// - Parameter proxy: The transducer proxy that provides the input interface + // /// and an event buffer. + // /// - Parameter env: The environment in which the transducer operates and which + // /// provides the necessary context for executing effects. + // /// - Parameter output: The subject to which the transducer's output will be + // /// sent. + // /// - Parameter systemActor: The actor isolation context in which the transducer + // /// operates. This parameter allows the caller to specify the actor context + // /// for isolation, ensuring thread safety and correct actor execution semantics + // /// when running the transducer. The default value `#isolation` uses the + // /// current actor context. + // /// + // /// - Returns: The final output produced by the transducer when the state + // /// became terminal. + // /// - Throws: An error if the transducer cannot execute its transition and + // /// output function as expected. For example, if the initial state is + // /// terminal, or if no output is produced, or when events could not be + // /// enqueued because of a full event buffer, or when the func `terminate()` + // /// is called on the proxy, or when the output value cannot be sent. + // /// + // /// > Note: State observation is not supported in this implementation of the + // /// run function. + // /// + // /// Specialization for transducers where `TransducerOutput == (Effect?, Output)`. + // /// This overload is the public entry point for running a transducer with output emission. + // /// - Note: The constraint `TransducerOutput == (Effect?, Output)` is required for this overload. + // /// - See documentation above for details on this specialization. + + @_disfavoredOverload + @discardableResult + public static func run( + initialState: State, + proxy: Proxy = Proxy(), + env: Env, + output: some Subject, + systemActor: isolated any Actor = #isolation + ) async throws -> Output { + try await Self.run( + storage: LocalStorage(value: initialState), + proxy: proxy, + env: env, + output: output, + systemActor: systemActor + ) + } + + // /// Executes the Finate State Machine (FSM) with the given initial state. + // /// + // /// This overload of `run` is specialized for transducers where + // /// `TransducerOutput == (Effect?, Output)`. + // /// + // /// The function `run(initialState:proxy:output:)` returns when the transducer + // /// reaches a terminal state or when an error occurs. + // /// + // /// The proxy, or more specically, the `Input` interface of the proxy, is used to + // /// send events to the transducer. The output can be used to connect to other + // /// components. This can also be another transducer. In this case, the output is + // /// connected to the input interface of another transducer. + // /// + // /// - Parameter initialState: The initial state of the transducer. + // /// - Parameter proxy: The transducer proxy that provides the input interface + // /// and an event buffer. + // /// - Parameter env: The environment in which the transducer operates and which + // /// provides the necessary context for executing effects. + // /// - Parameter systemActor: The actor isolation context in which the transducer + // /// operates. This parameter allows the caller to specify the actor context + // /// for isolation, ensuring thread safety and correct actor execution semantics + // /// when running the transducer. The default value `#isolation` uses the + // /// current actor context. + // /// + // /// - Throws: An error if the transducer cannot execute its transition and + // /// output function as expected. For example, if the initial state is + // /// terminal, or if no output is produced, or when events could not be + // /// equeued because of a full event buffer, or when the func `terminate()` + // /// is called on the proxy, or when the output value cannot be sent. + // /// + // /// > Note: State observation is not supported in this implementation of the + // /// run function. + // /// + // /// Specialization for transducers where `TransducerOutput == (Effect?, Output)`. + // /// This overload is used when no output emission is required. + // /// - Note: The constraint `TransducerOutput == (Effect?, Output)` is required for this overload. + // /// - See documentation above for details on this specialization. + // public static func run( + // initialState: State, + // proxy: Proxy, + // env: Env, + // systemActor: isolated any Actor = #isolation + // ) async throws { + // _ = try await Self.run( + // storage: LocalStorage(value: initialState), + // proxy: proxy, + // env: env, + // output: NoCallback(), + // systemActor: systemActor + // ) + // } + } -extension EffectTransducer { +/// Required for protocol conformance +extension EffectTransducer where TransducerOutput == Effect?, Output == Void { + + @inline(__always) + public static func compute(_ state: inout State, event: Event) -> (Effect?, Output) { + (update(&state, event: event), Void()) + } - package static func run( + @_disfavoredOverload + @discardableResult + public static func run( storage: some Storage, - proxy: Proxy, + proxy: Proxy = Proxy(), env: Env, + output: some Subject, systemActor: isolated any Actor = #isolation - ) async throws where TransducerOutput == Effect?, Output == Void { + ) async throws -> Output { try proxy.checkInUse() try Task.checkCancellation() let stream = proxy.stream @@ -252,7 +380,7 @@ extension EffectTransducer { try Task.checkCancellation() var nextEvent: Event? = event while let event = nextEvent { - let effect = Self.update(&storage.value, event: event) + let (effect, _) = Self.compute(&storage.value, event: event) if let effect { let moreEvents = try await execute( effect, @@ -309,15 +437,15 @@ extension EffectTransducer { // cancelled. Iff there should be running effects, we eagerly cancel // them all: context.cancellAllTasks() - + // Iff the current task has been cancelled, we do still reach here. In // this case, the transducer may have been interupted being in a non- // terminal state and the event buffer may still containing unprocessed // events. We do explicitly throw a `CancellationError` to indicate // this fact: try Task.checkCancellation() - - #if DEBUG + +#if DEBUG // Here, the event buffer may still have events in it, but the transducer // has finished processing. These events have been successfull enqueued, // and no error indicates this fact. In DEBUG we log these unprocessed @@ -329,60 +457,53 @@ extension EffectTransducer { ) ignoreCount += 1 } - #endif +#endif + return Void() } -} -extension EffectTransducer { + // /// Executes the Finite State Machine (FSM) with the given initial state. + // /// + // /// This overload of `run` is specialized for transducers where + // /// `TransducerOutput == Effect?`. + // /// + // /// The function `run(initialState:proxy:output:)` returns when the transducer + // /// reaches a terminal state or when an error occurs. + // /// + // /// The proxy, or more specifically, the `Input` interface of the proxy, is used to + // /// send events to the transducer. The output can be used to connect to other + // /// components. This can also be another transducer. In this case, the output is + // /// connected to the input interface of another transducer. + // /// + // /// - Parameter initialState: The initial state of the transducer. + // /// - Parameter proxy: The transducer proxy that provides the input interface + // /// and an event buffer. + // /// - Parameter env: The environment in which the transducer operates and which + // /// provides the necessary context for executing effects. + // /// - Parameter systemActor: The actor isolation context in which the transducer + // /// operates. This parameter allows the caller to specify the actor context + // /// for isolation, ensuring thread safety and correct actor execution semantics + // /// when running the transducer. The default value `#isolation` uses the + // /// current actor context. + // /// + // /// - Throws: An error if the transducer cannot execute its transition and + // /// output function as expected. For example, if the initial state is + // /// terminal, or if no output is produced, or when events could not be + // /// enqueued because of a full event buffer, or when the func `terminate()` + // /// is called on the proxy, or when the output value cannot be sent. + // /// + // /// > Note: State observation is not supported in this implementation of the + // /// run function. - /// Executes the Finite State Machine (FSM) with the given initial state. - /// - /// This overload of `run` is specialized for transducers where - /// `TransducerOutput == (Effect?, Output)`. - /// - /// The function `run(initialState:proxy:output:)` returns when the transducer - /// reaches a terminal state or when an error occurs. - /// - /// The proxy, or more specifically, the `Input` interface of the proxy, is used to - /// send events to the transducer. The output can be used to connect to other - /// components. This can also be another transducer. In this case, the output is - /// connected to the input interface of another transducer. - /// - /// - Parameter initialState: The initial state of the transducer. - /// - Parameter proxy: The transducer proxy that provides the input interface - /// and an event buffer. - /// - Parameter env: The environment in which the transducer operates and which - /// provides the necessary context for executing effects. - /// - Parameter output: The subject to which the transducer's output will be - /// sent. - /// - Parameter systemActor: The actor isolation context in which the transducer - /// operates. This parameter allows the caller to specify the actor context - /// for isolation, ensuring thread safety and correct actor execution semantics - /// when running the transducer. The default value `#isolation` uses the - /// current actor context. - /// - /// - Returns: The final output produced by the transducer when the state - /// became terminal. - /// - Throws: An error if the transducer cannot execute its transition and - /// output function as expected. For example, if the initial state is - /// terminal, or if no output is produced, or when events could not be - /// enqueued because of a full event buffer, or when the func `terminate()` - /// is called on the proxy, or when the output value cannot be sent. - /// - /// > Note: State observation is not supported in this implementation of the - /// run function. - /// - /// Specialization for transducers where `TransducerOutput == (Effect?, Output)`. - /// This overload is the public entry point for running a transducer with output emission. - /// - Note: The constraint `TransducerOutput == (Effect?, Output)` is required for this overload. - /// - See documentation above for details on this specialization. + @_disfavoredOverload + @discardableResult public static func run( initialState: State, - proxy: Proxy, + proxy: Proxy = Proxy(), env: Env, output: some Subject, systemActor: isolated any Actor = #isolation - ) async throws -> Output where TransducerOutput == (Effect?, Output) { + ) async throws -> Output { + // Note: currently, this will call the overload which is NOT handling an output, in case Output == Void try await Self.run( storage: LocalStorage(value: initialState), proxy: proxy, @@ -391,111 +512,79 @@ extension EffectTransducer { systemActor: systemActor ) } +} + +/// Convenience +extension EffectTransducer where TransducerOutput == Effect?, Output == Void { + + public static func run( + storage: some Storage, + proxy: Proxy = Proxy(), + env: Env, + systemActor: isolated any Actor = #isolation + ) async throws { + try await Self.run( + storage: storage, + proxy: proxy, + env: env, + output: NoCallback(), + systemActor: systemActor + ) + } - /// Executes the Finate State Machine (FSM) with the given initial state. - /// - /// This overload of `run` is specialized for transducers where - /// `TransducerOutput == (Effect?, Output)`. - /// - /// The function `run(initialState:proxy:output:)` returns when the transducer - /// reaches a terminal state or when an error occurs. - /// - /// The proxy, or more specically, the `Input` interface of the proxy, is used to - /// send events to the transducer. The output can be used to connect to other - /// components. This can also be another transducer. In this case, the output is - /// connected to the input interface of another transducer. - /// - /// - Parameter initialState: The initial state of the transducer. - /// - Parameter proxy: The transducer proxy that provides the input interface - /// and an event buffer. - /// - Parameter env: The environment in which the transducer operates and which - /// provides the necessary context for executing effects. - /// - Parameter systemActor: The actor isolation context in which the transducer - /// operates. This parameter allows the caller to specify the actor context - /// for isolation, ensuring thread safety and correct actor execution semantics - /// when running the transducer. The default value `#isolation` uses the - /// current actor context. - /// - /// - Throws: An error if the transducer cannot execute its transition and - /// output function as expected. For example, if the initial state is - /// terminal, or if no output is produced, or when events could not be - /// equeued because of a full event buffer, or when the func `terminate()` - /// is called on the proxy, or when the output value cannot be sent. - /// - /// > Note: State observation is not supported in this implementation of the - /// run function. - /// - /// Specialization for transducers where `TransducerOutput == (Effect?, Output)`. - /// This overload is used when no output emission is required. - /// - Note: The constraint `TransducerOutput == (Effect?, Output)` is required for this overload. - /// - See documentation above for details on this specialization. public static func run( initialState: State, - proxy: Proxy, + proxy: Proxy = Proxy(), env: Env, systemActor: isolated any Actor = #isolation - ) async throws where TransducerOutput == (Effect?, Output) { - _ = try await Self.run( + ) async throws { + try await Self.run( storage: LocalStorage(value: initialState), proxy: proxy, env: env, - output: NoCallback(), + output: NoCallback(), systemActor: systemActor ) } - } -extension EffectTransducer { +extension EffectTransducer where TransducerOutput == (Effect?, Output) { + + @discardableResult + public static func run( + storage: some Storage, + proxy: Proxy = Proxy(), + env: Env, + systemActor: isolated any Actor = #isolation + ) async throws -> Output { + try await Self.run( + storage: storage, + proxy: proxy, + env: env, + output: NoCallback(), + systemActor: systemActor + ) + } - /// Executes the Finite State Machine (FSM) with the given initial state. - /// - /// This overload of `run` is specialized for transducers where - /// `TransducerOutput == Effect?`. - /// - /// The function `run(initialState:proxy:output:)` returns when the transducer - /// reaches a terminal state or when an error occurs. - /// - /// The proxy, or more specifically, the `Input` interface of the proxy, is used to - /// send events to the transducer. The output can be used to connect to other - /// components. This can also be another transducer. In this case, the output is - /// connected to the input interface of another transducer. - /// - /// - Parameter initialState: The initial state of the transducer. - /// - Parameter proxy: The transducer proxy that provides the input interface - /// and an event buffer. - /// - Parameter env: The environment in which the transducer operates and which - /// provides the necessary context for executing effects. - /// - Parameter systemActor: The actor isolation context in which the transducer - /// operates. This parameter allows the caller to specify the actor context - /// for isolation, ensuring thread safety and correct actor execution semantics - /// when running the transducer. The default value `#isolation` uses the - /// current actor context. - /// - /// - Throws: An error if the transducer cannot execute its transition and - /// output function as expected. For example, if the initial state is - /// terminal, or if no output is produced, or when events could not be - /// enqueued because of a full event buffer, or when the func `terminate()` - /// is called on the proxy, or when the output value cannot be sent. - /// - /// > Note: State observation is not supported in this implementation of the - /// run function. - /// + + @discardableResult public static func run( initialState: State, - proxy: Proxy, + proxy: Proxy = Proxy(), env: Env, systemActor: isolated any Actor = #isolation - ) async throws where TransducerOutput == Effect?, Output == Void { + ) async throws -> Output { try await Self.run( storage: LocalStorage(value: initialState), proxy: proxy, env: env, + output: NoCallback(), systemActor: systemActor ) } } + extension EffectTransducer { private static func execute( diff --git a/Sources/Oak/FSM/OutputAdapter.swift b/Sources/Oak/FSM/OutputAdapter.swift new file mode 100644 index 0000000..f26ae56 --- /dev/null +++ b/Sources/Oak/FSM/OutputAdapter.swift @@ -0,0 +1,31 @@ +// Oak - OutputAdapter.swift +// +// Defines the adapter for transforming outputs between transducers. + +import Foundation + +/// Helper type to adapt outputs between transducers. +/// +/// OutputAdapter provides a mechanism to transform outputs from one type to another, +/// which is essential for composite transducers where component outputs need to be +/// converted to the composite output type. +public struct OutputAdapter { + /// The transformation function that converts from any type to the desired output type + private let transformClosure: (Any) -> Any? + + /// Initializes a new output adapter with a transformation function + /// - Parameter transform: The function that transforms inputs to outputs + public init(transform: @escaping (I) -> O?) { + self.transformClosure = { input in + guard let typedInput = input as? I else { return nil } + return transform(typedInput) + } + } + + /// Transforms a value from one type to another + /// - Parameter value: The value to transform + /// - Returns: The transformed value, or nil if transformation failed + public func transform(_ value: Any) -> Any? { + return transformClosure(value) + } +} diff --git a/Sources/Oak/FSM/Storage.swift b/Sources/Oak/FSM/Storage.swift index a910e35..c855b6f 100644 --- a/Sources/Oak/FSM/Storage.swift +++ b/Sources/Oak/FSM/Storage.swift @@ -7,6 +7,7 @@ public protocol Storage { associatedtype Value var value: Value { get nonmutating set } + } internal struct LocalStorage: Storage { diff --git a/Sources/Oak/FSM/Subject.swift b/Sources/Oak/FSM/Subject.swift index 1de7694..1a51ecb 100644 --- a/Sources/Oak/FSM/Subject.swift +++ b/Sources/Oak/FSM/Subject.swift @@ -62,3 +62,4 @@ public protocol Subject { /// - Throws: When the value could not be delivered to the destination, it throws an error. func send(_ value: sending Value, isolated: isolated any Actor) async throws } + diff --git a/Sources/Oak/FSM/Transducer.swift b/Sources/Oak/FSM/Transducer.swift index aff87f0..fc74196 100644 --- a/Sources/Oak/FSM/Transducer.swift +++ b/Sources/Oak/FSM/Transducer.swift @@ -110,12 +110,12 @@ /// In the example above, once the FSM receives an event `start` it also /// will terminate and the asynchronous `run()` will return. -public protocol Transducer: BaseTransducer { - +public protocol Transducer: BaseTransducer where Effect == Never, Env == Void { + associatedtype Event associatedtype State associatedtype Output = Void - + /// A pure function that combines the _transition_ and the _output_ function /// of the finite state machine (FSM) into a single function. /// @@ -130,13 +130,20 @@ public protocol Transducer: BaseTransducer { } extension Transducer { + + @inline(__always) + static func compute(_ state: inout State, event: Event) -> (Effect?, Output) { + (.none, update(&state, event: event)) + } - package static func run( + @discardableResult + public static func run( storage: some Storage, - proxy: Proxy, + proxy: Proxy = Proxy(), + env: Env = (), output: some Subject, systemActor: isolated any Actor = #isolation - ) async throws -> Output where Proxy.Event == Event { + ) async throws -> Output { try proxy.checkInUse() try Task.checkCancellation() let stream = proxy.stream @@ -156,7 +163,7 @@ extension Transducer { do { loop: for try await event in stream { try Task.checkCancellation() - let outputValue = Self.update(&storage.value, event: event) + let (_, outputValue) = Self.compute(&storage.value, event: event) try await output.send(outputValue, isolated: systemActor) try Task.checkCancellation() if storage.value.isTerminal { @@ -216,90 +223,123 @@ extension Transducer { } return result } -} - -extension Transducer { - - /// Executes the Finite State Machine (FSM) with the given initial state. - /// - /// The function `run(initialState:proxy:output:)` returns when the transducer - /// reaches a terminal state or when an error occurs. - /// - /// The proxy, or more specifically, the `Input` interface of the proxy, is used to - /// send events to the transducer. The output can be used to connect to other - /// components. This can also be another transducer. In this case, the output is - /// connected to the input interface of another transducer. - /// - /// - Parameter initialState: The initial state of the transducer. - /// - Parameter proxy: The transducer proxy that provides the input interface - /// and an event buffer. - /// - Parameter output: The subject to which the transducer's output will be - /// sent. - /// - Parameter systemActor: The isolation of the caller. - /// - /// > Note: State observation is not supported in this implementation of the - /// run function. - /// - /// - Returns: The final output produced by the transducer when the state - /// becomes terminal. - /// - /// - Throws: - /// - `TransducerError.noOutputProduced`: If no output is produced before reaching the terminal state. - /// - Other errors: If the transducer cannot execute its transition and output function as expected, - /// for example, when events could not be enqueued because of a full event buffer, - /// when the func `terminate()` is called on the proxy, or when the output value cannot be sent. - /// + @discardableResult public static func run( initialState: State, - proxy: Proxy, + proxy: Proxy = Proxy(), + env: Env = (), output: some Subject, systemActor: isolated any Actor = #isolation ) async throws -> Output { return try await Self.run( storage: LocalStorage(value: initialState), proxy: proxy, + env: env, output: output, systemActor: systemActor ) } - /// Executes the Finite State Machine (FSM) with the given initial state. - /// - /// The function `run(initialState:proxy:)` returns when the transducer - /// reaches a terminal state or when an error occurs. - /// - /// The proxy, or more specifically, the `Input` interface of the proxy, is used to - /// send events to the transducer. The output can be used to connect to other - /// components. This can also be another transducer. In this case, the output is - /// connected to the input interface of another transducer. - /// - /// - Parameter initialState: The initial state of the transducer. - /// - Parameter proxy: The transducer proxy that provides the input interface - /// and an event buffer. - /// - Parameter systemActor: The isolation of the caller. - /// - /// > Note: State observation is not supported in this implementation of the - /// run function. - /// - /// - Throws: An error if the transducer cannot execute its transition and - /// output function as expected. For example, if the initial state is - /// terminal, or if no output is produced, or when events could not be - /// enqueued because of a full event buffer, or when the func `terminate()` - /// is called on the proxy, or when the output value cannot be sent. - /// - public static func run( - initialState: State, - proxy: Proxy, - systemActor: isolated any Actor = #isolation - ) async throws where Output == Void { - try await Self.run( - storage: LocalStorage(value: initialState), - proxy: proxy, - output: NoCallback(), - systemActor: systemActor - ) - } +} + +extension Transducer { + + // /// Executes the Finite State Machine (FSM) with the given initial state. + // /// + // /// The function `run(initialState:proxy:output:)` returns when the transducer + // /// reaches a terminal state or when an error occurs. + // /// + // /// The proxy, or more specifically, the `Input` interface of the proxy, is used to + // /// send events to the transducer. The output can be used to connect to other + // /// components. This can also be another transducer. In this case, the output is + // /// connected to the input interface of another transducer. + // /// + // /// - Parameter initialState: The initial state of the transducer. + // /// - Parameter proxy: The transducer proxy that provides the input interface + // /// and an event buffer. + // /// - Parameter env: The environment used for the transducer. For non-effect + // /// transducers, its type is always `Void`. This parameter exists for consistency + // /// with `EffectTransducer` and to support composition patterns. + // /// - Parameter output: The subject to which the transducer's output will be + // /// sent. + // /// - Parameter systemActor: The isolation of the caller. + // /// + // /// > Note: State observation is not supported in this implementation of the + // /// run function. + // /// + // /// - Returns: The final output produced by the transducer when the state + // /// becomes terminal. + // /// + // /// - Throws: + // /// - `TransducerError.noOutputProduced`: If no output is produced before reaching the terminal state. + // /// - Other errors: If the transducer cannot execute its transition and output function as expected, + // /// for example, when events could not be enqueued because of a full event buffer, + // /// when the func `terminate()` is called on the proxy, or when the output value cannot be sent. + // /// + // @discardableResult + // public static func run( + // initialState: State, + // proxy: Proxy = Proxy(), + // env: Env = (), + // output: some Subject, + // systemActor: isolated any Actor = #isolation + // ) async throws -> Output { + // return try await Self.run( + // storage: LocalStorage(value: initialState), + // proxy: proxy, + // env: env, + // output: output, + // systemActor: systemActor + // ) + // } + + // /// Executes the Finite State Machine (FSM) with the given initial state. + // /// + // /// The function `run(initialState:proxy:output:)` returns when the transducer + // /// reaches a terminal state or when an error occurs. + // /// + // /// The proxy, or more specifically, the `Input` interface of the proxy, is used to + // /// send events to the transducer. The output can be used to connect to other + // /// components. This can also be another transducer. In this case, the output is + // /// connected to the input interface of another transducer. + // /// + // /// - Parameter initialState: The initial state of the transducer. + // /// - Parameter proxy: The transducer proxy that provides the input interface + // /// and an event buffer. + // /// - Parameter env: The environment used for the transducer. For non-effect + // /// transducers, its type is always `Void`. This parameter exists for consistency + // /// with `EffectTransducer` and to support composition patterns. + // /// - Parameter systemActor: The isolation of the caller. + // /// + // /// > Note: State observation is not supported in this implementation of the + // /// run function. + // /// + // /// - Returns: The final output produced by the transducer when the state + // /// becomes terminal. + // /// + // /// - Throws: + // /// - `TransducerError.noOutputProduced`: If no output is produced before reaching the terminal state. + // /// - Other errors: If the transducer cannot execute its transition and output function as expected, + // /// for example, when events could not be enqueued because of a full event buffer, + // /// when the func `terminate()` is called on the proxy, or when the output value cannot be sent. + // /// + // @discardableResult + // public static func run( + // initialState: State, + // proxy: Proxy, + // env: Env = (), + // systemActor: isolated any Actor = #isolation + // ) async throws -> Output { + // return try await Self.run( + // storage: LocalStorage(value: initialState), + // proxy: proxy, + // env: env, + // output: NoCallback(), + // systemActor: systemActor + // ) + // } + } extension Transducer { @@ -318,6 +358,7 @@ extension Transducer { /// - Parameter host: The host providing the backing store for the state. /// - Parameter proxy: The transducer proxy that provides the input interface /// and an event buffer. + /// with `EffectTransducer` and to support composition patterns. /// - Parameter output: The subject to which the transducer's output will be /// sent. /// - Parameter systemActor: The isolation of the caller. @@ -332,13 +373,14 @@ extension Transducer { public static func run( state: ReferenceWritableKeyPath, host: Host, - proxy: Proxy, + proxy: Proxy = Proxy(), output: some Subject, systemActor: isolated any Actor = #isolation ) async throws -> Output { try await run( storage: ReferenceKeyPathStorage(host: host, keyPath: state), proxy: proxy, + env: Void(), output: output, systemActor: systemActor ) @@ -357,6 +399,8 @@ extension Transducer { /// - state: A reference-writeable key path to the state. /// - host: The host providing the backing store for the state. /// - proxy: The proxy, that will be associated to the transducer as its agent. + /// transducers, its type is always `Void`. This parameter exists for consistency + /// with `EffectTransducer` and to support composition patterns. /// - Returns: The output, that has been generated when the transducer reaches a terminal state. /// - Warning: The backing store for the state variable must not be mutated by the caller or must not be used with any other transducer. /// - Throws: Throws an error indicating the reason, for example, when the Swift Task, where the @@ -366,14 +410,105 @@ extension Transducer { public static func run( state: ReferenceWritableKeyPath, host: Host, - proxy: Proxy, - isolated: isolated any Actor = #isolation, + proxy: Proxy = Proxy(), + isolated: isolated any Actor = #isolation ) async throws -> Output { try await run( storage: ReferenceKeyPathStorage(host: host, keyPath: state), proxy: proxy, + env: Void(), output: NoCallback() ) } } + +// MARK: Convenient Function when no output parameter is given + +extension Transducer { + + /// Executes the Finite State Machine (FSM) by using the given storage as + /// as a reference to its state. The current value of the state is the + /// initial state of the FSM. + /// + /// The function `run(storage:proxy:env:output:systemActor:)` returns + /// when the transducer reaches a terminal state or when an error occurs. + /// + /// The proxy, or more specifically, the `Input` interface of the proxy, is used to + /// send events to the transducer. The output can be used to connect to other + /// components. This can also be another transducer. In this case, the output is + /// connected to the input interface of another transducer. + /// + /// - Parameter storage: A reference to a storage which is used by the transducer + /// to store its state. The storage must conform to the `Storage` protocol. + /// The storage is used to read and write the state of the transducer. + /// - Parameter proxy: The transducer proxy that provides the input interface + /// and an event buffer. + /// - Parameter systemActor: The isolation of the caller. + /// + /// > Note: State observation is not supported in this implementation of the + /// run function. + /// + /// - Throws: + /// - `TransducerError.noOutputProduced`: If no output is produced before reaching the terminal state. + /// - Other errors: If the transducer cannot execute its transition and output function as expected, + /// for example, when events could not be enqueued because of a full event buffer, + /// when the func `terminate()` is called on the proxy, or when the output value cannot be sent. + /// + public static func run( + storage: some Storage, + proxy: Proxy = Proxy(), + systemActor: isolated any Actor = #isolation + ) async throws { + try await run( + storage: storage, + proxy: proxy, + env: Void(), + output: NoCallback(), + systemActor: systemActor + ) + } +} + +extension Transducer { + + /// Executes the Finite State Machine (FSM) with the given initial state. + /// + /// The function `run(initialState:proxy:)` returns when the transducer + /// reaches a terminal state or when an error occurs. + /// + /// The proxy, or more specifically, the `Input` interface of the proxy, is used to + /// send events to the transducer. The output can be used to connect to other + /// components. This can also be another transducer. In this case, the output is + /// connected to the input interface of another transducer. + /// + /// - Parameter initialState: The initial state of the transducer. + /// - Parameter proxy: The transducer proxy that provides the input interface + /// and an event buffer. + /// with `EffectTransducer` and to support composition patterns. + /// - Parameter systemActor: The isolation of the caller. + /// + /// > Note: State observation is not supported in this implementation of the + /// run function. + /// + /// - Throws: An error if the transducer cannot execute its transition and + /// output function as expected. For example, if the initial state is + /// terminal, or if no output is produced, or when events could not be + /// enqueued because of a full event buffer, or when the func `terminate()` + /// is called on the proxy, or when the output value cannot be sent. + /// + public static func run( + initialState: State, + proxy: Proxy = Proxy(), + systemActor: isolated any Actor = #isolation + ) async throws { + try await run( + storage: LocalStorage(value: initialState), + proxy: proxy, + env: Void(), + output: NoCallback(), + systemActor: systemActor + ) + } + +} diff --git a/Sources/Oak/FSM/TransducerProxy.swift b/Sources/Oak/FSM/TransducerProxy.swift index 8b08f22..d04714d 100644 --- a/Sources/Oak/FSM/TransducerProxy.swift +++ b/Sources/Oak/FSM/TransducerProxy.swift @@ -6,7 +6,6 @@ import struct Foundation.UUID public protocol TransducerProxy: TransducerProxyInternal, Identifiable, Equatable { associatedtype Event associatedtype Input - associatedtype AutoCancellation: Equatable /// A proxy is default-constructible. init() @@ -15,9 +14,6 @@ public protocol TransducerProxy: TransducerProxyInternal, Identifiable, E /// This is used to send events to the transducer. var input: Input { get } - /// An object which cancels the proxy when deinitialised. - var autoCancellation: AutoCancellation { get } - /// Terminates the proxy, preventing any further events from being sent and causing /// the `run` function to throw an error. /// diff --git a/Tests/OakTests/EffectInternalUsageTests.swift b/Tests/OakTests/EffectInternalUsageTests.swift index 7b12719..d53d8b9 100644 --- a/Tests/OakTests/EffectInternalUsageTests.swift +++ b/Tests/OakTests/EffectInternalUsageTests.swift @@ -27,6 +27,9 @@ struct EffectInternalUsageTests { func createEffectWithPrimaryInitialiser() async throws { enum T: EffectTransducer { + typealias Output = Void + + class Env { var value: Int = 0 } class Payload { var value: Int = 0 } enum State: Terminable { @@ -35,7 +38,7 @@ struct EffectInternalUsageTests { } enum Event { case start, payload(Payload) } - static func update(_ state: inout State, event: Event) -> T.Effect? { + static func update(_ state: inout State, event: Event) -> Self.Effect? { switch event { case .start: let effect = T.Effect { env, input, context, systemActor in diff --git a/Tests/OakTests/EffectUsageTests.swift b/Tests/OakTests/EffectUsageTests.swift index 5d07342..df5068a 100644 --- a/Tests/OakTests/EffectUsageTests.swift +++ b/Tests/OakTests/EffectUsageTests.swift @@ -24,6 +24,10 @@ struct EffectUsageTests { func createEffectWithActionInitialiser() async throws { enum T: EffectTransducer { + static func run(initialState: State, proxy: Oak.Proxy, env: Env, output: some Oak.Subject, systemActor: isolated any Actor) async throws -> () { + + } + class Env { var value: Int = 0 } class Payload { var value: Int = 0 } enum State: Terminable { diff --git a/Tests/OakTests/TransducerTests.swift b/Tests/OakTests/TransducerTests.swift index 4c7f980..00a118b 100644 --- a/Tests/OakTests/TransducerTests.swift +++ b/Tests/OakTests/TransducerTests.swift @@ -2,6 +2,238 @@ import Oak import Testing struct TransducerTests { + + @Suite + struct BasicInitializationTests { + // MARK: - Test Types + + enum VoidTransducer: Transducer { + enum State: NonTerminal { case start } + enum Event { case start } + static func update(_ state: inout State, event: Event) {} + } + + enum OutputTransducer: Transducer { + enum State: NonTerminal { case start } + enum Event { case start } + static func update(_ state: inout State, event: Event) -> Int { 1 } + } + + enum EffectTransducer: Oak.EffectTransducer { + enum State: NonTerminal { case start } + enum Event { case start } + static func update(_ state: inout State, event: Event) -> Self.Effect? { nil } + } + + enum EffectOutputTransducer: Oak.EffectTransducer { + enum State: NonTerminal { case start } + enum Event { case start } + typealias Output = Int + static func update(_ state: inout State, event: Event) -> (Self.Effect?, Output) { + (nil, 1) + } + } + + @MainActor + @Test func createVoidTransducer() async throws { + typealias T = VoidTransducer + let proxy = T.Proxy() + let task1 = Task { + try await T.run(initialState: .start) + } + let task2 = Task { + try await T.run(initialState: .start, proxy: proxy, env: Void(), output: Callback { _ in }) + } + task1.cancel() + task2.cancel() + #expect(T.Output.self == Void.self) + #expect(T.Proxy.self == Oak.Proxy.self) + #expect(T.Env.self == Void.self) + #expect(T.Effect.self == Never.self) + } + + @MainActor + @Test func createOutputTransducer() async throws { + typealias T = OutputTransducer + let proxy = T.Proxy() + let task1 = Task { + try await T.run(initialState: .start) + } + let task2 = Task { + try await T.run(initialState: .start, proxy: proxy, env: Void(), output: Callback { _ in }) + } + task1.cancel() + task2.cancel() + #expect(T.Output.self == Int.self) + #expect(T.Proxy.self == Oak.Proxy.self) + #expect(T.Env.self == Void.self) + #expect(T.Effect.self == Never.self) + } + + @MainActor + @Test func createEffectTransducer() async throws { + typealias T = EffectTransducer + let proxy = T.Proxy() + let task1 = Task { + try await T.run(initialState: .start, env: T.Env()) + } + let task2 = Task { + try await T.run(initialState: .start, proxy: proxy, env: T.Env(), output: Callback { _ in }) + } + task1.cancel() + task2.cancel() + #expect(T.Output.self == Void.self) + #expect(T.Proxy.self == Oak.Proxy.self) + #expect(T.Env.self == Void.self) + #expect(T.Effect.self == Oak.Effect.self) + } + + @MainActor + @Test func createEffectOutputTransducer() async throws { + typealias T = EffectOutputTransducer + let proxy = T.Proxy() + let task1 = Task { + try await T.run(initialState: .start, env: T.Env()) + } + let task2 = Task { + try await T.run(initialState: .start, proxy: proxy, env: T.Env(), output: Callback { _ in }) + } + task1.cancel() + task2.cancel() + #expect(T.Output.self == Int.self) + #expect(T.Proxy.self == Oak.Proxy.self) + #expect(T.Env.self == Void.self) + #expect(T.Effect.self == Oak.Effect.self) + } + } + + @Suite + struct TerminalCompletionTests { + // MARK: - Test Types + + enum VoidTransducer: Transducer { + enum State: Terminable { + case start, finished + var isTerminal: Bool { self == .finished } + } + enum Event { case start } + static func update(_ state: inout State, event: Event) { + state = .finished + } + } + + enum OutputTransducer: Transducer { + enum State: Terminable { + case start, finished + var isTerminal: Bool { self == .finished } + } + enum Event { case start } + static func update(_ state: inout State, event: Event) -> Int { + state = .finished + return 1 + } + } + + enum EffectTransducer: Oak.EffectTransducer { + enum State: Terminable { + case start, finished + var isTerminal: Bool { self == .finished } + } + enum Event { case start } + static func update(_ state: inout State, event: Event) -> Self.Effect? { + state = .finished + return nil + } + } + + enum EffectOutputTransducer: Oak.EffectTransducer { + enum State: Terminable { + case start, finished + var isTerminal: Bool { self == .finished } + } + enum Event { case start } + typealias Output = Int + static func update(_ state: inout State, event: Event) -> (Self.Effect?, Output) { + state = .finished + return (nil, 1) + } + } + + @MainActor + @Test func testRunReturnsWithVoidTransducer() async throws { + typealias T = VoidTransducer + let expectCompletionCalled = Expectation() + let expectCallbackCalled = Expectation() + let proxy = T.Proxy() + Task { + try await T.run(initialState: .start, proxy: proxy, output: Callback { _ in + expectCallbackCalled.fulfill() + }) + expectCompletionCalled.fulfill() + } + try proxy.send(.start) + try await expectCallbackCalled.await(nanoseconds: 1_000_000_000) + try await expectCompletionCalled.await(nanoseconds: 1_000_000_000) + } + + @MainActor + @Test func testRunReturnsWithOutputTransducer() async throws { + typealias T = OutputTransducer + let expectCompletionCalled = Expectation() + let expectCallbackCalled = Expectation() + let proxy = T.Proxy() + Task { + try await T.run(initialState: .start, proxy: proxy, output: Callback { _ in + expectCallbackCalled.fulfill() + }) + expectCompletionCalled.fulfill() + } + try proxy.send(.start) + try await expectCallbackCalled.await(nanoseconds: 1_000_000_000) + try await expectCompletionCalled.await(nanoseconds: 1_000_000_000) + } + + @MainActor + @Test func testRunReturnsWithEffectTransducer() async throws { + typealias T = EffectTransducer + let expectCompletionCalled = Expectation() + // let expectCallbackCalled = Expectation() + let proxy = T.Proxy() + Task { + try await T.run(initialState: .start, proxy: proxy, env: T.Env(), output: Callback { _ in + // Note: our current implementation does not call a callback + // handler when the output type equals Void. + // TODO: This subtle behaviour is currently not sufficently documented. + // The better solution should only choose the version which + // handles output, when an output parameter is given. Other- + // wise it might choose a faster implementation. However, + // the compiler *might* optimise this anyway sufficiently. + // expectCallbackCalled.fulfill() + }) + expectCompletionCalled.fulfill() + } + try proxy.send(.start) + // try await expectCallbackCalled.await(nanoseconds: 1000_000_000_000) + try await expectCompletionCalled.await(nanoseconds: 1000_000_000_000) + } + + @MainActor + @Test func testRunReturnsWithEffectOutputTransducer() async throws { + typealias T = EffectOutputTransducer + let expectCompletionCalled = Expectation() + let expectCallbackCalled = Expectation() + let proxy = T.Proxy() + Task { + try await T.run(initialState: .start, proxy: proxy, env: T.Env(), output: Callback { _ in + expectCallbackCalled.fulfill() + }) + expectCompletionCalled.fulfill() + } + try proxy.send(.start) + try await expectCallbackCalled.await(nanoseconds: 1_000_000_000) + try await expectCompletionCalled.await(nanoseconds: 1_000_000_000) + } + } @MainActor @Test