22. June 2018

Compile to JS like it's 2010 again: A sneak peek into ClojureScript, Elm and ReasonML

Recently, we’ve been seeing some kind of a renaissance of Compile-to-JS languages. When Babel and ES6 made the first wave of languages like CoffeScript obsolete, the toolchain that evolved around Babel (and TypeScript) made transpiling your code mainstream and probably even the de-facto standard for writing largish applications that run in the browser.

While the first wave of languages were dealing with syntactic improvements to JavaScript, the next one seems to be about types, state and immutable data. Over the last few months I took a look at a few of these languages, so this article is trying to share a few of the insights I had while doing so.


The scope

In each of the three languages I built the same, kind of basic app: a simple chat interface for conversing with a chatbot that is being talked to via a RESTful API. This means all apps needed to perform DOM manipulation, form and event handling and XHRs, but not much else. I also didn’t run any of this in production environments.

ClojureScript

ClojureScript compiles Clojure to JavaScript while leveraging Google’s Closure Compiler. As Clojure is a LISP dialect, the syntax (rather the absence of syntax) might feel very alien to people coming from a mostly JavaScript background. This is how listening for the submit event of a form looks like:

(events/listen form "submit"
  (fn [event]
    (.preventDefault event)
    (.log js/console (.-value (.querySelector (.-target event) "input"))))

Just like with syntax, ClojureScript will have you learn how to use a new build tool, most probably Leiningen that is used for handling dependencies and builds.

The overall learning curve therefore is rather steep for people who are used to “standard” web languages and tooling. That being said, I found paying the toll needed upfront totally worth it. Once you have made it, you get a language and an ecosystem at your hands that is capable of solving real world problems in a clean and simple way.

Building the interface mentioned above in ClojureScript was a pretty enjoyable experience for me. Immutable data structures, Atoms and other STM primitives provide everything you need to model your application state in a clean way. JavaScript interop is pretty painless when you really need it. In addition to the language, the hot reloading development environment that Figwheel provides has yet to be matched (give it a try, honestly). There are super nice HTML template DSLs like hiccup and projects like Reagent that provide bindings to React.

I was longing for any kind of types for my data though. There seems to be clojure.spec, but it definitely adds another level of complexity for a beginner. Also, the abundance of macros does not really help newcomers to understand what’s going on in code samples.

Considering that ClojureScript also gives you the option of using the same language on the server and in the client, I’d definitely like to give it a try on a larger project. It’s probably also a relatively safe bet as it’s established enough to have a community of developers that likely won’t go away anytime soon.

Elm

Heavily inspired by Haskell, Elm is a language designed by Evan Czaplicki. In the end, it’s way more than a language though. It also comes with a prescribed application architecture (the Elm architecture which for example heavily inspired Redux) and a virtual DOM implementation. It also has a heavily curated ecosystem of packages and tooling that piggy-backs on npm.

A function that takes an Elm Model and transforms it into a view might look something like this:

view : Model -> Html Msg
view model =
    div []
        [ ul []
            (model.messages
                |> List.map (\m -> li [] [ text m ])
                |> List.append [ li [ hidden (not model.typing) ] [ i [] [ text "Bot is typing..." ] ] ]
                |> List.reverse
            )
        , form [ onSubmit Submission ]
            [ input
                [ type_ "text"
                , id "chat-input"
                , placeholder "Enter message..."
                , value model.input
                , disabled model.typing
                , onInput Input
                ]
                []
            ]
        ]

An important concept behind Elm is that it tries to be a pure functional language, which means all of your code has to be free of possible side effects. If you need to perform side effects, you will need to send a “Command” to the Elm runtime, which then does the non-pure work for you.

I admittedly had a rather rough time trying out Elm. I liked the architecture that it provides by default, but I often had the feeling that it values theoretical correctness more than solving real world problems. For example, when I wanted to put force focus on the input element of the chat app, Elm simply wouldn’t let me do it, it’s impure and cannot be achieved by re-rendering the Model. I therefore had to spend almost half a day hunting down some officially blessed package that would let me do it, which is not necessarily trivial and also further complicated by the fact that the ecosystem totally loves using “that FP lingo”. Which is not exactly beginner-friendly. I also had to give up trying to componentize the app as the one Elm way of doing it wasn’t too intuitive for me, which had me end up with a single file containing everything.

All this aside, I did like giving Elm a try for giving me further insight in programming concepts I do not necessarily encounter on a daily basis. Data types and concepts like the Maybe type are extremely nice tools for modelling client side logic. Using them as a language feature for a while can definitely give you fresh perspectives on how to model your data flow when back in JavaScript land.

Which is why I would definitely encourage everyone to give Elm a try. I’m just a little skeptical if it’s an ecosystem that is actually interested in solving real world problems. I found it closer to a very nice playground for toying with concepts of strongly typed functional programming while building client side apps.

ReasonML

A “language” developed at Facebook, ReasonML is bascially just another syntax for writing OCaml, geared towards people coming from JavaScript. So when writing ReasonML, you are actually writing OCaml. The transpilation step is then performed by the BuckleScript project, but you can of course also target the standard OCaml compiler and compile to native code. If are worried now, all this sounds way more confusing and complex than it actually is. In real life, you can use npm to install all the tooling, and you can still use npm to manage your project and its dependencies, which gets you up to speed fairly quickly.

ReasonML is a strongly typed functional language that leverages the advanced type inference mechanisms of OCaml, so you get all the niceties of a typed language without having to annotate too much of your code. Which could look something like this:

type action =
  | SubmitMessage(string)
  | ReceiveMessage(string)
  | NetworkError;

let handleAction = (a) => {
  switch (a) {
  | NetworkError => Js.log("An error occured")
  | SubmitMessage(msg) | ReceiveMessage(msg) => Js.log("Got message: " ++ msg)
  };
};

In the above example, without any annotations, the compiler will figure out what is going on in handleAction and will for example also warn you if the pattern matching is not exhaustive (i.e. you are not handling a possible case).

When building the interface with ReasonML I also used ReasonReact which are the React bindings that Facebook wrote for porting messenger.com from JavaScript to ReasonML. The bindings work amazingly well, although some of the details of handling JSX in ReasonML are not the prettiest. You also get nice Redux-like state handling using reducerComponents out of the box, which means you’re most likely ready to get going by using the reason-scripts for create-react-app and don’t need to look into any new tooling for a start.

A feeling that I had quite often when working with ReasonML: it works very well, but oftentimes it’s not really pretty. Which I would consider a good thing. It’s looking to solve real world problems in a robust way instead of writing elegant code. When both is possible, it still lets you do both. I found myself being reminded of Golang a couple of times, even when the type systems couldn’t be more different. ReasonML’s type system is pretty amazing and accessible for people without hardcore FP experience.

The story for JavaScript interop is pretty good. If you want to, you can just call through in unsafe ways, but the language lets you gradually type your interaction with JavaScript which lets it scale pretty easily.

I’m not too sure if the plan to dress up OCaml in JavaScript clothing is the best of ideas though. I often found myself in the situation of guesstimating how a syntax construct could be derived from JavaScript - and failing - instead of looking it up. Maybe the upfront toll of getting accustomed to a new syntax wouldn’t be as much of a problem as the language designers consider it to be.

The feeling I got from using ReasonML is that it currently is basically a language built around using React (from what I understand the initial prototype of React was even written using Standard ML). Which it is pretty fantastic at. I fear you’d have a relatively hard time using it for something else though. Considering the speed at which it is evolving, I might be very wrong very soon with that though.

Is it worth it?

There are far too many factors to consider here to give an answer for a question like this, but I think I’d like to stress one thing that I already mentioned above: looking into languages that give you different paradigms and tools for solving the problems that you usually solve will give you perspectives you wouldn’t get if you stick to what you already know and use. To me, that is super important, and pays back even if you decide to stick to plain old JavaScript after evaluating one of these languages. I would probably never pick Elm for a production project, but I urge you to try it as soon as you can. The experience will positively contribute to what ever tech you pick for that production project.

I also think this applies to choosing a seemingly obscure technology or language for a team. You might be afraid you won’t be able to hire people who are proficient enough to join your team and you are stuck with the five people you know forever. But maybe it’s about seeing it as an opportunity for new team members to gain insights they’d never have if your choice of tools was purely driven by supply and demand. You can always hire someone who is interested in learning about it, just like you did learn it at some point.