Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use opaque reducers in 5.7 to fix builder inference #1591

Merged
merged 6 commits into from
Oct 28, 2022

Conversation

stephencelis
Copy link
Member

Turns out a lot of the bugs we were working around with CombineReducers inference, as well as buildFinalResult warnings (previously: #1467) go away if we use some ReducerProtocol<State, Action> on Swift 5.7 instead of the concrete alternative.

@slavapestov Not sure if you're aware of these functional differences between inference of generics vs. opaque constrained types or would like some fresh bug reports. It can take some time to file them, but if they're not being tracked yet we can try to file down a repro.

@stephencelis
Copy link
Member Author

I've tested this in isowords and things still build OK...hopefully CI here is also agreeable.

Comment on lines +39 to +42
public static func buildEither<R0: ReducerProtocol, R1: ReducerProtocol>(
second reducer: R1
) -> _Conditional<R0, R1>
where R0.State == State, R0.Action == Action, R1.State == State, R1.Action == Action {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildEither was the one spot I couldn't get some ReducerProtocol to work...so there may be some inference issues when it comes to if-else...but that's rare and I guess we can revisit if/when it comes up.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stephencelis were you using _Conditional<R0, R1>.first(reducer)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to, but couldn't figure out how to get R0 to be a some and R1 to be a generic that propagates.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I get you.
The only way I can think of at the moment would be to use custom inits. eg: _Conditional(first: reducer, secondType: R1.self) but not sure if that's any better than just passing the two generics.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fun idea! Seems to work and I imagine is better on the type checker than the alternative, so pushed, thanks! 😄

Comment on lines 388 to 389
case first(First, Second.Type = Second.self)
case second(Second, First.Type = First.self)
Copy link
Contributor

@iampatbrown iampatbrown Oct 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hadn't thought to include the type in the case. Did you try a static func as well?

@inlinable
static func first(_ reducer: First, _ secondType: Second.Type) -> Self {
  .first(reducer)
}

Is there a benefit including it as an associated value?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iampatbrown I spoke too soon, using it as either associated value or a static func doesn't appear to compile, unfortunately. Definitely down to merge something that compiles, but both my commit and your code above crashes the compiler 😞

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stephencelis So close.. yet so far...
buildEither probably wants a concrete type in order to match first: and second:.
I think this PR is great without it anyway.

}

@inlinable
public static func buildOptional<R: ReducerProtocol>(_ wrapped: R?) -> _Optional<R>
Copy link
Contributor

@tgrapperon tgrapperon Oct 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SwiftUI makes Optional<some View> a View itself. Was making Optional<some ReducerProtocol> a ReducerProtocol instead of using this _Optional<some Reducer> considered at some point?

EDIT: I made the comment in the 5.6 branch, but it's only because diffing is all messed up. My remark applies to both branches.

Copy link
Contributor

@tgrapperon tgrapperon Oct 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, with

extension Optional: ReducerProtocol where Wrapped: ReducerProtocol {
  public func reduce(into state: inout Wrapped.State, action: Wrapped.Action)
    -> EffectTask<Wrapped.Action>
  {
    switch self {
    case .none:
      return .none
    case .some(let wrapped):
      return wrapped.reduce(into: &state, action: action)
    }
  }
}

We can return directly wrapped in buildOptional, and Optional.some(wrapped) in buildLimitedAvailability, while using opaque reducers.

BTW, I've just spotted this thing while checking in SwiftUI interface:

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
@inlinable @inline(__always) internal func asOptional<T>(_ value: T) -> T? {
    func unwrap<T>() -> T { value as! T }
    let optionalValue: T? = unwrap()
    return optionalValue
}

This is some weird gymnastic and I'm not getting why they don't use Optional.some instead. But this is mostly trivia and unrelated to the PR.

EDIT: I think this is to make sure they don't create T??, etc. So we can probably use this too for reducers.

EDIT2: Well, it still creates T?? as I'm getting it, so I don't know. I'm not 100% on nested optionals, as I think there is some automatic coalescing in some places.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tgrapperon Good find! I'm not sure why we didn't conform optional directly, maybe because of a bug with type aliases in pre-5.7. Added support and removed _Optional, thanks!

We also realized our buildLimitedAvailability wasn't right. This function needs to erase the reducer somehow. I think we snuck a fix into this PR, though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we still need the buildOptional block? As I'm seeing it, it should be handled by the buildBlock(reducer: R?) now that R? is a reducer, isn't it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind, it is required to work with buildLimitedAvailability at least.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tgrapperon Just merged and missed this. What hit main should at least be an improvement, but if you find other tweaks that can be made def PR!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I think we need this. As I'm getting it now:

if #available(iOS 16, *) {
  R()
}

will buildLimitedAvailability R into AnyReducer and then buildOptional it to produce AnyReducer?. If you remove buildOptional, the test for buildLimitedAvailability doesn't pass anymore, so I guess this is what happens (and it is maybe involved with other blocks/situations).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stephencelis buildOptional is involved as soon as we have unbalanced if checks of some sort, so it is required for sure.

@stephencelis stephencelis merged commit 9413d9f into main Oct 28, 2022
@stephencelis stephencelis deleted the build-final-result-warning-fix-2 branch October 28, 2022 17:54
jeffersonsetiawan added a commit to tokopedia/RxComposableArchitecture that referenced this pull request Jan 5, 2023
jeffersonsetiawan added a commit to tokopedia/RxComposableArchitecture that referenced this pull request Feb 6, 2023
* add dependencies and its test

* update effect and reducer

* add reducers, rename anyreducer, delete xct related files

* extract storeconfig

* task result and task cancellable value

* observable

* effect

* fix package swift

* any disposable

* import xctdynamicoverlay

* update effects

* make any disposable public

* fix any reducer compatibility

* first passed test 5.7

* refactor package.swift

* remove anyequatable double

* update store

* wrap in discardable

* [TestStore] - Update to conform changes ReducerProtocol (#65)

* progress update teststore

* progressing update TestStore

* Fixing TestStore & make it success run test

* update indent code TestStore, add comment for send action assert testStore

* change observable

* update observable

* rename conflicted name

* undo

* add flatmap

* add xctest dynamic overlay podspec

* add xctdo podspec

* update exports.swift

* update podspec

* update typo podspec

* fix dispose issue

* restructure dependency module, fix test, update podspec and package.swift

* update store old behaviour, fix store tests

* remove unused code

* fix flatmap, add StoreOf

* update teststore generics order, remove all spi internals

* Rewrite example using Reducer Protocol style (#63)

* update store, rewrite basic and scoping example

* rewrite environment example

* rewrite pullback vc example

* update counterview, rewrite iflet

* rewrite never equal

* update timer example

* update test

* update old scope and store tests

* use OptionalPath

* remove duplicated StoreConfig

* add ACL for TestReducer class & property

* fix default value for useNewScope in StoreConfig

* [Reducer Protocol] - Add New Test Cases (#68)

* fix passing useNewScope to Store from TestStore

* add new test to TestStore

* add new TestStoreTest cases

* update to fix async task cancel

* update StoreTest test case

* add more StoreTests test cases

* update failingWhenNothingChanges to true by default on new TestStore code

* update & add new cases for TestStoreTest

* remove import combine

* separate oldScope & newScope test on Storetest and TestStoreTest

* update testcases based on new default value for  for Store and TestStore

* [Reducer Protocol] - Support Bootstrap MockKit (#70)

Support our Bootstrap MockKit when using ReducerProtocol Dependency Environment

* update pullback vc

* bump xcode versions

* update ci.yml to use xcode 14.1 for example tests

* update ci yml again for xcode 14.1

* refactor ci.yml

* update xcode 14 1 name

* sync with 0.44.1 minus PR 1510 and EffectTask

* fix warning

* Fix Reducer builder inference issue: pointfreeco/swift-composable-architecture#1591 Implement Test Pt1

* adding more test, still failing dependency test on both store and teststore

* pointfreeco/swift-composable-architecture#1570
pointfreeco/swift-composable-architecture#1575

* add usenewscope

* Sync Cancellation

* Readd PR #1510 and add tests, fix tests examples

* add optional reducer, update dependency values tests with binding

* sync up to 0.46.0

* sync up to 0.47.2

* sync up to 0.48.0

* bump xctestdynamicoverlay

* remove assert function on teststore

* add failing when nothing change on scope teststore

* update store send and mainqueue syntax

---------

Co-authored-by: Andhika Setiadi <[email protected]>
Co-authored-by: Jefferson Setiawan <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants