From 824a2c22bb3d8a4f68701b4db697c4fd98ae9a36 Mon Sep 17 00:00:00 2001 From: Tommaso Madonia Date: Tue, 27 May 2025 18:19:57 +0100 Subject: [PATCH 1/2] Add intoAlways overload that accepts a list of diffusers --- .../Diffuser/diffuser/Combinators.swift | 32 ++++++++ .../diffuser/IntoPropertiesTests.swift | 82 +++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/swift/Diffuser/Sources/Diffuser/diffuser/Combinators.swift b/swift/Diffuser/Sources/Diffuser/diffuser/Combinators.swift index 2118f98..28a3505 100644 --- a/swift/Diffuser/Sources/Diffuser/diffuser/Combinators.swift +++ b/swift/Diffuser/Sources/Diffuser/diffuser/Combinators.swift @@ -31,6 +31,38 @@ public extension Diffuser { return Diffuser(effect: effect) } + /// Create a Diffuser which merges a list of Diffusers and always executes them regardless of the value. + /// + /// This function is useful as a building block for more complex Diffusers, but it is unlikely that you would use + /// it on its own. Consider using `into` instead. + /// + /// - Parameter children: The list of Diffusers to merge + /// - Returns: A merged Diffuser which forwards any values it is `run` with to all + /// its children. + static func intoAlways( + _ children: [Diffuser] + ) -> Diffuser { + .intoAlways { value in + for child in children { + child.run(value) + } + } + } + + /// Create a Diffuser which merges a list of Diffusers and always executes them regardless of the value. + /// + /// This function is useful as a building block for more complex Diffusers, but it is unlikely that you would use + /// it on its own. Consider using `into` instead. + /// + /// - Parameter children: The list of Diffusers to merge + /// - Returns: A merged Diffuser which forwards any values it is `run` with to all + /// its children. + static func intoAlways( + _ children: Diffuser... + ) -> Diffuser { + .intoAlways(children) + } + /// Add an additional layer of caching to an existing Diffuser. /// /// This function is useful as a building block for more complex Diffusers, but it is unlikely that you would use diff --git a/swift/Diffuser/Tests/DiffuserTests/diffuser/IntoPropertiesTests.swift b/swift/Diffuser/Tests/DiffuserTests/diffuser/IntoPropertiesTests.swift index 282aa9a..4d9e058 100644 --- a/swift/Diffuser/Tests/DiffuserTests/diffuser/IntoPropertiesTests.swift +++ b/swift/Diffuser/Tests/DiffuserTests/diffuser/IntoPropertiesTests.swift @@ -107,6 +107,88 @@ class IntoPropertiesTest: XCTestCase { ) } + func testIntoAlwaysList() { + property("Running `intoAlways` on a list of `intoAlways` Diffusers is the same as running a list of `intoAlways` Diffusers individually in order") <- + forAll(integerLists, integersBetween0And10) { input, d in + var outputLhs: [Int] = [] + let range = (0...d) + let diffusersListLhs = range.map { (i: Int) -> Diffuser in + let diffuser: Diffuser = .intoAlways { _ in outputLhs.append(i) } + return diffuser + } + + var outputRhs: [Int] = [] + let diffusersListRhs = range.map { (i: Int) -> Diffuser in + let diffuser: Diffuser = .intoAlways { _ in outputRhs.append(i) } + return diffuser + } + let diffuserRhs = Diffuser.intoAlways(diffusersListRhs) + + input.forEach { value in + diffusersListLhs.forEach { diffuser in diffuser.run(value) } + + diffuserRhs.run(value) + } + + XCTAssertEqual(outputRhs, outputLhs) + return outputLhs == outputRhs + } + } + + func testIntoAlwaysListMapInto_intoAllMapInto_equivalence() { + property("Running `intoAlways` on a list of `map -> into` Diffusers is the same as running `intoAll` on a list of `map -> into` Diffusers") <- + forAll(integerLists, integersBetween0And10) { input, d in + var outputLhs: [Int] = [] + let diffuserLhs: Diffuser = .intoAlways( + .map(\.bitWidth, .into { _ in outputLhs.append(d) }), + .map({ $0 * 2 }, .into { _ in outputLhs.append(d + 1) }), + .map({ $0 % 2 == 0 }, .into { _ in outputLhs.append(d + 2) }) + ) + + var outputRhs: [Int] = [] + let diffuserRhs: Diffuser = .intoAll( + .map(\.bitWidth, .into { _ in outputRhs.append(d) }), + .map({ $0 * 2 }, .into { _ in outputRhs.append(d + 1) }), + .map({ $0 % 2 == 0 }, .into { _ in outputRhs.append(d + 2) }) + ) + + input.forEach { value in + diffuserLhs.run(value) + diffuserRhs.run(value) + } + + XCTAssertEqual(outputRhs, outputLhs) + + input.forEach { value in + diffuserLhs.run(value) + diffuserRhs.run(value) + } + + XCTAssertEqual(outputRhs, outputLhs) + + input.forEach { value in + diffuserLhs.run(value + 1) + diffuserRhs.run(value + 1) + } + + XCTAssertEqual(outputRhs, outputLhs) + + return outputLhs == outputRhs + } + } + + func testIntoAlwaysWithVarArgsIsSameAsIntoAlwaysWithList() { + property("intoAll with varargs is the same as intoAll with list") <- + // intoAlways(a, a, a) == intoAlways([a, a, a]) + diffusersBehaveTheSame( + formula( + lhs: { diffuser in .intoAlways(diffuser, diffuser, diffuser) }, + rhs: { diffuser in .intoAlways([diffuser, diffuser, diffuser]) } + ) + ) + } + + func testDiffuserInitializerWithListIsSameAsIntoAllWithList() { property("Diffuser initializer with list is the same as intoAll with list") <- // Diffuser([a, a, a]) == intoAll([a, a, a]) From f13459c8d2b49622322054f3943f153432f6a18f Mon Sep 17 00:00:00 2001 From: Tommaso Madonia Date: Wed, 28 May 2025 09:37:32 +0100 Subject: [PATCH 2/2] Update documentation --- .../Diffuser/diffuser/Combinators.swift | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/swift/Diffuser/Sources/Diffuser/diffuser/Combinators.swift b/swift/Diffuser/Sources/Diffuser/diffuser/Combinators.swift index 28a3505..dc0b574 100644 --- a/swift/Diffuser/Sources/Diffuser/diffuser/Combinators.swift +++ b/swift/Diffuser/Sources/Diffuser/diffuser/Combinators.swift @@ -33,10 +33,25 @@ public extension Diffuser { /// Create a Diffuser which merges a list of Diffusers and always executes them regardless of the value. /// - /// This function is useful as a building block for more complex Diffusers, but it is unlikely that you would use - /// it on its own. Consider using `into` instead. + /// This function is useful as a building block for more complex Diffusers when using non-`Equatable` values + /// that have `Equatable` members. If the value is `Equatable`, consider using `intoAll` instead. + /// + /// For example, using this function it is possible to create composite Diffusers without conforming the model + /// to `Equatable`: + /// + /// ```Swift + /// struct Model { + /// let a: String + /// let b: String + /// } + /// + /// let diffuser: Diffuser = .intoAlways([ + /// .map(\.a, .into { a in /* ... */ }), + /// .map(\.b, .into { b in /* ... */ }), + /// ]) + /// ``` /// - /// - Parameter children: The list of Diffusers to merge + /// - Parameter children: The list of Diffusers to merge. /// - Returns: A merged Diffuser which forwards any values it is `run` with to all /// its children. static func intoAlways( @@ -51,10 +66,25 @@ public extension Diffuser { /// Create a Diffuser which merges a list of Diffusers and always executes them regardless of the value. /// - /// This function is useful as a building block for more complex Diffusers, but it is unlikely that you would use - /// it on its own. Consider using `into` instead. + /// This function is useful as a building block for more complex Diffusers when using non-`Equatable` values + /// that have `Equatable` members. If the value is `Equatable`, consider using `intoAll` instead. + /// + /// For example, using this function it is possible to create composite Diffusers without conforming the model + /// to `Equatable`: + /// + /// ```Swift + /// struct Model { + /// let a: String + /// let b: String + /// } + /// + /// let diffuser: Diffuser = .intoAlways( + /// .map(\.a, .into { a in /* ... */ }), + /// .map(\.b, .into { b in /* ... */ }) + /// ) + /// ``` /// - /// - Parameter children: The list of Diffusers to merge + /// - Parameter children: The list of Diffusers to merge. /// - Returns: A merged Diffuser which forwards any values it is `run` with to all /// its children. static func intoAlways(