Reswift: Automatically skip repeats for Equatable types does not work for optional substates

Created on 23 Dec 2017  路  9Comments  路  Source: ReSwift/ReSwift

When the AppState has an optional substate the implicit skip repeats fails. Modified test case to demonstrate the issue:

import XCTest
import ReSwift

class AutomaticallySkipRepeatsTests: XCTestCase {

    private var store: Store<State>!
    private var subscriptionUpdates: Int = 0

    override func setUp() {
        super.setUp()
        // Put setup code here. This method is called before the invocation of each test method in the class.
        store = Store<State>(reducer: reducer, state: nil)
        subscriptionUpdates = 0
    }

    override func tearDown() {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
        store = nil
        subscriptionUpdates = 0
        super.tearDown()
    }

    func testDispatchUnrelatedActionWithoutExplicitSkipRepeats() {
        store.subscribe(self) { $0.select { $0.name } }
        XCTAssertEqual(self.subscriptionUpdates, 1)
        store.dispatch(ChangeAge(newAge: 30))
        XCTAssertEqual(self.subscriptionUpdates, 1) // this fails as the count is 2
    }

}

extension AutomaticallySkipRepeatsTests: StoreSubscriber {
    func newState(state: String?) {
        subscriptionUpdates += 1
    }
}

private struct State: StateType {
    let age: Int
    let name: String?
}

extension State: Equatable {
    static func == (lhs: State, rhs: State) -> Bool {
        return lhs.age == rhs.age && lhs.name == rhs.name
    }
}

struct ChangeAge: Action {
    let newAge: Int
}

private let initialState = State(age: 29, name: "Daniel")

private func reducer(action: Action, state: State?) -> State {
    let defaultState = state ?? initialState
    switch action {
        case let changeAge as ChangeAge:
            return State(age: changeAge.newAge, name: defaultState.name)
        default:
            return defaultState
    }
}

Most helpful comment

@DivineDominion Conditional conformances are actually coming to Swift in 4.1, which is in Xcode 9.3 beta right now, so its not far off. I don't think writing a readme for this is worthwhile given the short time until 4.1 is released.

All 9 comments

Have you found what's causing this? I've experienced the same behavior.

After going through the code, I think the way the store.subscribe has been defined does not work for Optionals.

The below subscribe method will be called in case your SelectedState is Equatable. This was added in for the release 4.0.1. See how it calls skipRepeats() on the transformed subscription?

extension Store where State: Equatable {
    open func subscribe<SelectedState: Equatable, S: StoreSubscriber>(
        _ subscriber: S, transform: ((Subscription<State>) -> Subscription<SelectedState>)?
        ) where S.StoreSubscriberStateType == SelectedState
    {
        let originalSubscription = Subscription<State>()

        var transformedSubscription = transform?(originalSubscription)
        if subscriptionsAutomaticallySkipRepeats {
            transformedSubscription = transformedSubscription?.skipRepeats()
        }
        _subscribe(subscriber, originalSubscription: originalSubscription,
                   transformedSubscription: transformedSubscription)
    }
}

But if your SubState is not Equatable, (Optionals are not Equatable. Their wrapped types might be Equatable, but that does not make Optional itself Equatable), hence it goes to the original subscribe method:

...
open func subscribe<SelectedState, S: StoreSubscriber>(
        _ subscriber: S, transform: ((Subscription<State>) -> Subscription<SelectedState>)?
    ) where S.StoreSubscriberStateType == SelectedState
    {
        // Create a subscription for the new subscriber.
        let originalSubscription = Subscription<State>()
        // Call the optional transformation closure. This allows callers to modify
        // the subscription, e.g. in order to subselect parts of the store's state.
        let transformedSubscription = transform?(originalSubscription)

        _subscribe(subscriber, originalSubscription: originalSubscription,
                   transformedSubscription: transformedSubscription)
    }
...

The above does not add the skipRepeats() call to the transformed subscription and I feel that's why it does not work.

As you've indicated above, this is because in Swift currently does not have conditional protocol conformances. This means that Optional<T> is not Equatable where T: Equatable. As such ReSwift cannot detect that skipRepeats can be called on the type. We've attempted to implement another function to work for T? where T: Equatable, but it causes type inference problems, and also leads to the "when do you stop?" as T?? is also an issue, as is (T, U) etc.

Swift has added support for these conditional conformances and as such, this functionality will begin working with a future swift release.

Waiting for swift5 this is my own implementation to support Optional where T is Equatable .. it's not an API change but a simple extension API on top of ReSwift . Maybe someone can find it helpful, it also supports multiple subscribers since the subscriber itself is just an object

https://gist.github.com/nferruzzi/a36e2be5c5da7dbe25e90a56fd1049ad

I know its not much but after your great support for the benchmark problem I feel its the least I can share :)

Since this is such a regular topic, maybe we can put this into the README or the getting started guide until Swift 5 comes along. What do you think, @mjarvis @Ben-G?

@DivineDominion Conditional conformances are actually coming to Swift in 4.1, which is in Xcode 9.3 beta right now, so its not far off. I don't think writing a readme for this is worthwhile given the short time until 4.1 is released.

I have updated a big project on Swift 4.1, I've removed Equatable implementation from my state and models (Except in some very specific part where I want a lite compare). And this has been really great. I was writing quite a bit of custom code to compare array of optional Equatable and such... It made my entire codebase simpler, and less bug prone. Auto conformance seems stable, my tests still pass on state comparison.

Great to hear! I'm totally behind this kind of news, it seems. :)

Since we now have Swift 4.2 and conditional protocol conformance, this is doable 馃憤

Was this page helpful?
0 / 5 - 0 ratings