The Elm Architecture in Swift (A sketch)
Because I generally dislike using Javascript, I try to investigate different ways of building Web user interfaces. A web-developer is quite spoiled at the moment. There are many frameworks and even languages that transpile to to Javascript.
For me, one of the better examples is the Elm language. It is built entirely for the task of implementing web user interfaces. It is a functional programing language of the ML family most directly inspired by Haskell, all be it with a simpler type-system.
A user interface created with Elm uses what is known as The Elm Architecture.. This is a style of programming often referred to as uni-directional-flow. The basic idea is that rather than a two way binding between the user interface and application logic, events from the UI propagate, are processed, and state is updated. The final step is updating the UI from that state. This differs in other models, where events in the UI result in it immediately being mutated/updated.
The Elm guide has a good overview of this style of UI programming. It makes a good case for how this model provides a method for building out complicated apps with the same pattern repeatedly. I would recommend reading a little about the architecture first, since I’ll be assuming some knowledge of it for the rest of this post.
What I Want
My main goals are mostly focussed on preserving the style of programming espoused in the Elm Architecture. Any deviations should only come from the differences between Elm and Swift. For example, Elm has a runtime which abstracts away certain operations e.g. side-effects.
- Remain functional in style; pure functions where possible
- Preserve uni-directional-flow
- Allow complex applications to be built up through the repeated composition of Model, View, Update
An Implementation
It turns out, the basics are actually quite simple. The most important types in my definition are the Component
and the App
. The Component
handles the Model, View and Update. The App
initialises the program, then handles actions and propagates updates to the components.
The only tricky part is the Dispatcher
type. This is used to provide a mechanism for sending actions from the views all the way up through the component stack to the app. To achieve this, each component generates a new dispatcher scoped to themselves, then passes it to the child components. This is clearer in the example below.
import Foundation
protocol ActionType {}
protocol StateType {}
protocol CommandType {}
// A protocol which describes the requirements for the Model/View/Update of the
// Elm Architecture. Very simply, it requires a type that implements three
// static functions.
protocol Component {
// The actions which propagate from the view. The component is free to use
// any type, but generally would be a custom `enum`
associatedtype ComponentAction: ActionType
// Much like the action type, state can be any type. Usually a struct, but
// could easily be a scalar value like an `Int`.
associatedtype ComponentState: StateType
// The view type is paramaterised. We don't assume a view type in the
// application. This allows any kind of UI-diffing framework to be plugged
// in.
associatedtype ComponentView
// Generates the initial state used to bootstrap a component.
static func initialState() -> ComponentState
// Handles the current state and an action, generating a new state.
static func update(state: ComponentState, action: ComponentAction) -> (ComponentState, CommandType?)
// Accepts the state and outputs a view. The `dispatcher` is the mechanism
// the views use to propagate actions.
static func view(state: ComponentState, dispatcher: Dispatcher<ComponentAction>) -> ComponentView
}
// The app class sits at the root of the application and coordinates the
// dispatch of actions, updates and re-rendering views.
class App<Root: Component, ViewType> where Root.ComponentView == ViewType {
var state: Root.ComponentState
init() {
state = Root.initialState()
}
func dispatch(action: Root.ComponentAction) {
let (update, command) = Root.update(state: state, action: action)
self.state = update
}
func view() -> ViewType {
return Root.view(state: state, dispatcher: Dispatcher { self.dispatch(action: $0) })
}
}
// The dispatcher isn't super interesting, so let's just say it helps propagate
// actions.
struct Dispatcher<Input: ActionType> {
private let closure: (Input) -> Void
init(_ closure: @escaping (Input) -> Void) {
self.closure = closure
}
func send(_ action: Input) {
self.closure(action)
}
func wrap<A: ActionType>(wrapper: @escaping (A) -> Input) -> Dispatcher<A> {
return Dispatcher<A> { a in
return self.closure(wrapper(a))
}
}
}
In Use
This example defines a root component, with a child component for a navigation bar. It shows a number of interesting things.
- How each component defines and handles it’s own state and view
- The mechanism for wrapping and propagating actions from child views
- That the view type is paramaterised via
typealias ComponentView
struct Root: Component {
// Each component defines it's own action and state types.
typealias ComponentAction = Action
typealias ComponentState = State
// The type of the view is defined with an alias, so it can be an NSView,
// UIView or for our example a String.
typealias ComponentView = String
// Here are the actions that can be sent from the root component.
enum Action: ActionType {
case begin
// This case is for wrapping actions from the NavBar child component.
case navBar(NavBar.Action)
}
// The root state, which in this example just wraps the NavBar state.
struct State: StateType {
let navBar: NavBar.State
}
static func initialState() -> State {
return State(navBar: NavBar.initialState())
}
static func update(state: State, action: Action) -> (State, CommandType?) {
switch action {
case .begin:
return (state, nil)
case let .navBar(navAction):
// Here we handle an action from the nav bar. This involves calling
// the NavBar's update function.
let (update, command) = NavBar.update(state: state.navBar, action: navAction)
return (State(navBar: update), command)
}
}
static func view(state: State, dispatcher: Dispatcher<Action>) -> String {
// We generate a new dispatcher which wraps the NavBar actions with
// a root action. This allows the update function to handle and unpack it.
let dispatcher = dispatcher.wrap { .navBar($0) }
// Rendering the view is simple here since it's a string. You can see
// how it is calling the NavBar's view function and passing it it's
// state and the newly scoped dispatcher.
return "Section: \(NavBar.view(state: state.navBar, dispatcher: dispatcher))"
}
}
struct NavBar: Component {
typealias ComponentAction = Action
typealias ComponentState = State
typealias ComponentView = String
enum Action: ActionType {
case selectBrowse
case selectPlaying
case selectSearch
}
struct State: StateType {
enum Section {
case browse
case playing
case search
}
let current: Section
}
static func initialState() -> State {
return State(current: .browse)
}
static func update(state: State, action: Action) -> (State, CommandType?) {
switch action {
case .selectBrowse: return (State(current: .browse), nil)
case .selectPlaying: return (State(current: .playing), nil)
case .selectSearch: return (State(current: .search), nil)
}
}
static func view(state: State, dispatcher: Dispatcher<Action>) -> String {
return "\(state.current)"
}
}
// Initialise the app with the root component and the view type.
let app = App<Root, String>()
// Render the initial view
let initial = app.view() // "Section: browse"
// Dispatch an action from the navbar
app.dispatch(action: Root.Action.navBar(.selectSearch))
// Re-render the view
let next = app.view() // "Section: search"
Challenges
The dispatcher pattern seems quite awkward to me. But I’m unsure of how to propagate an event from a view otherwise. I certainly don’t want to have views make reference to some global object. Perhaps it’s not so bad, it’s just it’s not magical like Elm :)
Side-effects are not handled in any way. The CommandType
is intended to help there — an update
function can return an updated state and a command to be run — but I haven’t explored this.
The view-type here is just a String. In theory it’s easy enough to replace it with some diffing-UI library, but how the cycle of action-update-render is unspecified. The example is just manually stepping through the cycle. It’s possible that this process could be deferred to an adapter of some kind.
Is it Worth it?
It may not be worth using in the end, but there is more to be explored yet, so the answer for me is yes. I have a number of other things I would like to explore before I offer a final verdict.
- Rendering and updating an actual UI
- Handling side-effects
- Adapting to different UI frameworks
- Handling styling, layout and animation in UIs
- Recording and playing back the history of updates for debugging
- Handling state that cuts across components
In the future I hope to expand this example a bit more to explore the above ideas. It might even turn into a real library!