diff --git a/README.md b/README.md index 5e57f6c..9ba7216 100644 --- a/README.md +++ b/README.md @@ -104,9 +104,33 @@ let forkedActor = ForkedActor( } ) -try await forkedActor.act() +let actorValue = await forkedActor.act().value -let actorValue = await forkedActor.actor.value +XCTAssertEqual(actorValue, 3) +``` + +### ForkedActor KeyPathActor Example + +```swift +let forkedActor = ForkedActor( + value: 0, + leftOutput: { actor in + await actor.update(to: { $0 + 1 }) + }, + rightOutput: { actor in + try await actor.fork( + leftOutput: { actor in + await actor.update(to: { $0 + 1 }) + }, + rightOutput: { actor in + await actor.update(\.self, to: { $0 + 1 }) + } + ) + .act() + } +) + +let actorValue = try await forkedActor.act().value XCTAssertEqual(actorValue, 3) ``` diff --git a/Sources/Fork/Actors/KeyPathActor.swift b/Sources/Fork/Actors/KeyPathActor.swift new file mode 100644 index 0000000..4267192 --- /dev/null +++ b/Sources/Fork/Actors/KeyPathActor.swift @@ -0,0 +1,31 @@ +/// A generic Actor that uses KeyPaths to update and set values +public actor KeyPathActor { + + /// The wrapped value of the Actor + public var value: Value + + /// Wrapped the Value into a KeyPathActor + public init(value: Value) { self.value = value } + + /// Set the value + public func set( + to newValue: Value + ) { value = newValue } + + /// Set the key path to a new value + public func set( + _ keyPath: WritableKeyPath, + to newValue: KeyPathValue + ) { value[keyPath: keyPath] = newValue } + + /// Update the value + public func update( + to newValue: (Value) -> Value + ) { set(to: newValue(value)) } + + /// Update the key path to a new value + public func update( + _ keyPath: WritableKeyPath, + to newValue: (KeyPathValue) -> KeyPathValue + ) { set(keyPath, to: newValue(value[keyPath: keyPath])) } +} diff --git a/Sources/Fork/Extensions/Array+ForkedArray.swift b/Sources/Fork/Extensions/Array+ForkedArray.swift index 3574d75..885c56e 100644 --- a/Sources/Fork/Extensions/Array+ForkedArray.swift +++ b/Sources/Fork/Extensions/Array+ForkedArray.swift @@ -1,7 +1,7 @@ extension Array { /// Create a ``ForkedArray`` from the current `Array` public func fork( - filter: @escaping (Element) async throws -> Bool = { _ in true }, + filter: @escaping (Element) async throws -> Bool, map: @escaping (Element) async throws -> Output ) -> ForkedArray { ForkedArray( @@ -11,6 +11,13 @@ extension Array { ) } + /// Create a ``ForkedArray`` from the current `Array` + public func fork( + map: @escaping (Element) async throws -> Output + ) -> ForkedArray { + fork(filter: { _ in true}, map: map) + } + /// Create a ``ForkedArray`` from the current `Array` and get the Output Array public func forked( filter: @escaping (Element) async throws -> Bool, @@ -24,6 +31,13 @@ extension Array { .output() } + /// Create a ``ForkedArray`` from the current `Array` and get the Output Array + public func forked( + map: @escaping (Element) async throws -> Output + ) async throws -> [Output] { + try await forked(filter: { _ in true }, map: map) + } + /// Returns an array containing the results of mapping the given closure over the sequence’s elements. public func asyncMap( _ transform: @escaping (Element) async throws -> Output @@ -37,4 +51,11 @@ extension Array { ) async throws -> [Element] { try await fork(filter: isIncluded, map: identity).output() } + + /// Calls the given closure for each of the elements in the Array. This function uses ``ForkedArray`` and will be parallelized when possible. + public func asyncForEach( + _ transform: @escaping (Element) async throws -> Void + ) async throws { + _ = try await asyncMap(transform) + } } diff --git a/Sources/Fork/ForkedActor.swift b/Sources/Fork/ForkedActor.swift index 01d03f0..b18a683 100644 --- a/Sources/Fork/ForkedActor.swift +++ b/Sources/Fork/ForkedActor.swift @@ -33,8 +33,35 @@ public struct ForkedActor { ) } + /// Create a ``ForkedActor`` using a single value that is passed into the left and right async functions. + /// - Parameters: + /// - value: Any value to be passed into the map functions. This value is wrapped into an `actor` using ``KeyPathActor``. + /// - leftOutput: An `async` closure that uses the `actor` as its input + /// - rightOutput: An `async` closure that uses the `actor` as its input + public init( + value: Input, + leftOutput: @escaping (_ actor: Value) async throws -> Void, + rightOutput: @escaping (_ actor: Value) async throws -> Void + ) where Value == KeyPathActor { + self.actor = KeyPathActor(value: value) + self.fork = Fork( + value: actor, + leftOutput: { actor in + try await leftOutput(actor) + + return actor + }, + rightOutput: { actor in + try await rightOutput(actor) + + return actor + } + ) + } + /// Asynchronously resolve the fork using the actor - public func act() async throws { + @discardableResult + public func act() async throws -> Value { try Task.checkCancellation() async let leftForkedTask = fork.left() @@ -45,5 +72,7 @@ public struct ForkedActor { _ = try await [leftForkedTask, rightForkedTask] try Task.checkCancellation() + + return actor } } diff --git a/Sources/Fork/ForkedArray.swift b/Sources/Fork/ForkedArray.swift index 0ec3fc5..f51841f 100644 --- a/Sources/Fork/ForkedArray.swift +++ b/Sources/Fork/ForkedArray.swift @@ -20,7 +20,7 @@ public struct ForkedArray { /// - output: An `async` closure that uses the `Array.Element` as its input public init( _ array: [Value], - filter: @escaping (Value) async throws -> Bool = { _ in true }, + filter: @escaping (Value) async throws -> Bool, map: @escaping (Value) async throws -> Output ) { self.array = array @@ -45,6 +45,17 @@ public struct ForkedArray { } } + /// Create a ``ForkedArray`` using a single `Array` + /// - Parameters: + /// - array: The `Array` to be used in creating the output + /// - output: An `async` closure that uses the `Array.Element` as its input + public init( + _ array: [Value], + map: @escaping (Value) async throws -> Output + ) { + self.init(array, filter: { _ in true }, map: map) + } + /// Asynchronously resolve the forked array public func output() async throws -> [Output] { try await fork.merged( diff --git a/Tests/ForkTests/ForkTests.swift b/Tests/ForkTests/ForkTests.swift index 8721cfb..f6f9c81 100644 --- a/Tests/ForkTests/ForkTests.swift +++ b/Tests/ForkTests/ForkTests.swift @@ -29,4 +29,47 @@ final class ForkTests: XCTestCase { XCTAssertEqual(output, "1010") } + + func testForkClosure() async throws { + let fork = Fork( + value: UUID.init, + leftOutput: { $0.uuidString == "UUID" }, + rightOutput: { Array($0.uuidString.reversed().map(\.description)) } + ) + + let leftOutput = try await fork.left() + let rightOutput = try await fork.right() + + XCTAssertEqual(leftOutput, false) + XCTAssertEqual(rightOutput.count, 36) + + let mergedFork: () async throws -> [String] = fork.merge( + using: { bool, string in + if bool { + return string + string + } + + return string + } + ) + + let output = try await mergedFork() + + XCTAssertNotEqual(output, rightOutput) + } + + func testForkVoid() async throws { + try await Fork( + leftOutput: { print("Hello", terminator: "") }, + rightOutput: { + try await Fork( + leftOutput: { print(" ", terminator: "") }, + rightOutput: { print("World", terminator: "") } + ) + .merged() + } + ) + .merged() + print() + } } diff --git a/Tests/ForkTests/ForkedActorTests.swift b/Tests/ForkTests/ForkedActorTests.swift index f30929f..200b816 100644 --- a/Tests/ForkTests/ForkedActorTests.swift +++ b/Tests/ForkTests/ForkedActorTests.swift @@ -38,31 +38,25 @@ class ForkedActorTests: XCTestCase { } func testHigherOrderForkedActor() async throws { - actor TestActor { - var value: Int = 0 - - func increment() { - value += 1 - } - } - let forkedActor = ForkedActor( - actor: TestActor(), + value: 0, leftOutput: { actor in - await actor.increment() + await actor.update(to: { $0 + 1 }) }, rightOutput: { actor in try await actor.fork( - leftOutput: { await $0.increment() }, - rightOutput: { await $0.increment() } + leftOutput: { actor in + await actor.update(to: { $0 + 1 }) + }, + rightOutput: { actor in + await actor.update(\.self, to: { $0 + 1 }) + } ) .act() } ) - try await forkedActor.act() - - let actorValue = await forkedActor.actor.value + let actorValue = try await forkedActor.act().value XCTAssertEqual(actorValue, 3) } diff --git a/Tests/ForkTests/ForkedArrayTests.swift b/Tests/ForkTests/ForkedArrayTests.swift index 63587c0..c410803 100644 --- a/Tests/ForkTests/ForkedArrayTests.swift +++ b/Tests/ForkTests/ForkedArrayTests.swift @@ -2,6 +2,24 @@ import XCTest @testable import Fork class ForkedArrayTests: XCTestCase { + func testForkedArray() async throws { + let photoNames: [String] = (0 ... Int.random(in: 1 ..< 10)).map(\.description) + @Sendable func downloadPhoto(named: String) async -> String { named } + func show(_ photos: [String]) { } + + let forkedArray = ForkedArray( + photoNames, + map: downloadPhoto(named:) + ) + let photos = try await forkedArray.output() + + XCTAssertEqual(photos, photoNames) + } + + func testForkedArray_ForEach() async throws { + try await ["Hello", " ", "World", "!"].asyncForEach { print($0) } + } + func testForkedArray_none() async throws { let photoNames: [String] = [] @Sendable func downloadPhoto(named: String) async -> String { named } @@ -27,7 +45,6 @@ class ForkedArrayTests: XCTestCase { @Sendable func downloadPhoto(named: String) async -> String { named } let photos = try await photoNames.forked( - filter: { _ in true }, map: downloadPhoto(named:) ) @@ -52,4 +69,14 @@ class ForkedArrayTests: XCTestCase { XCTAssertEqual(photos, photoNames) } + + func testForkedArray_order() async throws { + let photoNames = ["Hello", " ", "World", "!"] + @Sendable func downloadPhoto(named: String) async -> String { named } + + let forkedArray = photoNames.fork(map: downloadPhoto(named:)) + let photos = try await forkedArray.output() + + XCTAssertEqual(photos, photoNames) + } }