Chapter 03

Composable Architecture

Yasuhiro Inami
Yasuhiro Inami
2020.09.30に更新

Next, let's talk about Composable Architecture.

Since I only have a limited time in this talk and there is also another session for this specific topic, I will only cover the very key points of Composable Architecture that are interesting and unique from other architectures.


The overview of Composable Architecture will look like this slide.

As most of you probably know, this framework is developed by Point-Free team: Brandon Williams and Stephen Celis, which you can find their online video tutorials and documentation in the official homepage.
It is also Elm-like Architecture and built on top of SwiftUI and Combine,
so the basic concept is very similar to Harvest.

The main differences are as follows:

  1. Composable Architecture adopts Multi-Store architecture
  2. Swift Case Paths (Smart Prism)

Let's take a look at one by one.


This is the diagram of the SwiftUI view tree with Composable Architecture's Store attached to each view.

We have already seen the similar diagram in Harvest, but the difference here is that, in Composable Architecture, child view will also own its Store rather than borrowing the partial reference from the root store.

Each Store in a child view owns a partial copy of the parent's state, and synchronizes the data reactively using Combine.

By the way, each Store is actually a wrapper called ViewStore which I will mention in a minute.


Now, let's take a look at what happens when child view receives events (actions) from a user.

Unlike Harvest where actions will be sent directly up to the root node, Composable Architecture will send them 1-level up to the parent node instead, and then that parent will also send the actions up to the grandparent, and so on.


After actions reach the toplevel root store, its reducer will be invoked and generates a new state.
It will be set in root store, and then FRP takes care of sending a new partial state down to the child stores.
This is what I meant by reactive synchronization.

So, the dataflow in Composable Architecture is actually a parent-child communication (not a simple unidirectional dataflow), and this messaging scheme is more like React than Elm.


By the way, there is a caution that when child store sends action to the parent, that parent store will also invoke its own reducer as well, updating its internal state.


Eventually, there will be a possibility of the intermediate view getting re-rendered multiple times by 1. AppView's update and re-rendering children, and 2. intermediate store updates its state and re-renders itself.

So how Composable Architecture solves this problem is by adding ViewStore as a store wrapper to ignore the duplication by calling Combine's removeDuplicates.

In my opinion, using removeDuplicates is a bit costly operation (both in memory cache cost and the requirement of state's Equatable check), but there might be some hidden advantages that I don't know yet, so for now, I won't go too detail here.


Next, let's talk about Case Paths.

In short, if we have WritableKeyPath as struct's data accessors (getter and setter), CasePath is like data accessors for enum values.

And actually, we have already seen this in Optics!
WritableKeyPath (getter and setter) is really just a Lens, and CasePath can be thought as Prism.

Normally, when we use WritableKeyPath, we use backslash character e.g. \.someMember as a syntax sugar.
In CasePath, on the other hand, it has a custom prefix func / so that we can call e.g. /.someCase.

Since CasePath and WritableKeyPath are dual to each other, using a forward-slash instead of backslash looks like a dual syntax which I like about this syntax.


Now, let's take a look at the structure of CasePath.

Then we will see 2 functions: embed and extract, and this is exactly the same shape as Prism! (build and tryGet)

That means, to understand more about CasePath, it's good to know about Optics too.

What's even more nicer about CasePath is that it's not just a simple Prism replacement: We can call it as Smart Prism.
What do I mean by that?
Well, Prism normally requires 2 functions as I mentioned above, but for CasePath, we only require embed function so that extract can be automagically derived from it.

How is that possible?
The secret is in the code below, which uses Swift Mirror (reflection) and C memory access operations.
It's magic inside 🎩

By using this technique, we can remove a lot of boilerplate code to setup all possible Prism instances without using a code-generation e.g. swift-enum-properties.


So, to summarize, "Multi-Store" and "Case Paths" are the 2 points that I found interesting in Composable Architecture.

To know more about it, please also check out another Composable Architecture session by @yimajo for more details (NOTE: It's in Japanese)