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!

◀︎ Return to homepage