Cyclic reducer dependencies

Hey everyone, got a unique TCA issue I'm trying to figure out. Working a dynamic form builder project and trying to figure out how to have two reducers that depend on each other. Below is a simplified example of what I'm trying to accomplish:

struct Form {
    var rootSection: SectionDefinition
}

struct SectionDefinition {
    var units: IdentifiedArrayOf<UnitDefinition>
}

enum UnitDefinition: Equatable {
    case section(SectionDefinition)
    case field(FieldDefinition)
}

This is my basic data model; a Form has a root section SectionDefinition and a section definition has a list of UnitDefinition enums. One of those unit types is also a section, since they can be nested. Trying to figure out how to set up my reducers to make it work:

typealias FormReducer = Reducer<Form, FormAction, FormEnvironment>

let formReducer: FormReducer = .combine(
    sectionReducer
        .pullback(
            state: \.rootSection,
            action: /FormAction.rootSection,
            environment: { $0.rootSection }
        )
)


typealias SectionReducer = Reducer<SectionDefinition, SectionAction, SectionEnvironment>

let sectionReducer: SectionReducer = .combine(
    unitReducer.forEach(
        state: \.units,
        action: /SectionAction.unit(id:action:),
        environment: { $0.unit }
    )
)

typealias UnitReducer = Reducer<UnitDefinition, UnitAction, UnitEnvironment>

let unitReducer: UnitReducer = .combine(
    sectionReducer
        .pullback(
            state: /UnitDefinition.section,
            action: /UnitAction.section,
            environment: { $0.section }
        ),
    fieldReducer
        .pullback(
            state: /UnitDefinition.field,
            action: /UnitAction.field,
            environment: { $0.field }
        )
)

The formReducer does a pullback on the root section using the section reducer, the section reducer does a forEach over the units using the unitReducer, but then the unit reducer also needs to do a pullback on the section reducer. This compiles fine but crashes at runtime. If I remove the sectionReducer.pullback from the unitReducer things mostly work, except for supporting nested sections.

Are there any tricks or tips for making this work?

Thanks in advance!

1 Like

The problem is, to compute the unitReducer value, Swift needs the sectionReducer value, and to compute the sectionReducer value, Swift needs the unitReducer value. (Both of the ‘values’ in this case are functions.)

So one way to fix the problem is to change sectionReducer's value so that it can be computed without unitReducer's value. Instead, sectionReducer can get unitReducer's value just at the moment it actually needs to call unitReducer.

let sectionReducer: SectionReducer = .combine(
    Reducer { unitReducer.run(&$0, $1, $2) }.forEach(
        state: \.units,
        action: /SectionAction.unit(id:action:),
        environment: { $0.unit }
    )
)

This isn't really much of a change, but you might want abstract it into its own combinator for clarity:

extension Reducer {
    /// Doc comment explaining why you would want to use this.
    public static func deferred(_ inner: @escaping () -> Reducer) -> Reducer {
        .init { inner().run(&$0, $1, $2) }
    }
}

let sectionReducer: SectionReducer = .combine(
    Reducer.deferred { unitReducer }.forEach(
        state: \.units,
        action: /SectionAction.unit(id:action:),
        environment: { $0.unit }
    )
)
1 Like

@matt-dewitt The recursion case study may also help, but @mayoff's suggestion is a nice, simple alternative.

@mayoff Awesome, this work's great! Just what I was looking for! Thanks!

@stephencelis I was looking at the recursion case study and trying to come up with a way to adapt it to this, but I think my brain gut stuck in a loop trying to work through it LOL.

Thanks for the quick replies!