A video series exploring functional programming and Swift.
#79 • Monday Nov 4, 2019 • Subscriber-only

Effectful State Management: The Point

We’ve got the basic story of side effects in our architecture, but the story is far from over. Turns out that even side effects themselves are composable. Base effect functionality can be extracted and shared, and complex effects can be broken down into simpler pieces.

#79 • Monday Nov 4, 2019 • Subscriber-only

Effectful State Management: The Point

We’ve got the basic story of side effects in our architecture, but the story is far from over. Turns out that even side effects themselves are composable. Base effect functionality can be extracted and shared, and complex effects can be broken down into simpler pieces.


Subscribe to Point‑Free

This episode is for subscribers only. To access it, and all past and future episodes, become a subscriber today!

See subscription optionsorLog in

Sign up for our weekly newsletter to be notified of new episodes, and unlock access to any subscriber-only episode of your choosing!

Sign up for free episode

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

👋 Hey there! Does this episode sound interesting? Well, then you may want to subscribe so that you get access to this episodes and more!


Exercises

  1. Right now the isPrime function is defined as a little helper in IsPrimeModal.swift. It’s a very straightforward way of checking primes, but it can be slow for very large primes. For example, using the Counter 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 an isPrime: Effect<Bool> so that it can be run on a background queue. Make sure to also use receive(on: .main) so that the result of the effect is delivered back on the main queue.

  2. In the previous exercise you probably used a DispatchQueue directly in the definition of the isPrime 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")
      }
    }
    
  3. 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 and receive(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.

  4. 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")
      }
    }
    
  5. Continuing the previous exercise, in the counterReducer, cancel an in-flight nthPrime effect whenever either the increment or decrement buttons are tapped.

  6. Continuing the previous exercise, improve the implementations of cancellable and cancel by:

    • Allow for any Hashable id, not just a String.
    • Use a DispatchWorkItem to represent the cancellable unit of work instead of a Bool.
    • Use a os_unfair_lock to properly protect access to the private dictionary that holds the DispatchWorkItem’s.
  7. 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 the duration passed.

  8. Use the debounce higher-order effect to implement automatic saving of favorite primes by debouncing any AppAction by 10 seconds and then performing the save effect.

  9. 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?

  10. Implement the following function for transforming an Effect<Never> into an Effect<B>:

    extension Effect where A == Never {
      func fireAndForget<B>() -> Effect<B> {
        fatalError("Unimplemented")
      }
    }
    
  11. 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?

  12. 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

Chapters
Introduction
00:05
What’s the point?
02:47
Composable, transformable effects
04:13
Reusable effects: network requests
11:16
Reusable effects: threading
17:02
Getting everything building again
20:59
Conclusion
26:26