Skip to content

Instantly share code, notes, and snippets.

@BinaryMuse
Forked from evancz/Architecture.md
Created March 24, 2016 02:45
Show Gist options
  • Select an option

  • Save BinaryMuse/4ad1bbf8a97b9b5e49c9 to your computer and use it in GitHub Desktop.

Select an option

Save BinaryMuse/4ad1bbf8a97b9b5e49c9 to your computer and use it in GitHub Desktop.
Ideas and guidelines for architecting larger applications in Elm to be modular and extensible

Architecture in Elm

This document is a collection of concepts and strategies to make your Elm projects modular and extensible.

We will start by thinking about the structure of signals in our program. Broadly speaking, your application state should live in one big foldp. You will probably merge a bunch of input signals into a single stream of updates. This sounds a bit crazy at first, but it is in the same ballpark as Om or Facebook's Flux. There are a couple major benefits to having a centralized home for your application state:

  1. There is a single source of truth. Traditional approaches force you to write a decent amount of custom and error prone code to synchronize state between many different stateful components. (The state of this widget needs to be synced with the application state, which needs to be synced with some other widget, etc.) By placing all of your state in one location, you eliminate an entire class of bugs in which two components get into inconsistent states. We also think you will end up writing much less code, at least, that has been our observation in Elm so far.

  2. Save and Undo become quite easy. Many applications would benefit from the ability to save all application state and send it off to the server so it can be reloaded at some later date. This is extremely difficult when your application state is spread all over the place and potentially tied to objects that cannot be serialized. With a central store, this becomes very simple. Many applications would also benefit from the ability to easily undo user's actions. For example, a painting app is better with Undo. Since everything is immutable in Elm, this is also very easy. Saving past states is trivial, and you will automatically get pretty good sharing guarantees to keep the size of the snapshots down.

I think these two strengths will be extremely worthwhile in large applications, though I feel that strength 1 is a huge deal for speeding up development and avoiding silly bugs that waste your time.

So most of your code will be pure functions that make your big foldp actually do the right thing. The rest of this document focuses on how to make that code modular and extensible.

Modularity

To make things modular, the major strategy is to hide implementation details, as shown in this pseudocode. When you create a widget, this makes it possible to expose exactly the API you want. Essentially just this kind of info:

type Model.State
type Model.Action

Model.initialize : String -> ... -> State

Update.step : Action -> State -> State
Update.resetField : State -> State

View.full : Input Action -> State -> Element
View.mini : Input Action -> State -> Element

As a user, you don't know anything about what State really is, but you have carefully selected functions for creating it, stepping it, and doing custom modifications without an Action (e.g. resetField) in case other components need to act on the state. The designer has full control over the API they expose and can hide any details they want.

In Elm you have the added benefit that these abstraction boundaries are quite strong. Unlike in JS, you cannot just inspect the structure of arbitrary values and do what you want with that. That means best practices must be enforced with culture or dogma. In Elm you can actually ensure that people write code in a good way.

Extensibility

When you have a ton of widgets, all with different sets of actions, how do you use them all together? You cannot have one giant ADT. There are a few techniques here.

Nesting

So lets say we have three different widgets, each with their code living in modules called SearchBar, Filters, and Results. This means we have three sets of actions SearchBar.Action, Filters.Action, and Results.Action which we do not know anything about. To put them together, we would create a meta action:

data Action
    = SearchBar SearchBar.Action
    | Filters Filters.Action
    | Results Results.Action

step : Action -> State -> State
step action state =
  case action of
    SearchBar a -> SearchBar.step a state.searchBar
    Filters a -> Filters.step a state.filters
    Results a -> Results.step a state.results

You can just keep nesting and nesting ADTs like this. For example the Results module may be made up of 4 smaller ones and we don't need to know anything about that.

Generalizing Actions

In some cases where you want an ADT to be more extensible, it may be good to include one case that is fully general:

data Action = RemoveTask Int | ... | Anything (State -> State)

I suspect this is not such a great idea in practice, but if you need to make your widget extensible to outsiders, some variation of this may be nice. Perhaps instead of exposing all internal state, you can expose a function that only has access to the parts you want people to know about. Or not expose any details about State and just provide some functions like (resize : Int -> Int -> State -> State).

I'm less confident that this will be great, but I do think it should be explored. I can see it going either way.

There are a couple other techniques, but I have thought about them less and will save them for later.

Specializing Inputs

I think it may be pleasant/necessary to introduce a function something like this:

specialize : (particular -> general) -> Input general -> Input particular

The idea is that you can create one input, but have all 3 of your widgets report to it with things like this:

searchBarInput = specialize SearchBar appInput filtersInput = specialize Filters appInput resultsInput = specialize Results appInput

It may also be a good idea to make input creation syntactic as with ports and to demand that they all be created in the Main module to ensure that people structure their code in a reusable way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment