There was also some discussion in issue #51 on the episode-code-samples repo. I gave a description of my Elm-subscription-based implementation there, but I have refined it a bit since then. Anyway, here are some thoughts.
I don't like the name “subscription” for this concept, specifically because Combine already has a Subscription protocol, and you need to create a Combine Subscription for each Elm-like subscription. Witness this statement from @pteasima's post: “if that subscription isnt running (presumably failed, since subscriptions usually dont finish), subscribe to the new one (restart it)” A subscription is apparently a thing you subscribe to, but (at least in the Combine world) it's also what we call the thing you get back when you subscribe to a publisher. This is confusing.
There are really three kinds of objects in play here:
- There is a key identifying the Elm-subscription. In @pteasima's PR, the key is an
AnyHashable.
- There is the
Effect corresponding to the key.
- There is the Combine
Subscription to the Effect. This is managed by the Store and doesn't show up in the Reducer.
So in my implementation, I'm currently calling the Elm-subscription a “condition”. Yes, “condition” also has other meanings, and I'd like to find an even better name.
I think it's incorrect to claim that “subscriptions usually dont finish”. I use Elm-style subscriptions that finish.
I store the current subscriptions, and their corresponding AnyCancellables, in a container held (privately) by the Store. It seems like the natural place to store subscriptions.
The implementations by @pteasima (here) and PF (here) use a single function that returns a dictionary [AnyHashable: Effect<Action, Never>]. I think it is useful to separate that into two functions. In my implementation, I pass the two functions as arguments to the root store's initializer. Something like this:
public convenience init<SubId: Hashable, Environment>(
initialState: State,
reducer: Reducer<State, Action, Environment>,
environment: Environment,
subscriptionsForState: @escaping (State) -> Set<SubId>,
effectOfSubscription: @escaping (SubId) -> Effect<Action, Never>
) { ... }
I still have the initializer that doesn't take the subscription-related arguments. A Store constructed that way doesn't track any subscriptions. In Elm terms, this is like using Browser.sandbox when you don't need subscriptions, and Browser.element when you do.
Here's some things I like about this separation of concerns:
- I get strong typing instead of
AnyHashable.
- I can write separate, simpler tests for the two functions.
- I can change the
effectOfSubscription function while keeping subscriptionsForState exactly the same. This lets me easily return different effects for previews, tests, and production.