Skip to content

Commit b8ce649

Browse files
committed
Initial Commit
0 parents  commit b8ce649

File tree

12 files changed

+1647
-0
lines changed

12 files changed

+1647
-0
lines changed

.gitignore

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj
5+
xcuserdata/
6+
DerivedData/
7+
.swiftpm/config/registries.json
8+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9+
.netrc
10+
11+
coverage.txt
12+
.coveralls.yml
13+
**/test_reports
14+
.DS_Store?
15+
._*
16+
.Spotlight-V100
17+
.Trashes
18+
ehthumbs.db
19+
Thumbs.db
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Scheme
3+
LastUpgradeVersion = "1400"
4+
version = "1.3">
5+
<BuildAction
6+
parallelizeBuildables = "YES"
7+
buildImplicitDependencies = "YES">
8+
<BuildActionEntries>
9+
<BuildActionEntry
10+
buildForTesting = "YES"
11+
buildForRunning = "YES"
12+
buildForProfiling = "YES"
13+
buildForArchiving = "YES"
14+
buildForAnalyzing = "YES">
15+
<BuildableReference
16+
BuildableIdentifier = "primary"
17+
BlueprintIdentifier = "Reducer"
18+
BuildableName = "Reducer"
19+
BlueprintName = "Reducer"
20+
ReferencedContainer = "container:">
21+
</BuildableReference>
22+
</BuildActionEntry>
23+
<BuildActionEntry
24+
buildForTesting = "YES"
25+
buildForRunning = "YES"
26+
buildForProfiling = "NO"
27+
buildForArchiving = "NO"
28+
buildForAnalyzing = "YES">
29+
<BuildableReference
30+
BuildableIdentifier = "primary"
31+
BlueprintIdentifier = "ReducerTests"
32+
BuildableName = "ReducerTests"
33+
BlueprintName = "ReducerTests"
34+
ReferencedContainer = "container:">
35+
</BuildableReference>
36+
</BuildActionEntry>
37+
</BuildActionEntries>
38+
</BuildAction>
39+
<TestAction
40+
buildConfiguration = "Debug"
41+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
42+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
43+
shouldUseLaunchSchemeArgsEnv = "YES"
44+
codeCoverageEnabled = "YES"
45+
onlyGenerateCoverageForSpecifiedTargets = "YES">
46+
<CodeCoverageTargets>
47+
<BuildableReference
48+
BuildableIdentifier = "primary"
49+
BlueprintIdentifier = "Reducer"
50+
BuildableName = "Reducer"
51+
BlueprintName = "Reducer"
52+
ReferencedContainer = "container:">
53+
</BuildableReference>
54+
</CodeCoverageTargets>
55+
<Testables>
56+
<TestableReference
57+
skipped = "NO">
58+
<BuildableReference
59+
BuildableIdentifier = "primary"
60+
BlueprintIdentifier = "ReducerTests"
61+
BuildableName = "ReducerTests"
62+
BlueprintName = "ReducerTests"
63+
ReferencedContainer = "container:">
64+
</BuildableReference>
65+
</TestableReference>
66+
</Testables>
67+
</TestAction>
68+
<LaunchAction
69+
buildConfiguration = "Debug"
70+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
71+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
72+
launchStyle = "0"
73+
useCustomWorkingDirectory = "NO"
74+
ignoresPersistentStateOnLaunch = "NO"
75+
debugDocumentVersioning = "YES"
76+
debugServiceExtension = "internal"
77+
allowLocationSimulation = "YES">
78+
</LaunchAction>
79+
<ProfileAction
80+
buildConfiguration = "Release"
81+
shouldUseLaunchSchemeArgsEnv = "YES"
82+
savedToolIdentifier = ""
83+
useCustomWorkingDirectory = "NO"
84+
debugDocumentVersioning = "YES">
85+
<MacroExpansion>
86+
<BuildableReference
87+
BuildableIdentifier = "primary"
88+
BlueprintIdentifier = "Reducer"
89+
BuildableName = "Reducer"
90+
BlueprintName = "Reducer"
91+
ReferencedContainer = "container:">
92+
</BuildableReference>
93+
</MacroExpansion>
94+
</ProfileAction>
95+
<AnalyzeAction
96+
buildConfiguration = "Debug">
97+
</AnalyzeAction>
98+
<ArchiveAction
99+
buildConfiguration = "Release"
100+
revealArchiveInOrganizer = "YES">
101+
</ArchiveAction>
102+
</Scheme>

Package.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// swift-tools-version: 5.7
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "Reducer",
6+
platforms: [.macOS(.v10_13), .iOS(.v11), .tvOS(.v11), .watchOS(.v4)],
7+
products: [
8+
.library(name: "Reducer", targets: ["Reducer"])
9+
],
10+
targets: [
11+
.target(name: "Reducer"),
12+
.testTarget(name: "ReducerTests", dependencies: ["Reducer"])
13+
],
14+
swiftLanguageVersions: [.v5]
15+
)

README.md

Lines changed: 276 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
extension Reducer {
2+
/**
3+
Composes two or more reducers in series, to be evaluated from the left to the right for each incoming action.
4+
5+
When composing reducer A with reducer B, when an action X arrives, first it will be forwarded to
6+
reducer A together with the initial state. This reducer may return a slightly (or completely) changed state from
7+
that operation, and this state will then be forwarded to reducer B together with the same action X. If you change
8+
the order, results may vary as you can imagine. Monoids don't necessarily hold the commutative axiom, although
9+
sometimes they do. What they necessarily hold is the associativity axiom, which means that if you compose A and B,
10+
and later C, it's exactly the same as if you compose A to a previously composed B and C:
11+
`.compose(.compose(A, B), C) == .compose(A, .compose(B, C))`. So please don't worry about surrounding your reducers with parenthesis:
12+
```
13+
let globalReducer = .compose(firstReducer, secondReducer, thirdReducer, andSoOn)
14+
```
15+
16+
- Parameters:
17+
- first: First reducer `(ActionType, inout StateType) -> Void`, let's call it `f(x)`
18+
- others: Second, Third, nth reducers `(ActionType, inout StateType) -> Void`, let's call it `g(x)`
19+
- Returns: a composed reducer `(ActionType, inout StateType) -> Void` equivalent to `g(f(x))`
20+
*/
21+
public static func compose(_ first: Reducer, _ others: Reducer...) -> Reducer {
22+
.reduce { action, state in
23+
first.reduce(action, &state)
24+
others.forEach { $0.reduce(action, &state) }
25+
}
26+
}
27+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
extension Reducer {
2+
/**
3+
No-op reducer. Composing it with any other reducer will not change anything from the other reducer behaviour, regardless if the identity reducer
4+
is on the left-hand side or the right-hand side or this composition. This is the neutral element in a monoidal composition.
5+
6+
Therefore:
7+
```
8+
.compose( Reducer<ActionType, StateType>, .identity )
9+
== .compose( .identity, Reducer<ActionType, StateType> )
10+
== Reducer<ActionType, StateType>
11+
```
12+
13+
This is useful for composition purposes, for example when you call a function `Array.reduce` in an array of Reducers and you need a no-op start:
14+
```
15+
[reducer1, reducer2].reduce(.identity) { accumulator, nextReducer in
16+
Reducer.compose(accumulator, nextReducer)
17+
}
18+
// .identity won't have any behaviour and the final composition ".identity >>> reducer1, reducer2" will be as if .identity wasn't there.
19+
```
20+
21+
The implementation of this reducer, as one should expect, simply ignores the action and returns the state unchanged
22+
*/
23+
public static var identity: Reducer<ActionType, StateType> {
24+
.init { _, _ in }
25+
}
26+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
extension Reducer {
2+
/**
3+
A type-lifting method. The global state of your app is _Whole_, and the `Reducer` handles _Part_, that is a
4+
sub-state.
5+
6+
Let's suppose you may want to have a `gpsReducer` that knows about the following `struct`:
7+
```
8+
struct Location {
9+
let latitude: Double
10+
let longitude: Double
11+
}
12+
```
13+
14+
Let's call it _Part_. Both, this state and its reducer will be part of an external framework, used by dozens of
15+
apps. Internally probably the `Reducer` will receive some known `ActionType` and calculate a new location. On the
16+
main app we have a global state, that we now call _Whole_.
17+
18+
```
19+
struct MyGlobalState {
20+
let title: String?
21+
let listOfItems: [Item]
22+
let currentLocation: Location
23+
}
24+
```
25+
26+
As expected, _Part_ (`Location`) is a property of _Whole_ (`MyGlobalState`). This relationship could be less
27+
direct, for example there could be several levels of properties until you find the _Part_ in the _Whole_, like
28+
`global.firstLevel.secondLevel.currentLocation`, but let's keep it a single-level for this example.
29+
30+
Because our `Store` understands _Whole_ (`MyGlobalState`) and our `gpsReducer` understands _Part_ (`Location`), we
31+
must `lift` the `Reducer` to the _Whole_ level, by using:
32+
33+
```
34+
let globalStateReducer = gpsReducer.lift(
35+
actionGetter: { $0 },
36+
stateGetter: { global in global.currentLocation },
37+
stateSetter: { global, part in global.currentLocation = path }
38+
)
39+
// where:
40+
// globalStateReducer: Reducer<MyAction, MyGlobalState>
41+
// ↑ lift
42+
// gpsReducer: Reducer<MyAction, Location>
43+
```
44+
45+
Now this reducer can be used within our `Store` or even composed with others. It also can be used in other apps as
46+
long as we have a way to lift it to the world of _Whole_.
47+
48+
Same strategy works for the `action`, as you can guess by the `actionGetter` parameter. You can provide a function
49+
that takes a global action (_Whole_) and returns an optional local action (_Part_). It's optional because perhaps
50+
you want to ignore actions that are not relevant for this reducer.
51+
52+
- Parameters:
53+
- actionGetter: a way to convert a global action into a local action, but it's optional because maybe this
54+
reducer shouldn't care about certain actions. Because actions are usually enums, you can switch
55+
over the enum and in case it's nothing you care about, you simply return nil in the closure. If
56+
you don't want to lift this reducer in terms of `action`, just provide the identity function
57+
`{ $0 }` as input.
58+
- stateGetter: a way to read from a global state and extract only the part that it's relevant for this reducer,
59+
by traversing the tree of the global state until you find the property you want, for example:
60+
`{ $0.currentGame.scoreBoard }`
61+
- stateSetter: a way to write back into the global state once you finished reducing the _Part_, so now you have
62+
a new part that was calculated by this reducer and you want to set it into the global state, also
63+
provided as the first parameter as an `inout` property:
64+
`{ globalState, newScoreBoard in globalState.currentGame.scoreBoard = newScoreBoard }`
65+
- Returns: a `Reducer<GlobalAction, GlobalState>` that maps actions and states from the original specialised
66+
reducer into a more generic and global reducer, to be used in a larger context.
67+
*/
68+
public func lift<GlobalActionType, GlobalStateType>(
69+
actionGetter: @escaping (GlobalActionType) -> ActionType?,
70+
stateGetter: @escaping (GlobalStateType) -> StateType,
71+
stateSetter: @escaping (inout GlobalStateType, StateType) -> Void)
72+
-> Reducer<GlobalActionType, GlobalStateType> {
73+
.reduce { globalAction, globalState in
74+
guard let localAction = actionGetter(globalAction) else { return }
75+
var localState = stateGetter(globalState)
76+
self.reduce(localAction, &localState)
77+
stateSetter(&globalState, localState)
78+
}
79+
}
80+
}

0 commit comments

Comments
 (0)