Unlock This Episode
Our Free plan includes 1 subscriber-only episode of your choice, plus weekly updates from our newsletter.
Introduction
So we are now getting pretty close to accomplishing yet another architectural problem that we set out to solve at the beginning of this series of episodes. We stated that we wanted to be able to build large complex applications out of simple, composable units.
We can now do this with our reducers and the state they operate on. We can write our reducers so that they operate on just the bare minimum of state necessary to get the job done, and then pull them back to fit inside a reducer that is much larger and operates on a full application’s state.
Ideally we’d even want those simple, composable units to be so isolated that we may even be able to put them in their own module so that they could easily be shared with other modules and apps.
This is getting pretty exciting! But, there’s still a problem. Even though our reducers are operating on smaller pieces of data, they still know far too much about the larger reducer they are embedded in, particularly they can listen in on every single app action.
It sounds like we need to repeat the same story for actions that we have for state.
Subscribe to Point-Free
Access this episode, plus all past and future episodes when you become a subscriber.
Already a subscriber? Log in
Exercises
We’ve seen that it is possible to pullback reducers along action 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, Action, OtherAction>( _ reducer: @escaping (inout Value, Action) -> Void, value: WritableKeyPath<Action, OtherAction> ) -> (inout Value, OtherAction) -> Void { fatalError("Unimplemented") }
Can this function be implemented? If not, what goes wrong?
Solution
This function cannot be implemented. As we saw last time with value key paths, if we try to implement this function we quickly hit a roadblock:
func map<Value, Action, OtherAction>( _ reducer: @escaping (inout Value, Action) -> Void, value: WritableKeyPath<Action, OtherAction> ) -> (inout Value, OtherAction) -> Void { return { value, otherAction in } }
We have a local
OtherAction
that we can pluck out of a globalAction
, but thereducer
we have requires a globalAction
, which we have no access to.Right now we have activity feed logic scattered throughout a few reducers, such as our
primeModalReducer
andfavoritePrimesReducer
. The mutations we perform for the activity feed are independent of the other logic going on in those reducers, which means it’s ripe for extracting in some way.Explore how one can extract all of the activity feed logic out of our reducers by transforming our
appReducer
into a whole new reducer, and inside that transformation one would perform all of the activity feed logic. Such a transformation would have the following signature:func activityFeed( _ reducer: @escaping (inout AppState, AppAction) -> Void ) -> (inout AppState, AppAction) -> Void { fatalError("Unimplemented activity feed logic") }
You would apply this function to the
appReducer
to obtain a whole new reducer that has the activity feed logic baked in, without needing to add anything to the reducers that make upappReducer
.Solution
func activityFeed( _ reducer: @escaping (inout AppState, AppAction) -> Void ) -> (inout AppState, AppAction) -> Void { return { state, action in switch action { case .counter: break case .primeModal(.removeFavoritePrimeTapped): state.activityFeed.append( .init(timestamp: Date(), type: .removedFavoritePrime(state.count)) ) case .primeModal(.addFavoritePrime): state.activityFeed.append( .init(timestamp: Date(), type: .saveFavoritePrimeTapped(state.count)) ) case let .favoritePrimes(.deleteFavoritePrimes(indexSet)): for index in indexSet { state.activityFeed.append( .init(timestamp: Date(), type: .removedFavoritePrime(state.favoritePrimes[index])) ) } } reducer(&state, action) } } activityFeed(appReducer)
Explore ways of adding logging to our application. Perhaps the easiest is to add
print
statements to thesend
action of ourStore
. That would allow you to get logging for every single action sent to the store, and you can log the state that resulted from that mutation.However, there is a nicer way of adding logging to our application. Instead of putting it in the
Store
, where not all users of theStore
class may want logging, try implementing a transformation of reducer functions that automatically adds logging to any reducer.Such a function would have the following signature:
func logging<Value, Action>( _ reducer: @escaping (inout Value, Action) -> Void ) -> (inout Value, Action) -> Void
You would apply this function to the
appReducer
to obtain a whole new reducer that logs whenever an action is processed by the reducer.Are there any similarities to this transformation and the transformation from the previous exercise?
Solution
func logging( _ reducer: @escaping (inout AppState, AppAction) -> Void ) -> (inout AppState, AppAction) -> Void { return { value, action in reducer(&value, action) print("Action: \(action)") print("State:") dump(value) print("---") } }
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.
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.
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”.
Structs 🤝 Enums
Brandon Williams & Stephen Celis • Monday Mar 25, 2019To understand why it is so important for Swift to treat structs and enums fairly, look no further than our episode on the topic. In this episode we demonstrate how many features of one manifest themselves in the other naturally, yet there are still some ways in which Swift favors structs over enums.
Name a more iconic duo… We’ll wait. Structs and enums go together like peanut butter and jelly, or multiplication and addition. One’s no more important than the other: they’re completely complementary. This week we’ll explore how features on one may surprisingly manifest themselves on the other.
Enum Properties
Brandon Williams & Stephen Celis • Monday Apr 1, 2019The concept of “enum properties” were essential for our implementation of the “action pullback” operation on reducers. We first explored this concept in episode #52 and showed how this small amount of boilerplate can improve the ergonomics of data access in enums.
Swift makes it easy for us to access the data inside a struct via dot-syntax and key-paths, but enums are provided no such affordances. This week we correct that deficiency by defining the concept of “enum properties”, which will give us an expressive way to dive deep into the data inside our enums.
Swift Syntax Command Line Tool
Brandon Williams & Stephen Celis • Monday Apr 22, 2019Although “enum properties” are powerful, it is a fair amount of boilerplate to maintain if you have lots of enums. Luckily we also were able to create a CLI tool that can automate the process! We use Apple’s SwiftSyntax library to edit source code files directly to fill in these important properties.
Today we finally extract our enum property code generator to a Swift Package Manager library and CLI tool. We’ll also do some next-level snapshot testing: not only will we snapshot-test our generated code, but we’ll leverage the Swift compiler to verify that our snapshot builds.
pointfreeco/swift-enum-properties
Brandon Williams & Stephen Celis • Monday Apr 29, 2019Our open source tool for generating enum properties for any enum in your code base.
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.
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.