There’s a potential performance problem lurking in the composable architecture, and it’s time to finally solve it. But, in doing so, we will stumble upon a wonderful way to make the architecture adaptive to many more situations.
It’s time to put the finishing touches to our architecture so that we can use it in production. This week we begin exploring how to make the composable architecture adapt to many use cases, and we will use a potential performance problem as inspiration for this exploration.
👋 Hey there! See anything you like? You may be interested in subscribing so that you get access to these episodes and all future ones.
Now that we’ve baked the “environment” of dependencies directly into the composable architecture, we’re ready to refactor our app’s frameworks and tests to work with them in a modular and more lightweight way.
While we love the “environment” approach to dependency injection, which we introduced many episodes ago, it doesn’t feel quite right in the composable architecture and introduces a few problems in how we manage dependencies. Today we’ll make a small tweak to the architecture in order to solve them!
Let’s explore a real world application of “case paths,” which provide key path-like functionality to enum cases. We’ll upgrade our composable architecture to use them and see why they’re a better fit than our existing approach.
Although case paths are powerful and a natural extension of key paths, they are difficult to work with right now. They require either hand-written boilerplate, or code generation. However, there’s another way to generate case paths for free, and it will make them just as ergonomic to use as key paths.
We’ve now seen that it’s possible to define “case paths”: the enum equivalent of key paths. So what are their features? Let’s explore a few properties of key paths to see if there are corresponding concepts on case paths.
You’ve heard of key paths, but…case paths!? Today we introduce the concept of “case paths,” a tool that helps you generically pick apart an enum just like key paths allow you to do for structs. It’s the tool you never knew you needed.
We’ve made testing in our architecture a joy! We can test deep aspects of our application with minimal ceremony, but it took us a whole 18 episodes to get here! So this week we ask: what’s the point!? Can we write these kinds of tests in vanilla SwiftUI?
We not only want our architecture to be testable, but we want it to be super easy to write tests, and perhaps even a joy to write tests! Right now there is a bit of ceremony involved in writing tests, so we will show how to hide away those details behind a nice, ergonomic API.
Side effects are by far the hardest thing to test in an application. They speak to the outside world and they tend to be sprinkled around to get the job done. However, we can get broad test coverage of our reducer’s effects with very little work, and it will all be thanks to a simple technique we covered in the past.
It’s time to see how our architecture handles the fifth and final problem we identified as being important to solve when building a moderately complex application: testing! Let’s get our feet wet and write some tests for all of the reducers powering our application.
Let’s explore the Combine framework and its correspondence with the Effect type. Combine introduces several concepts that overlap with how we model effects in our composable architecture. Let’s get an understanding of how they work together and compare them to our humble Effect type.
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.
It’s time to finish our architecture’s story for side effects. We’ve described synchronous effects and unidirectional effects, but we still haven’t captured the complexity of async effects. Let’s fix that with a final, functional refactor.
We’ve modeled side effects in our architecture, but it’s not quite right yet: a reducer can write to the outside world, but it can’t read data back in! This week our architecture’s dedication to unidirectional data flow will lead us there.
Side effects are one of the biggest sources of complexity in any application. It’s time to figure out how to model effects in our architecture. We begin by adding a few new side effects, and then showing how synchronous effects can be handled by altering the signature of our reducers.
We’ve now fully modularized our app by extracting its reducers and views into their own modules. Each screen of our app can be run as a little app on its own so that we can test its functionality, all without needing to know how it’s plugged into the app as a whole. And this is the point of modular state management!
While we’ve seen that each reducer we’ve written is super modular, and we were easily able to extract each one into a separate framework, our views are still far from modular. This week we address this by considering: what does it mean to transform the state a view has access to?
In exploring four forms of composition on reducer functions, we made the claim that it gave us the power to fully isolate app logic, making it simpler and easier to understand. This week we put our money where our mouth is and show just how modular these reducers are!
We will explore a form of reducer composition that will take our applications to the next level. Higher-order reducers will allow us to implement broad, cross-cutting functionality on top of our applications with very little work, and without littering our application code with unnecessary logic. And, we’ll finally answer “what’s the point?!”
Turns out, reducers that work on local actions can be pulled back to work on global actions. However, due to an imbalance in how Swift treats enums versus structs it takes a little work to implement. But never fear, a little help from our old friends “enum properties” will carry us a long way.
So far we have pulled a lot of our application’s logic into a reducer, but that reducer is starting to get big. Turns out that reducers emit many types of powerful compositions, and this week we explore two of them: combines and pullbacks.
Now that we understand some of the fundamental problems that we will encounter when building a complex application, let’s start solving some of them! We will begin by demonstrating a technique for describing the state and actions in your application, as well as a consistent way to apply mutations to your application’s state.
With our moderately complex SwiftUI application complete we can finally ask ourselves: “what’s the point!?” What does SwiftUI have to say about app architecture? What questions are left unanswered? What can we do about it?
Let’s begin exploring application architecture by understanding what are the common problems we encounter when trying to build large, complex applications. We will build an app in SwiftUI to see how Apple’s new framework approaches solving these problems.
Now that we’ve looked at how to parse multiple values given a single parser, let’s try to parse a single value using multiple parsers! And after defining a bunch of these parser combinators we’ll finally be able to ask: “what’s the point!?”
Let’s solve another common parsing problem using parser combinators! It’s common to want to parse multiple values off a string, and while
zip gets us part of the way there, it doesn’t let us parse any number of values! Luckily there’s a parser combinator that can help, and it really packs a punch.
zip pack a punch, there are still many parsing operations that can’t be done using them alone. This is where “parser combinators” come into play. Let’s look at a few common parsing problems and solve them using parser combinators!
flatMap allowed us to take our parser type to the next level, it introduced a nesting problem. Isn’t
flatMap all about solving nesting problems!? Well, we have one more operation at our disposal:
zip! Let’s define
zip on the parser type, see what it brings to the table, and finally ask, “what’s the point?”
map function on parsers is powerful, but there are still a lot of things it cannot do. We will see that in trying to solve some of its limitations we are naturally led to our old friend the
We now have a precise, efficient definition for parsing, but we haven’t even scratched the surface of its relation to functional programming. In this episode we begin to show how all of the functional operators we know and love come into play, starting with map.
It’s time to ask the all important question: what’s the point? We now have a properly defined parser type, one that can parse efficiently and incrementally, but does it give us anything new over existing tools?
Now that we’ve looked at a bunch of parsers that are at our disposal, let’s ask ourselves what a parser really is from the perspective of functional programming and functions. We’ll take a multi-step journey and optimize using Swift language features.
Parsing is a difficult, but surprisingly ubiquitous programming problem, and functional programming has a lot to say about it. Let’s take a moment to understand the problem space of parsing, and see what tools Swift and Apple gives us to parse complex text formats.
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.
This week we’ll put the finishing touches on our enum property code generation tool. We’ll add support for enum cases with multiple associated values and enum cases with no associated values, and we’ll add a feature that will make enums even more ergonomic to work with!
We’ve seen how “enum properties” help close the gap between the ergonomics of accessing data on structs and enums, but defining them by hand requires a lot of boilerplate. This week we join forces with Apple’s Swift Syntax library to generate this boilerplate automatically!
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.
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.
This week we finally make our untestable Gen type testable. We’ll compare several different ways of controlling Gen, consider how they affect Gen’s API, and find ourselves face-to-face with yet another
Finishing our 3-part answer to the all-important question “what’s the point?”, we finally show that standing on the foundation of our understanding of
flatMap we can now ask and concisely answer very complex questions about the nature of these operations.
Continuing our 3-part answer to the all-important question “what’s the point?”, we show that the definitions of
flatMap are precise and concisely describe their purpose. Knowing this we can strengthen our APIs by not smudging their definitions when convenient.
We are now ready to answer the all-important question: what’s the point? We will describe 3 important ideas that are now more accessible due to our deep study of
flatMap. We will start by showing that this trio of operations forms a kind of functional, domain-specific language for data transformations.
Now that we know that
flatMap is important for flattening nested arrays and optionals, we should feel empowered to define it on our own types. This leads us to understanding its structure more in depth and how it’s different from
Previously we’ve discussed the
zip operations in detail, and today we start completing the trilogy by exploring
flatMap. This operation is precisely the tool needed to solve a nesting problem that
zip alone cannot.
Our snapshot testing library is now officially open source! In order to show just how easy it is to integrate the library into any existing code base, we add some snapshot tests to a popular open source library for attributed strings. This gives us the chance to see how easy it is to write all new, domain-specific snapshot strategies from scratch.
The snapshot testing library we have been designing over the past few weeks has a serious problem: it can’t snapshot asynchronous values, like web views and anything that uses delegates or callbacks. Today we embark on a no-regret refactor to fix this problem with the help of a well-studied and well-understood functional type that we have discussed numerous times before.
We previously refactored a library using protocols to make it more flexible and extensible but found that it wasn’t quite as flexible or extensible as we wanted it to be. This week we re-refactor our protocols away to concrete datatypes using our learnings from earlier in the series.
With our library fully generalized using protocols, we show off the flexibility of our abstraction by adding new conformances and functionality. In fleshing out our library we find out why protocols may not be the right tool for the job.
Perhaps the most popular approach to code reuse and extensibility in Swift is to liberally adopt protocol-oriented programming, and many Swift libraries are designed with protocol-heavy APIs. In today’s episode we refactor a sample library to use protocols and examine the pros and cons of this approach.
We complete our dictionary for translating Swift protocol concepts into concrete datatypes and functions. This includes protocol inheritance, protocol extensions, default implementations and protocols with associated types. Along the way we will also show how concrete types can express things that are currently impossible with Swift protocols.
Now that we know it’s possible to replace protocols with concrete datatypes, and now that we’ve seen how that opens up new ways to compose things that were previously hidden from us, let’s go a little deeper. We will show how to improve the ergonomics of writing Swift in this way, and show what Swift’s powerful conditional conformance feature is represented by just plain functions.
Last time we covered some basics with protocols, and demonstrated one of their biggest pitfalls: types can only conform to a protocol a single time. Sometimes it’s valid and correct for a type to conform to a protocol in many ways. We show how to remedy this by demonstrating that one can scrap any protocol in favor of a simple datatype, and in doing so opens up a whole world of composability.
Protocols are a great tool for abstraction, but aren’t the only one. This week we begin to explore the tradeoffs of using protocols by highlighting a few areas in which they fall short in order to demonstrate how we can recover from these problems using a different tool and different tradeoffs.
This week we compare our
Decodable solution to building random structures with a composable solution involving the
Gen type, exploring the differences and trade-offs of each approach. Along the way we’ll rediscover a familiar old friend with a brand new application.
This week we dive deeper into randomness and composition by looking to a seemingly random place: the
Decodable protocol. While we’re used to using the
Codable set of protocols when working with JSON serialization and deserialization, it opens the opportunity for so much more.
Randomness is a topic that may not seem so functional, but it gives us a wonderful opportunity to explore composition. After a survey of what randomness looks like in Swift today, we’ll build a complex set of random APIs from just a single unit.
Templating languages are the most common way to render HTML in web frameworks, but we don’t think they are the best way. We compare templating languages to the DSL we previously built, and show that the DSL fixes many problems that templates have, while also revealing amazing compositions that were previously hidden.
We finish our introduction to DSLs by adding two new features to our toy example: support for multiple variables and support for let-bindings so that we can share subexpressions within a larger expression. With these fundamentals out of the way, we will be ready to tackle a real-world DSL soon!
We interact with domain-specific languages on a daily basis, but what does it take to build your own? After introducing the topic, we will begin building a toy example directly in Swift, which will set the foundation for a future DSL with far-reaching applications.
In part two of our series on
zip we will show that many types support a
zip-like operation, and some even support multiple distinct implementations. However, not all
zips are created equal, and understanding this can lead to some illuminating properties of our types.
zip function comes with the Swift standard library, but its utility goes far beyond what we can see there. Turns out,
zip generalizes a function that we are all familiar with, and it can unify many seemingly disparate concepts. Today we begin a multipart journey into exploring the power behind
Join us for a tour of the code base that powers this very site and see what functional programming can look like in a production code base! We’ll walk through cloning the repo and getting the site running on your local machine before showing off some of the fun functional programming we do on a daily basis.
We use Swift playgrounds on this series as a tool to dive deep into functional programming concepts, but they can be so much more. Today we demonstrate a few tricks to allow you to use playgrounds for everyday development, allowing for a faster iteration cycle.
We often deal with collections that we know can never be empty, yet we use arrays to model them. Using the ideas from our last episode on algebraic data types, we develop a
NonEmpty type that can be used to transform any collection into a non-empty version of itself.
Our third installment of algebraic data types explores how generics and recursive data types manifest themselves in algebra. This exploration allows us to construct a useful, precise type that can be useful in everyday programming.
Let’s have some fun with the “environment” form of dependency injection we previously explored. We’re going to extract out a few more dependencies, strengthen our mocks, and use our Overture library to make manipulating the environment friendlier.
We revisit an old topic: styling UIKit components. Using some of the machinery we have built from previous episodes, in particular setters and function composition, we refactor a screen’s styles to be more modular and composable.
Today we’re going to control the world! Well, dependencies to the outside world, at least. We’ll define the “dependency injection” problem and show a lightweight solution that can be implemented in your code base with little work and no third party library.
Functional setters can be very powerful, but the way we have defined them so far is not super ergonomic or performant. We will provide a friendlier API to use setters and take advantage of Swift’s value mutation semantics to make setters a viable tool to bring into your code base today.
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.
Why does the
map function appear in every programming language supporting “functional” concepts? And why does Swift have two
map functions? We will answer these questions and show that
map has many universal properties, and is in some sense unique.
We typically model our data with very general types, like strings and ints, but the values themselves are often far more specific, like emails and ids. We’ll explore how this can lead to subtle runtime bugs and how we can strengthen these types in an ergonomic way using several features new to Swift 4.1.
While we unabashedly promote custom operators in this series, we understand that not every codebase can adopt them. Composition is too important to miss out on due to operators, so we want to explore some alternatives to unlock these benefits.
Swift 4.1 deprecated and renamed a particular overload of
flatMap. What made this
flatMap different from the others? We’ll explore this and how understanding that difference helps us explore generalizations of the operation to other structures and derive new, useful code!
We continue our explorations into algebra and the Swift type system. We show that exponents correspond to functions in Swift, and that by using the properties of exponents we can better understand what makes some functions more complex than others.
Key paths aren’t just for setting. They also assist in getting values inside nested structures in a composable way. This can be powerful, allowing us to make the Swift standard library more expressive with no boilerplate.
This week we explore how functional setters can be used with the types we build and use everyday. It turns out that Swift generates a whole set of functional setters for you to use, but it can be hard to see just how powerful they are without a little help.
The programs we write can be reduced to transforming data from one form into another. We’re used to transforming this data imperatively, with setters. There’s a strange world of composition hiding here in plain sight, and it has a surprising link to a familiar functional friend.
Most of the time we interact with code we did not write, and it doesn’t always play nicely with the types of compositions we have developed in previous episodes. We explore how higher-order functions can help unlock even more composability in our everyday code.
We bring tools from previous episodes down to earth and apply them to an everyday task: UIKit styling. Plain functions unlock worlds of composability and reusability in styling of UI components. Have we finally solved the styling problem?
Side effects: can’t live with ’em; can’t write a program without ’em. Let’s explore a few kinds of side effects we encounter every day, why they make code difficult to reason about and test, and how we can control them without losing composition.