Categories
Front-End Software

Shifting from OOP Web Components to FP: A brief look at Elm

A couple of months ago, someone asked on the JavaScript subreddit how functional programmers handle dealing with unavoidably immutable actions, like DOM manipulation. I gave my $0.02, saying that imposing functional programming constraints on your DOM-specific code is more trouble than it’s worth, and to leverage FP on the boundaries of your code that don’t interact with it. I ended with a recommendation for Elm, which is a neat little functional language that compiles to JavaScript, if someone is fully intent on writing more of their application in a functional style.

Recently I’ve spent some time playing more with Elm, and I’ve actually been enjoying it quite a lot! The documentation has improved immensely over the last few years and it is a lot more friendly getting on-boarded. I learned functional programming about 6 years ago at university — learning Haskell for the FP course and Principles of Programming Languages course) — but never spent time applying it in a UI context, and still found the documentation/supplementary material on Elm a bit lacking (until recently). So today I thought I’d outline some of the principle differences in building UIs between the component-based model (which we see in the likes of React, Vue.js, etc…) and Elm.

Note: this article will assume knowledge of JSX for the component examples, as well as and basic knowledge of Elm’s syntax for the Elm code samples.

The Component Model and Object Orientation

When building modern web components, we use a distinctly Object Oriented model. We decompose the application into a collection of components, which represent a fragment of the overall user interface and handle some part of the application’s state. We wire these components together by structuring them in a tree — as we do with HTML — and then allow the components to communicate with each other via a defined interface. In OOP this interface is the publicly exposed object methods, and with web components this is the publicly subscribable component events. This can be accessed programatically (e.g. elem.addEventListener() or, more commonly via passing callback functions as attributes to the respective elements (e.g. <elem onSomeEvent={myCallback}/>)). We pass data from parents to children via attributes (also called props in some frameworks), and from children to parents via callbacks; updating local state where necessary, and re-rendering the subtree where necessary.

In the case of a basic todo-list application, you’d probably have a <Todo> component which represents an individual item which you can "tick" off as done, change the description for, or delete. You’d also have a <TodoList> component which renders a list of Todo components based on its internal list of todo objects. Then in your top level <App>, you might have a <form> that users can type text into, and when they submit the form it adds the typed text as a new todo.

class App extends React.Component {
    // omitted other methods for brevity...
    render() {
        return <div>
            <form onSubmit={this.addTodo}>
                <input value={this.inputText} onChange={this.handleInputText}/>
            </form>
            <TodoList todos={this.state.todos}/>
        </div>;
    }
}
class TodoList extends React.Component {
    // ...
    render() {
        return <ul>
            {this.state.todos.map(todo => (<li>
                <Todo
                    id={todo.id}
                    description={todo.description}
                    done={todo.done}
                    onChange={this.handleChange}
                    onDelete={this.handleDelete}
                />
            <li>))}
        </ul>;
    }
}
class Todo extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            mode: "view",
            description: props.description,
        }

        // ...
    }

    // ...

    render() {
        switch (this.state.mode) {
            case "edit":
                return <div class="edit-mode">
                    <input value={this.state.description} onChange={this.handleChange}/>
                </div>;
            default:
                return <div>
                    <input type="checkbox" checked={this.props.done} onChange={this.props.onChange}/>
                    <span>{this.props.description}</span>
                    <a href="#" onClick={this.enableEditMode}>Edit</a>
                    <a href="#" onClick={this.props.onDelete}>Delete</a>
                </div>;
        }
    }
}

This model is great for reducing the complexity of a massive user interface because it allows us to think of it in terms of smaller — but still useful — "blocks", which slot together to form the overall application. You don’t need to have the whole application "in your head" in order to build it – you can build it out in isolated pieces, then wire together the desired pieces. You can (read: should!) write tests on this "wiring" to ensure that the components behave together as expected, but this isn’t a massive trade-off to make when it can hugely increase the cognitive ease of understanding the application.

That trade-off however comes with an acknowledgement that understanding the full state-space in a predictable, reliable fashion, is almost impossible beyond very simple applications. In some cases, this can be quite undesirable. For instance, if there is an entire class of bugs that can arise from my application entering particular states, how can I safely prevent the user from ever being able to enter these states, when it requires synchronisation across so many component boundaries?