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.
--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)
If you haven't touched TCA yet, we recommend the following sites.
** 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.
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
.
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 {}
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.
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.
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
. ** **
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.
Recommended Posts