[SWIFT] What is Pullback doing in The Composable Architecture

Introduction

In understanding the design pattern The Composable Architecture (hereinafter TCA), which has been attracting attention with the advent of Swift UI recently, it takes a little time to understand the processing performed by Pullback while there are important functions such as Effect and Store. It took me so I thought I'd summarize the details.

Target

--Those who understand the outline of TCA --Those who want to deepen their understanding of Pullback and combine -Point-Free does not take an hour for those who want to review briefly (it is definitely better to see)

Those who do not know TCA yet

If you haven't touched TCA yet, we recommend the following sites.

What is Pullback doing

** By taking out the State and Action of the Reducer, which are divided into smaller parts, and changing the type to the AppState and AppAction that can be used globally, the separate Reducers can be integrated. ** **

Why change to a reducer that can be used globally in the first place? How to get the State out. How to retrieve Action.

I would like to take a look at these.

Reducer divided into small pieces

Let's take a look at a simple count-up app as a sample. (For the sake of clarity, the function has been slightly changed from Point-Free Countup App)

function --Count up / countdown --Judgment of prime numbers

AppReducer

Now, suppose you have a appReducer that is responsible for all the business logic of your app.

func appReducer(value: inout AppState, action: AppAction) -> Void {
  switch action {
  case .counter(.decrTapped):
    //Countdown

  case .counter(.incrTapped):
    //count up

  case .primeModal(.calculateIsPrimeTapped):
    //Primality test

  case .primeModal(.saveFavoritePrimeTapped):
    //Register the number judged as a prime number as a favorite

  case let .favoritePrimes(.deleteFavoritePrimes(indexSet)):
    //Remove from favorites
  }
}

You can see that AppAction has 3 cases (.counter, .primeModal, .favoritePrimes). If each Action is fired from a separate screen, I think it is natural to want to ** divide the Reducer for each function ** in anticipation of the future expansion of the appReducer.

Split appReducer

For example, in the case of counterReducer, it is unnecessary except for State and Action required for counting. In other words, if each Reducer has a global AppState and AppAction when splitting the Reducer, the value it has for that Reducer's role has too much influence. Therefore, it is cut out into individual State and Action respectively.

func counterReducer(value: inout CounterState, action: CounterAction) -> Void {}
func primeModalReducer(value: inout PrimeModalState, action: PrimeModalAction) -> Void {}
func favoriteReducer(value: inout FavoriteState, action: FavoriteAction) -> Void {}

Integrate split Reducers

The split is good, but in the end you have to integrate the split Reducer as a appReducer that receives the AppState and the AppAction. Therefore, combine is used to integrate each Reducer.


func combine<Value, Action>(
//You can deliver as many Reducers as you like
  _ reducers: (inout Value, Action) -> Void...
) -> (inout Value, Action) -> Void {
  return { value, action in
    for reducer in reducers {
      reducer(&value, action)
    }
  }
}

// But..
let appReducer = combine(
  counterReducer,
  primeModalReducer,
  favoriteREducer
)
// Error - Cannot convert value

But there is a problem. combine cannot be done unless the Value and Action of each Reducer to combine are the same. In other words, you must finally change the values ​​that are currently divided into CounterState and FavoriteState to the types of AppState and AppAction to make them integrated. ** **

That's where the Pullback process goes.

State Pullback

First, let's look at the Pullback of State. Add each State as a Computed Property in AppState.

struct AppState {
  var counterState: CounterState {
    get { //.. }
    set { //.. }
  }
  var primeModalState: PrimeModalState {
    get { //.. }
    set { //.. }
  }
  //..
}

This will allow you to get the counterState as a WritableKeyPath.

//Pull back State
func pullback<LocalValue, GlobalValue, Action>(
    //Passed reducer and its Local Value(State)
  _ reducer: @escaping (inout LocalValue, Action) -> Void,
  value: WritableKeyPath<GlobalValue, LocalValue>
) -> (inout GlobalValue, Action) -> Void {
  return { globalValue, action in
    reducer(&globalValue[keyPath: value], action)
  }
}

let appReducer = combine(
  pullback(counterReducer, value: \.counterState),
  pullback(primeModalReducer, value: \.primeModalState),
  //..
)

With this, the State of each Reducer is different, but it seems that they can be integrated.

However, in this implementation, it is assumed that ** Action is the same type just by pulling back State. ** ** Next, let's look at the implementation that pulls back including Action.

Action Pullback

The difference between Action and State is that ** State is struct and Action is enum. ** ** In other words, the Keypath used at the time of State cannot be used.

Therefore, it is OSS, pointfreeco/swift-case-paths, which is also published by Point-Free, that enables Keypath in enum.

//Pull back both State and Action
func pullback<GlobalValue, LocalValue, GlobalAction, LocalAction>(
  _ reducer: @escaping (inout LocalValue, LocalAction) -> Void,
  value: WritableKeyPath<GlobalValue, LocalValue>,
  action: CasePath<GlobalAction, LocalAction>
) -> (inout GlobalValue, GlobalAction) -> Void {
  return { globalValue, globalAction in
    guard let localAction = action.extract(from: globalAction) else { return }
    reducer(&globalValue[keyPath: value], localAction)
  }
}

enum AppAction {
  case counter(CounterAction)
  case primeModal(PrimeModalAction)
  //..
}

let appReducer = combine(
  pullback(counterReducer, value: \.counterState, action: \.counter)
  pullback(primeModalReducer, value: \.primeModalState, action: \.primeModal)
  //..
)

Finally, I was able to pull back both State and Action to ** AppState and AppAction. ** **

About the implementation of Pullback in the first place

Pullback does the opposite of map, so we renamed it to Pullback to make it intuitively easier to understand what Point-Free originally defined as contramap. This name was chosen because it is based on the concept of category theory "pullback" = Pullback. That's all for this article, but if you are interested in the idea of ​​Pullback in the first place, it may be interesting to read the following article.

The above is my understanding, so if you have any suggestions, I would be grateful if you could comment.

reference

Recommended Posts

What is Pullback doing in The Composable Architecture
What is the main method in Java?
What is the pluck method?
What is the BufferedReader class?
What is the constructor for?
What is the initialize method?
What is the representation of domain knowledge in the [DDD] model?
What is the difference between the responsibilities of the domain layer and the application layer in the onion architecture [DDD]
'% 02d' What is the percentage of% 2?
What is a snippet in programming?
What is @Autowired in Spring boot?
What are the rules in JUnit?
What to do if Cloud9 is full in the Rails tutorial
What is @Override or @SuppressWarnings ("SleepWhileInLoop") in front of the function? ?? ??
What is a class in Java language (3 /?)
Spring Autowired is written in the constructor
What is Swift? Findings obtained in 3 weeks
What you are doing in the confirmation at the time of gem update
What to do if the prefix c is not bound in JSP
What is the right migration data type? ??
Understand what Dagger2's Android support is doing
What is testing? ・ About the importance of testing
[Technical memo] What is "include" in Ruby?
What is the best file reading (Java)
What is a class in Java language (1 /?)
What is a class in Java language (2 /?)
What is the model test code testing
What is the data structure of ActionText?
What is the Facade pattern useful for?
What is gem'foge', require: false in Gemfile?
What is CHECKSTYLE: OFF found in the Java source? Checkstyle to know from
[Beginner] What is Docker in the first place? Easy-to-understand explanation from the basics!
Androd: What to do about "The Realm is already in a write transaction in"
Retrieve the first day of week in current locale (what day of the week is it today?)
What is ... (3 dots) found in the Java source? Variadic arguments to know from
What is JSP? ~ Let's know the basics of JSP !! ~
What is the Java Servlet / JSP MVC model?
The ruby version is managed in the .rbenv / version file
What is different from the PHP language. [Note]
The intersection type introduced in Java 10 is amazing (?)
What is the volatile modifier for Java variables?
What is the difference between SimpleDateFormat and DateTimeFormatter? ??
After all, what is [rails db: migrate] doing?
[Java] Something is displayed as "-0.0" in the output
When the project is not displayed in eclipse
Ebean.update () is not executed in the inherited model.
What is Cubby
What is Docker?
What is null? ]
What is java
What is Keycloak
What is maven?
What is Jackson?
Get the value from the array and find out what number it is included in
What is Docker
What is self
What is Jenkins
What is ArgumentMatcher?
What is IM-Juggling?
What is params
What is SLF4J?