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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
325 changes: 181 additions & 144 deletions Sources/ComposableArchitecture/Reducer/ReducerBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,175 +7,101 @@
/// See ``CombineReducers`` for an entry point into a reducer builder context.
@resultBuilder
public enum ReducerBuilder<State, Action> {
@inlinable
public static func buildArray<R: ReducerProtocol>(_ reducers: [R]) -> _SequenceMany<R>
where R.State == State, R.Action == Action {
_SequenceMany(reducers: reducers)
}

@inlinable
public static func buildBlock() -> EmptyReducer<State, Action> {
EmptyReducer()
}

@inlinable
public static func buildBlock<R: ReducerProtocol>(_ reducer: R) -> R
where R.State == State, R.Action == Action {
reducer
}

@inlinable
public static func buildEither<R0: ReducerProtocol, R1: ReducerProtocol>(
first reducer: R0
) -> _Conditional<R0, R1>
where R0.State == State, R0.Action == Action {
.first(reducer)
}

@inlinable
public static func buildEither<R0: ReducerProtocol, R1: ReducerProtocol>(
second reducer: R1
) -> _Conditional<R0, R1>
where R1.State == State, R1.Action == Action {
.second(reducer)
}

@inlinable
public static func buildExpression<R: ReducerProtocol>(_ expression: R) -> R
where R.State == State, R.Action == Action {
expression
}

@inlinable
public static func buildFinalResult<R: ReducerProtocol>(_ reducer: R) -> R
where R.State == State, R.Action == Action {
reducer
}

#if swift(<5.7)
@_disfavoredOverload
#if swift(>=5.7)
@inlinable
public static func buildFinalResult<R: ReducerProtocol>(_ reducer: R) -> Reduce<State, Action>
where R.State == State, R.Action == Action {
Reduce(reducer)
public static func buildArray(
_ reducers: [some ReducerProtocol<State, Action>]
) -> some ReducerProtocol<State, Action> {
_SequenceMany(reducers: reducers)
}
#endif

@inlinable
public static func buildLimitedAvailability<R: ReducerProtocol>(
_ wrapped: R
) -> _Optional<R>
where R.State == State, R.Action == Action {
_Optional(wrapped: wrapped)
}

@inlinable
public static func buildOptional<R: ReducerProtocol>(_ wrapped: R?) -> _Optional<R>
where R.State == State, R.Action == Action {
_Optional(wrapped: wrapped)
}

@inlinable
public static func buildPartialBlock<R: ReducerProtocol>(first: R) -> R
where R.State == State, R.Action == Action {
first
}

@inlinable
public static func buildPartialBlock<R0: ReducerProtocol, R1: ReducerProtocol>(
accumulated: R0, next: R1
) -> _Sequence<R0, R1>
where R0.State == State, R0.Action == Action {
_Sequence(accumulated, next)
}

public enum _Conditional<First: ReducerProtocol, Second: ReducerProtocol>: ReducerProtocol
where
First.State == Second.State,
First.Action == Second.Action
{
case first(First)
case second(Second)

@inlinable
public func reduce(into state: inout First.State, action: First.Action) -> EffectTask<
First.Action
> {
switch self {
case let .first(first):
return first.reduce(into: &state, action: action)

case let .second(second):
return second.reduce(into: &state, action: action)
}
public static func buildBlock() -> some ReducerProtocol<State, Action> {
EmptyReducer()
}
}

public struct _Optional<Wrapped: ReducerProtocol>: ReducerProtocol {
@usableFromInline
let wrapped: Wrapped?
@inlinable
public static func buildBlock(
_ reducer: some ReducerProtocol<State, Action>
) -> some ReducerProtocol<State, Action> {
reducer
}

@usableFromInline
init(wrapped: Wrapped?) {
self.wrapped = wrapped
@inlinable
public static func buildEither<R0: ReducerProtocol, R1: ReducerProtocol>(
first reducer: R0
) -> _Conditional<R0, R1>
where R0.State == State, R0.Action == Action, R1.State == State, R1.Action == Action {
.first(reducer)
}

@inlinable
public func reduce(
into state: inout Wrapped.State, action: Wrapped.Action
) -> EffectTask<Wrapped.Action> {
switch wrapped {
case let .some(wrapped):
return wrapped.reduce(into: &state, action: action)
case .none:
return .none
}
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 {
Comment on lines +39 to +42
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! 😄

.second(reducer)
}
}

public struct _Sequence<R0: ReducerProtocol, R1: ReducerProtocol>: ReducerProtocol
where R0.State == R1.State, R0.Action == R1.Action {
@usableFromInline
let r0: R0
@inlinable
public static func buildExpression(
_ expression: some ReducerProtocol<State, Action>
) -> some ReducerProtocol<State, Action> {
expression
}

@usableFromInline
let r1: R1
@inlinable
public static func buildFinalResult(
_ reducer: some ReducerProtocol<State, Action>
) -> some ReducerProtocol<State, Action> {
reducer
}

@usableFromInline
init(_ r0: R0, _ r1: R1) {
self.r0 = r0
self.r1 = r1
@inlinable
public static func buildLimitedAvailability(
_ wrapped: some ReducerProtocol<State, Action>
) -> Reduce<State, Action> {
Reduce(wrapped)
}

@inlinable
public func reduce(into state: inout R0.State, action: R0.Action) -> EffectTask<R0.Action> {
self.r0.reduce(into: &state, action: action)
.merge(with: self.r1.reduce(into: &state, action: action))
public static func buildOptional(
_ wrapped: (some ReducerProtocol<State, Action>)?
) -> some ReducerProtocol<State, Action> {
wrapped
}
}

public struct _SequenceMany<Element: ReducerProtocol>: ReducerProtocol {
@usableFromInline
let reducers: [Element]
@inlinable
public static func buildPartialBlock(
first: some ReducerProtocol<State, Action>
) -> some ReducerProtocol<State, Action> {
first
}

@usableFromInline
init(reducers: [Element]) {
self.reducers = reducers
@inlinable
public static func buildPartialBlock(
accumulated: some ReducerProtocol<State, Action>, next: some ReducerProtocol<State, Action>
) -> some ReducerProtocol<State, Action> {
_Sequence(accumulated, next)
}
#else
@inlinable
public static func buildArray<R: ReducerProtocol>(_ reducers: [R]) -> _SequenceMany<R>
where R.State == State, R.Action == Action {
_SequenceMany(reducers: reducers)
}

@inlinable
public func reduce(
into state: inout Element.State, action: Element.Action
) -> EffectTask<Element.Action> {
self.reducers.reduce(.none) { $0.merge(with: $1.reduce(into: &state, action: action)) }
public static func buildBlock() -> EmptyReducer<State, Action> {
EmptyReducer()
}
}
}

public typealias ReducerBuilderOf<R: ReducerProtocol> = ReducerBuilder<R.State, R.Action>
@inlinable
public static func buildBlock<R: ReducerProtocol>(_ reducer: R) -> R
where R.State == State, R.Action == Action {
reducer
}

#if swift(<5.7)
extension ReducerBuilder {
@inlinable
public static func buildBlock<
R0: ReducerProtocol,
Expand Down Expand Up @@ -403,5 +329,116 @@ public typealias ReducerBuilderOf<R: ReducerProtocol> = ReducerBuilder<R.State,
r9
)
}

@inlinable
public static func buildEither<R0: ReducerProtocol, R1: ReducerProtocol>(
first reducer: R0
) -> _Conditional<R0, R1>
where R0.State == State, R0.Action == Action {
.first(reducer)
}

@inlinable
public static func buildEither<R0: ReducerProtocol, R1: ReducerProtocol>(
second reducer: R1
) -> _Conditional<R0, R1>
where R1.State == State, R1.Action == Action {
.second(reducer)
}

@inlinable
public static func buildExpression<R: ReducerProtocol>(_ expression: R) -> R
where R.State == State, R.Action == Action {
expression
}

@inlinable
public static func buildFinalResult<R: ReducerProtocol>(_ reducer: R) -> R
where R.State == State, R.Action == Action {
reducer
}

@_disfavoredOverload
@inlinable
public static func buildFinalResult<R: ReducerProtocol>(_ reducer: R) -> Reduce<State, Action>
where R.State == State, R.Action == Action {
Reduce(reducer)
}

@inlinable
public static func buildLimitedAvailability<R: ReducerProtocol>(
_ wrapped: R
) -> Reduce<R.State, R.Action>
where R.State == State, R.Action == Action {
Reduce(wrapped)
}

@inlinable
public static func buildOptional<R: ReducerProtocol>(_ wrapped: R?) -> R?
where R.State == State, R.Action == Action {
wrapped
}
#endif

public enum _Conditional<First: ReducerProtocol, Second: ReducerProtocol>: ReducerProtocol
where
First.State == Second.State,
First.Action == Second.Action
{
case first(First)
case second(Second)

@inlinable
public func reduce(into state: inout First.State, action: First.Action) -> EffectTask<
First.Action
> {
switch self {
case let .first(first):
return first.reduce(into: &state, action: action)

case let .second(second):
return second.reduce(into: &state, action: action)
}
}
}
#endif

public struct _Sequence<R0: ReducerProtocol, R1: ReducerProtocol>: ReducerProtocol
where R0.State == R1.State, R0.Action == R1.Action {
@usableFromInline
let r0: R0

@usableFromInline
let r1: R1

@usableFromInline
init(_ r0: R0, _ r1: R1) {
self.r0 = r0
self.r1 = r1
}

@inlinable
public func reduce(into state: inout R0.State, action: R0.Action) -> EffectTask<R0.Action> {
self.r0.reduce(into: &state, action: action)
.merge(with: self.r1.reduce(into: &state, action: action))
}
}

public struct _SequenceMany<Element: ReducerProtocol>: ReducerProtocol {
@usableFromInline
let reducers: [Element]

@usableFromInline
init(reducers: [Element]) {
self.reducers = reducers
}

@inlinable
public func reduce(
into state: inout Element.State, action: Element.Action
) -> EffectTask<Element.Action> {
self.reducers.reduce(.none) { $0.merge(with: $1.reduce(into: &state, action: action)) }
}
}
}

public typealias ReducerBuilderOf<R: ReducerProtocol> = ReducerBuilder<R.State, R.Action>
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,3 @@ public struct CombineReducers<Reducers: ReducerProtocol>: ReducerProtocol {
self.reducers.reduce(into: &state, action: action)
}
}

#if swift(>=5.7)
extension ReducerProtocol {
// NB: This overload is provided to work around https://github.com/apple/swift/issues/60445
/// Combines multiple reducers into a single reducer.
public func CombineReducers<State, Action>(
@ReducerBuilder<State, Action> _ build: () -> some ReducerProtocol<State, Action>
) -> some ReducerProtocol<State, Action> {
ComposableArchitecture.CombineReducers(build)
}
}
#endif
Loading