Strongly typed, functional languages as an alternative to the popular React + Redux stack

tl;dr
tl;dr german

The unidirectional Elm architecture has seen a lot of usage since the rise of React + Redux. Its origins are in Elm, which is a purely functional, Haskell-like language. So why is this much used, functional architecture seldomly used with languages designed for functional programming? Are there real alternatives to the React + Redux stack?

To answer these questions, two functional programming languages were evaluated alongside TypeScript with React and Redux. One of those languages is Elm because of its deeper relationship with Redux (as mentioned above). The other language is Reason from Facebook (the company that made React) which is a functional language too but in a JavaScript dress. In our study project we explored if and where functional languages like Elm or Reason may be a real alternative for new projects which choose this unidirectional approach. As a result, we implemented a simple HTTP client in TypeScript with React + Redux, Reason with ReasonReact + Reductive and Elm and evaluated the process.

All the information given in this blog is the result of a study project conducted at the HSR in the fall semester of 2019. If you are reading this at a later time it is possible that not all the statements are still correct. The technical report is available in german (only a german version exists).

The Architecture

For this comparison we used the Elm architecture in all three languages. The Elm architecture is a unidirectional architecture which means that data only ever flows in one direction. The basic cycle is as follows:

  • The initial state gets rendered.
  • The rendered elements generate messages when the user interacts with the browser.
  • These messages change the state.
  • The new state gets rendered again. And so forth.
Elm architecture

This architecture is well suited for a functional environment because all the interactions with the user are mapped to purely functional constructs. The update and view method are pure functions which just depend on their parameters. The actions are nicely represented as algebraic data types. With pattern matching the relevant information is extracted easily.

The Languages

TypeScript

TypeScript is JavaScript with types. From the languages in the study, it is surely the most used one.

Tutorial for TypeScript + React + Redux

The good things

  • Wide distribution and correspondingly large ecosystem: This results in a low project risk, since both finding help with problems and finding new developers should be easy.
  • Very close to JavaScript: JavaScript developers can be productive very quickly and the interactions with JavaScript are simple and natural.

The bad things

  • The functional style given by the Elm architecture is only supported to a limited extent, which leads to unnecessarily verbose solutions.
  • Due to the type system, runtime errors can only be avoided partially, not completely.

Example state reducer with deox (type-safe reducer utility library): This example toggles one boolean state property.

const appViewReducer = createReducer(initialState, handleAction => [
handleAction(toggleProViewVisibility, state => ({
...state,
isProViewVisible: !state.isProViewVisible,
})),
]);

Example component which renders a list of HTTP headers.

const RequestHeaders: React.FC<{
headers: readonly HttpRequestHeader[];
}> = ({ headers }) => (
<div className="request-headers">
<h3 className="mdc-typography--headline6">Headers</h3>
<ul>
{headers.map(({ id, key, value }) => (
<li key={id}>
{key}: {value}
</li>
))}
</ul>
</div>
);

Elm

Elm is a language designed solely for frontend programming in the web. It was inspired by Haskell but remains a lot simpler. No type classes, no higher-kinded types, no GADTs.

A gentle introduction to elm

The good things

  • The language supports the Elm architecture ideally and the work in this architecture is accordingly natural.
  • Pure functions: Only pure functions can be implemented, which significantly increases runtime stability and testability.

The bad things

  • Core Team: Language development is conducted very strictly and with limited transparency. This leads to an increased risk for long-term projects, which depend on updates over a longer period of time.
  • Complex interaction with JavaScript: In most cases, it is very cumbersome to integrate an existing JavaScript library or API.

Example state “reducer” (called update in Elm) without libraries: It does the same thing as the TypeScript example.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
ToggleProViewVisibility ->
( { model | isProViewVisible = not model.isProViewVisible }, Cmd.none )

Example element: In comparison to TypeScript, Elm does not use a special language construct for defining components.

viewRequestHeaders : List CallRequestHeader -> Html TabMsg
viewRequestHeaders headers =
div [ class "request-headers" ]
[ h3 [ class "mdc-typography--headline6" ] [ text "Headers" ]
, ul []
(List.map
(\{ key, value } ->
li [] [ text (key ++ ": " ++ value) ]
)
headers
)
]

Reason

Reason aka ReasonML is a young, web optimized dialect of OCaml. It integrates seamlessly with ReasonReact and supports JSX as well (called Reason JSX).

We chose Reason as something between Elm and TypeScript. It is quite promising because Facebook is one of the driving forces behind it and it is already in use at Facebook Messenger.

Step by Step tutorial for ReasonReact

The good things

  • The functional elements of the Elm architecture can be represented very well through corresponding language constructs.
  • Closeness to React: For React developers, the learning curve is reduced due to the very similar elements. In addition, future React features will likely be supported.

The bad things

  • The tooling (compiler, IDE, code coverage calculation etc.) is not mature enough yet.
  • Small ecosystem: In many areas there are no mature and well-documented libraries available.

Example state reducer without libraries: It does the same thing as the TypeScript & Elm example. Here the language is more like Elm.

let reducer = (state: state, action: rootAction): state => {
switch (action) {
| ToggleProView => {isProViewVisible: !state.isProViewVisible}
| _ => state
};
};

Example component: It does the same thing as the TypeScript & Elm example. Here the language is more like TypeScript.

[@react.component]
let make = (~headers: list(httpRequestHeader)) =>
<div className="request-headers">
<h3 className="mdc-typography--headline6"> {ReasonReact.string("Headers")} </h3>
<ul>
{mapToElements(
({id, key, value}: httpRequestHeader) =>
<li key=id> {ReasonReact.string(key ++ ": " ++ value)} </li>,
headers,
)}
</ul>
</div>;

The Differences

The functional paradigms help to realize this architecture. In TypeScript we used a lot of libraries to work around the language’s limitations. TypeScript has no pattern matching (the main thing simple reducers do) on algebraic data types and its “pattern matching” on union types just feels wrong and is prone to errors. If you or one of your coworkers is not careful it is easy to stray away from the chosen architecture in TypeScript. Side effects can slip into containers, components or reducers easily, even in selectors. Immutability of the single source of truth can be spoiled without bad intention.

Elm does not let your coworkers do such things. Data is immutable, there is nothing mutable in the Elm world. Side effects are channelled to the ugly outside world, as well as all the JavaScript libraries. The rendering of components is based on normal functions. The components are always rendered in a pure way and therefore everything they depend on has to be passed down the component tree. Its syntax differs greatly from JavaScript and is more like Haskell but it’s easily learned.

Reason is somewhere in between. Reason has powerful functional features like ADTs and Pattern Matching and integrates nicely with the JavaScript world. It does not enforce functional principles as strictly as Elm and allows you for example to execute side effects anywhere in your application. Sadly, the compiler is very immature at the moment. The message “Syntax Error” without an explanation and often a wrong line number is the most common message the compiler produces, at least this is the impression we got from working with it. Libraries are currently very rare. For core features like parsing JSON there is no de-facto standard available.

The Results

Detailed results as well as the reasoning can be taken from our technical report (german). If you need help deciding which language to use for your next project this may help (the same in german).

Elm and Reason are clearly more stable at runtime. We got around zero runtime errors in both languages. In Reason the application took the most time to be developed. This is mostly because of the immaturity of the tooling (compiler, documentation, libraries) and not because of the language itself. The implementation in TypeScript was the fastest. If you subtract the time we used in Elm for integrating a JavaScript library, Elm and TypeScript took about the same time to develop. TypeScript is clearly surrounded by the largest ecosystem. Elm does not have a large ecosystem, but because the language sets a defined way for things, the community is very centralised.

What to use for new projects?

If your project needs to interact a lot with JavaScript libraries, we’d suggest TypeScript. If it is a more or less self contained project, Elm could be better suited than TypeScript. There are some real advantages. If a stable runtime is critical, you should also consider using Elm.

When to use Reason? In some years when it has matured, it may then be an all around better choice than TypeScript or Elm. We’re excited about the future path this language will take.

Co-authored by Joel Fisch

--

--