Next Up
Unlock This Episode
Our Free plan includes 1 subscriber-only episode of your choice, plus weekly updates from our newsletter.
Introduction
Now that we have the basics of our architecture in place we can start to explore things to do with it that unlock capabilities that were not even possible in the old way of making the app. There is a concept that we have discussed a number of times on Point-Free known as “higher-order constructions.” This is where you take some construction that you have been studying and lift it to a higher-order by considering functions that take that object as input and return that object as output. The canonical example is “higher-order functions”, which are functions that take functions as input and return functions as output.
But on Point-Free we’ve also considered “higher-order random number generators”, which were functions that took our Gen type as input and returned the Gen type as output. And we’ve considered “higher-order parsers”, which are functions that take parsers as input and return parsers as output. Each time you form one of these higher-order constructions you gain the ability to unlock something new that the vanilla constructions could not do alone.
Subscribe to Point-Free
Access this episode, plus all past and future episodes when you become a subscriber.
Already a subscriber? Log in
Exercises
Create a higher-order reducer with the following signature:
func filterActions<Value, Action>(_ predicate: @escaping (Action) -> Bool) -> (@escaping (inout Value, Action) -> Void) -> (inout Value, Action) -> Void { fatalError("Unimplemented") }
This allows you to transform any reducer into one that only listens to certain actions.
Solution
func filterActions<Value, Action>(_ predicate: @escaping (Action) -> Bool) -> (@escaping (inout Value, Action) -> Void) -> (inout Value, Action) -> Void { return { reducer in return { value, action in if predicate(action) { reducer(&value, action) } } } }
Create a higher-order reducer that adds the functionality of undo to any reducer. You can start by providing new types to augment the existing state and actions of a reducer:
struct UndoState<Value> { var value: Value var history: [Value] var canUndo: Bool { !self.history.isEmpty } } enum UndoAction<Action> { case action(Action) case undo }
And then implement the following function to implement the functionality:
func undo<Value, Action>( _ reducer: @escaping (inout Value, Action) -> Void ) -> (inout UndoState<Value>, UndoAction<Action>) -> Void { fatalError("Unimplemented") }
Solution
func undo<Value, Action>( _ reducer: @escaping (inout Value, Action) -> Void ) -> (inout UndoState<Value>, UndoAction<Action>) -> Void { return { undoState, undoAction in switch undoAction { case let .action(action): var currentValue = undoState.value reducer(¤tValue, action) undoState.history.append(currentValue) case .undo: guard undoState.canUndo else { return } undoState.value = undoState.history.removeLast() } } }
Enhance the undo higher-order reducer so that it limits the size of the undo history.
Solution
func undo<Value, Action>( _ reducer: @escaping (inout Value, Action) -> Void, limit: Int ) -> (inout UndoState<Value>, UndoAction<Action>) -> Void { return { undoState, undoAction in switch undoAction { case let .action(action): var currentValue = undoState.value reducer(¤tValue, action) undoState.history.append(currentValue) if undoState.history.count > limit { undoState.history.removeFirst() } case .undo: guard undoState.canUndo else { return } undoState.value = undoState.history.removeLast() } } }
Enhance the undo higher-order reducer to also allow redoing.
Solution
In order to keep track of redoes, the state can be modified to track what’s been undone and whether or not there are things to be redone:
struct UndoState<Value> { var value: Value var history: [Value] var undone: [Value] var canUndo: Bool { !self.history.isEmpty } var canRedo: Bool { !self.undone.isEmpty } }
Meanwhile, we need a new action to redo:
enum UndoAction<Action> { case action(Action) case undo case redo }
And finally, the higher-order reducer must handle the
redo
:func undo<Value, Action>( _ reducer: @escaping (inout Value, Action) -> Void, limit: Int ) -> (inout UndoState<Value>, UndoAction<Action>) -> Void { return { undoState, undoAction in switch undoAction { case let .action(action): var currentValue = undoState.value reducer(¤tValue, action) undoState.history.append(currentValue) undoState.undone = [] if undoState.history.count > limit { undoState.history.removeFirst() } case .undo: guard undoState.canUndo else { return } undoState.undone.append(undoState.value) undoState.value = undoState.history.removeLast() case .redo: guard undoState.canRedo else { return } undoState.history.append(undoState.value) undoState.value = undoState.undone.removeFirst() } } }
Add undo and redo buttons to the
CounterView
, and make them undo and redo only the counter actions on that screen.
References
Contravariance
Brandon Williams & Stephen Celis • Monday Apr 30, 2018We first explored the concept of the pullback
in our episode on “contravariance”, although back then we used a different name for the operation. The pullback
is an instrumental form of composition that arises in certain situations, and can often be counter-intuitive at first sight.
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.
Elm: A delightful language for reliable webapps
Elm is both a pure functional language and framework for creating web applications in a declarative fashion. It was instrumental in pushing functional programming ideas into the mainstream, and demonstrating how an application could be represented by a simple pure function from state and actions to state.
Redux: A predictable state container for JavaScript apps.
The idea of modeling an application’s architecture on simple reducer functions was popularized by Redux, a state management library for React, which in turn took a lot of inspiration from Elm.
Composable Reducers
Brandon Williams • Tuesday Oct 10, 2017A talk that Brandon gave at the 2017 Functional Swift conference in Berlin. The talk contains a brief account of many of the ideas covered in our series of episodes on “Composable State Management”.