Unlock This Episode
Our Free plan includes 1 subscriber-only episode of your choice, plus weekly updates from our newsletter.
Introduction
We’ve finally fully extracted the most complex, asynchronous side effect in our application to work in our architecture to work exactly as it did before. There were a few bumps along the way, but we were able to address every single one of them.
This effect was definitely more complicated than the others, for two reasons:
First, this effect was bundled up with the idea of showing and dismissing alerts, which is something we hadn’t previously considered in our architecture. Solving it required us to consider what it means to extract out local state associated with the alert presentation, how to manage bindings, dismissal, and so on. Most of the bugs were around that, so in the future we’ll explore better means of interfacing with SwiftUI APIs.
Second, the effect was asynchronous! It’s just inherently more complex than a synchronous effect. We needed to take into account a threading issue, though it’s not a fault of the architecture and is an issue that anyone extracting logic from a view to an observable object would encounter.
We now have the type of our effect: that Parallel-like shape, where you get to hand off a function to someone else, where they get to do their work and invoke that function when it’s ready. And we have the shape of our reducer, which can return any number of effects in an array, where the results can be fed back into the store. And it was cool to see that we were able to, once again, do an async functional refactoring and have everything just work in the end.
We were also able to embrace the idea of “unidirectional data flow.” Even with the complications that effects and asynchronicity introduce, we’re still able to reason about how data flows through the application, because effects can only mutate our app’s state through actions send back through the store. The store is the single entryway for mutations. This is the basic version of the story for effects in our architecture.
Subscribe to Point-Free
Access this episode, plus all past and future episodes when you become a subscriber.
Already a subscriber? Log in
Exercises
Right now the
isPrime
function is defined as a little helper inIsPrimeModal.swift
. It’s a very straightforward way of checking primes, but it can be slow for very large primes. For example, using theCounter
playground, start the application’s state with the following very large prime number:21,111,111,111,113
That’s a two followed by 12 ones and a three! If you ask if that number is prime, the UI will hang for about 5 seconds before showing the modal. Clearly not a great user experience.
To fix this, upgrade the
isPrime
helper to anisPrime: Effect<Bool>
so that it can be run on a background queue. Make sure to also usereceive(on: .main)
so that the result of the effect is delivered back on the main queue.In the previous exercise you probably used a
DispatchQueue
directly in the definition of theisPrime
effect. Rather than hard coding a queue in the effect, implement the following function that allows you to determine the queue an effect will be run from:extension Effect { func run(on queue: DispatchQueue) -> Effect { fatalError("Unimplemented") } }
A “higher-order effect” is a function that takes an effect as input and returns an effect as output. This allows you to enrich an existing effect with additional behavior. We’ve already seen a few examples of this, such as
map
andreceive(on:)
. The next few exercises will walk you through writing a cancellation higher-order effect.Start by implementing an effect transformation of the form:
extension Effect { func cancellable(id: String) -> Effect { fatalError("Unimplemented") } }
This enriches an existing
Effect
with the behavior that allows it to be canceled at a later time. To achieve this, record whether or not a particular effect has been canceled by maintaining a private[String: Bool]
dictionary at the file scope, and use the boolean to determine if future effect values should be delievered.Continuing the previous exercise, implement an effect that can cancel an in-flight effect with a particular
id
:extension Effect { static func cancel(id: String) -> Effect { fatalError("Unimplemented") } }
Continuing the previous exercise, in the
counterReducer
, cancel an in-flightnthPrime
effect whenever either the increment or decrement buttons are tapped.Continuing the previous exercise, improve the implementations of
cancellable
andcancel
by:- Allow for any
Hashable
id, not just aString
. - Use a
DispatchWorkItem
to represent the cancellable unit of work instead of aBool
. - Use a
os_unfair_lock
to properly protect access to the private dictionary that holds theDispatchWorkItem
’s.
- Allow for any
Using the previous exercise on cancellation as inspiration, create a similar higher-order effect for debouncing an existing effect:
extension Effect { public func debounce<Id: Hashable>( for duration: TimeInterval, id: Id ) -> Effect { fatalError("Unimplemented") } }
This should cancel any existing in-flight effect with the same
id
while delay the current effect by theduration
passed.Use the
debounce
higher-order effect to implement automatic saving of favorite primes by debouncing anyAppAction
by 10 seconds and then performing the save effect.Consider an effect of the form
Effect<Never>
. What can be said about how such an effect behaves without knowing anything about how it works internally?Implement the following function for transforming an
Effect<Never>
into anEffect<B>
:extension Effect where A == Never { func fireAndForget<B>() -> Effect<B> { fatalError("Unimplemented") } }
Consider an analytics client that can track events by using an
Effect
:struct AnalyticsClient { let track: (String) -> Effect<???> }
What type of generic should be used for the
Effect
?Construct a live implementation of the above analytics client:
extension AnalyticsClient { static let live: AnalyticsClient = ??? }
For now you can just perform
print
statements, but in a real production application you could make an API request to your analytics provider.Use the above live analytics client to instrument the reducers in the PrimeTime application. You may find the
fireAndForget
function to be helpful for using the analytics effects in our reducers.
References
Elm: Commands and Subscriptions
Elm is a pure functional language wherein applications are described exclusively with unidirectional data flow. It also has a story for side effects that closely matches the approach we take in these episodes. This document describes how commands (like our effect functions) allow for communication with the outside world, and how the results can be mapped into an action (what Elm calls a “message”) in order to be fed back to the reducer.
Redux: Data Flow
The Redux documentation describes and motivates its “strict unidirectional data flow.”
Redux Middleware
Redux, at its core, is very simple and has no single, strong opinion on how to handle side effects. It does, however, provide a means of layering what it calls “middleware” over reducers, and this third-party extension point allows folks to adopt a variety of solutions to the side effect problem.
Redux Thunk
Redux Thunk is the recommended middleware for basic Redux side effects logic. Side effects are captured in “thunks” (closures) to be executed by the store. Thunks may optionally utilize a callback argument that can feed actions back to the store at a later time.
ReSwift
ReSwift is one of the earliest, most popular Redux-inspired libraries for Swift. Its design matches Redux, including its adoption of “middleware” as the primary means of introducing side effects into a reducer.
SwiftUIFlux
Thomas RicouardAn early example of Redux in SwiftUI. Like ReSwift, it uses “middleware” to handle side effects.
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.
Why Functional Programming Matters
John Hughes • Saturday Apr 1, 1989A classic paper exploring what makes functional programming special. It focuses on two positive aspects that set it apart from the rest: laziness and modularity.
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”.