Unlock This Episode
Our Free plan includes 1 subscriber-only episode of your choice, plus weekly updates from our newsletter.
Introduction
We now have a very basic version of our architecture in place. We have a store class that is generic over a state type which represents the full state of our application and its generic over an action type that represents all of the user actions that can take place in our application.
- The store class wraps a state value, which is just a simple value type, and this allows us to once and for all hook into the observer so that we can notify SwiftUI anytime a change is about to happen to our state.
- The store class also holds onto a reducer, which is the brains of our application. It describes how to take the current state of the application, and an incoming action from the user, and produce a whole new state of the application that can be then rendered and displayed to the user.
Already this little bit of work as solved 2 of the 5 problems we outlined at the beginning of this episode.
But, as cool as all of this is, we can go further. Let’s address the problem that is starting to develop in our appReducer
. Right now it’s looking pretty hefty: one giant reducer that is handling the mutations for 3 different screens. This doesn’t seem particularly scalable. If we had two dozen screens are we really going to want a single switch statement that switches over every single action of 24 different screens? That’s not going to work.
We need to investigate ways of composing reducers into bigger reducers. How can break up that one big reducer into lots of little tiny ones that do one specific thing and then glue them together to form our master appReducer
? Let’s start to study that.
Subscribe to Point-Free
Access this episode, plus all past and future episodes when you become a subscriber.
Already a subscriber? Log in
Exercises
In this episode we mentioned that pullbacks along key paths satisfy a simple property: if you pull back along the identity key path you do not change the reducer, i.e.
reducer.pullback(\.self) == reducer
.Pullbacks also satisfy a property with respect to composition, which is very similar to that of map:
map(f >>> g) == map(f) >>> map(g)
. Formulate what this property would be for pullbacks and key paths.We had to create a struct,
FavoritePrimeState
, to hold just the data that the favorite primes screen needed, which was the activity feed and the array of favorite primes. Is it possible to instead use a typelias of a tuple with named fields instead of the struct? Does anything need to change to get the application compiling again? Do you like this approach over the struct?By the end of this episode we showed how to make reducers work on local state by using the pullback operation. However, the reducers still operate on the full
AppAction
enum, even if it doesn’t care about all of the cases in that enum. Try to repeat what we did in this episode for action enums, i.e. define apullback
operation that is capable of transforming reducers that work with local actions to ones that work on global actions.For the state pullback we needed a key path to implement this function. What kind of information do you need to implement the action pullback?
By the end of this episode we showed how to make reducers work on local state by using the pullback operation. However, all of the views still take the full global store,
Store<AppState, AppAction>
, even if they only need a small part of the state. Explore how one might transform aStore<GlobalValue, Action>
into aStore<LocalValue, Action>
. Such an operation would help simplify views by allowing them to focus on only the data they care about.We’ve seen that it is possible to pullback reducers along state key paths, but could we have also gone the other direction? That is, can we define a
map
with key paths too? If this were possible, then we could implement the following signature:func map<Value, OtherValue, Action>( _ reducer: @escaping (inout Value, Action) -> Void, value: WritableKeyPath<Value, OtherValue> ) -> (inout OtherValue, Action) -> Void { fatalError("Unimplemented") }
Can this function be implemented? If not, what goes wrong?
Solution
This function cannot be implemented. When we try to return a new reducer that works on
OtherValue
we hit a roadblock quickly:func map<Value, OtherValue, Action>( _ reducer: @escaping (inout Value, Action) -> Void, value: WritableKeyPath<Value, OtherValue> ) -> (inout OtherValue, Action) -> Void { return { otherValue, action in } }
OtherValue
is a local something that we can pluck out of more globalValue
, but we cannot create a more globalValue
from the localOtherValue
we have access to, so we can never call thereducer
that requires this globalValue
.The previous exercise leads us to realize that there is something specific happening between the interplay of key paths and pullbacks when it comes to reducers. What do you think the underlying reason is that we can pullback reducers with key paths but we cannot map reducers with key paths?
References
Contravariance
Brandon Williams & Stephen Celis • Monday Apr 30, 2018We first explored the concept of the pullback
in our episode on “contravariance”, although back then we used a different name for the operation. The pullback
is an instrumental form of composition that arises in certain situations, and can often be counter-intuitive at first sight.
Let’s explore a type of composition that defies our intuitions. It appears to go in the opposite direction than we are used to. We’ll show that this composition is completely natural, hiding right in plain sight, and in fact related to the Liskov Substitution Principle.
Pullback
We use the term pullback for the strange, unintuitive backwards composition that seems to show up often in programming. The term comes from a very precise concept in mathematics. Here is the Wikipedia entry:
In mathematics, a pullback is either of two different, but related processes: precomposition and fibre-product. Its “dual” is a pushforward.
Some news about contramap
Brandon Williams • Monday Oct 29, 2018A few months after releasing our episode on Contravariance we decided to rename this fundamental operation. The new name is more friendly, has a long history in mathematics, and provides some nice intuitions when dealing with such a counterintuitive idea.
Category Theory
The topic of category theory in mathematics formalizes the idea we were grasping at in this episode where we claim that pulling back along key paths is a perfectly legimate thing to do, and not at all an abuse of the concept of pullbacks. In category theory one fully generalizes the concept of a function that maps values to values to the concept of a “morphism”, which is an abstract process that satisfies some properties with respect to identities and composition. Key paths are a perfectly nice example of morphisms, and so category theory is what gives us the courage to extend our usage of pullbacks to key paths.
Elm: A delightful language for reliable webapps
Elm is both a pure functional language and framework for creating web applications in a declarative fashion. It was instrumental in pushing functional programming ideas into the mainstream, and demonstrating how an application could be represented by a simple pure function from state and actions to state.
Redux: A predictable state container for JavaScript apps.
The idea of modeling an application’s architecture on simple reducer functions was popularized by Redux, a state management library for React, which in turn took a lot of inspiration from Elm.
Composable Reducers
Brandon Williams • Tuesday Oct 10, 2017A talk that Brandon gave at the 2017 Functional Swift conference in Berlin. The talk contains a brief account of many of the ideas covered in our series of episodes on “Composable State Management”.