| Similarities: 1) Re-frame and Elm are both backed by a virtual dom (Re-frame leverages React behind the scenes, Elm uses virtual-dom) 2) Both impose a single app state 3) Both require user events and outgoing state mutation to be explicitly defined somewhere outside of the context of a view 4) Both operate on a sort of "game loop" style of processing events and calling view functions 5) All data is immutable in both languages (though Clojurescript provides explicit mutable structures if desired, which require special syntax so any mutation is evident) Differences: 1) Elm's model is pure FP -- data is passed to a function, and then it calls other functions. An individual view function in Elm cannot independently subscribe to any kind of data event, either a state change or a socket message, etc; it must receive the data it needs as an argument to the function only. Depending on who you talk to, this is either limiting or properly restrictive. It does mean that a branch of your Elm user interface should generally correspond to a single branch of your app state, or that view functions in complex interfaces must take quite a lot of arguments; this is a pattern for which re-frame explicitly offers a workaround (though you can also do it the Elm way in re-frame if you want). In general, these differences lead to more re-usable components in re-frame than in Elm. 2) Elm has no real asynchronous library for high-level concurrency. Most Clojurescript projects (at least large SPAs) often leverage core.async as an abstraction for large UIs that handle many independent processes simultaneously. Core.async makes clojurescript particularly versatile for accomplishing things in a single-threaded environment as if it were modelled with multiple threads. 3) Elm has its own compilation story that is separate from the JS ecosystem. The Elm devs are working on dead code elimination. Clojurescript projects are all built on Closure, which provides DCE, compression, and global inlining which can provide some projects notable speedups. The Closure libraries also give Clojurescript cross-browser abstractions for hundreds of common tasks in front-end development that have been battle-tested by Google in all of their web services, so it can greatly reduce development and debugging time for complex apps that run everywhere. 4) Elm controls JS interop much differently than Clojurescript. The tradeoff is that in Clojurescript you will inevitably spend more time debugging runtime errors, while in Elm you will spend more time developing your JS interop code and testing cross-browser compatibility. 5) There are many UI libraries available for re-frame because it can wrap existing JS tools (including wrapping UI libraries built for React). There are fewer for Elm, and it's more common to roll your own UI in Elm. There are pros and cons here. I've worked on two large projects in Clojurescript, one which was all custom UI components and another that leveraged polished libraries in the wild. The former took much much longer to make but looked more unique. Having the choice to go either way is a big benefit since project goals widely vary. If you are primarily a developer/coder and not a web/graphic designer, you may be frustrated at UI design in Elm. If you work with a designer, this is not a problem. But if you work alone, having access to a lot of UI tools can free up your time and efforts quite a bit, and Clojurescript has an edge here. 6) Types: Elm has static types, while Clojurescript offers clojure.spec (which is optional though widely used). Spec is a runtime contract system so you can guarantee that all args passed to functions or setup in data structures meet specified criteria; not just types of the args, but also any other predicates, such as a valid range for an integer, etc. For example, an Elm union type would be a Clojure set, where each element could be a specific data structure with other specs attached to it. However, Elm has proper static types, which are caught at compile time, not runtime. There are benefits of both styles. If you want to tightly catch very specific data aberrations that flow through an app, clojure.spec is easy to use for that purpose; but if you want more general checks before the program runs, Elm would be preferable. 7) Performance: All of Clojurescript libraries that wrap React (including Re-frame, vanilla Reagent, and Om) offer an interesting runtime optimization that I've not seen in other languages, and is not available in React itself. If the data that a view function requires (either via its arguments or subscription) has not changed from the prior render frame, then the entire view function is skipped without running, since its output would not be any different. What this means is vast sections of your app's code don't even need to run on each render frame, and this is not trivial. In a small app, it would make no difference, but in complex SPAs this can be very significant. Elm offers a library, Html.Lazy, that attempts something similar, though my impression is that most Elm projects do not use it since it can be tricky to explicitly add this behavior; in Clojurescript, it is built in automatically. In Re-frame, they go an extra step by de-duplicating any queries or subscriptions so that multiple views which use the same data context do not query, fetch or calculate the required data more than once, which is then passed to all requested functions. If you are working on an app that processes lots of data frequently, this can be the single selling point of using Clojurescript, as it frees up the CPU to deal with only those things that are guaranteed to require processing. Clojure's syntax is very tight and concise, while Elm's language is elegant but more verbose. I have found that there is approximately a 2.5X increase in code size in Elm when trying the same ideas out in both languages. All other things being equal, you can probably get a re-frame app going very quickly; if you need to prototype something or get to an MVP as soon as possible, it is really hard to beat Clojurescript compared to any other compile-to-JS language. If however you are going for application purity and a reduction in runtime debugging, Elm (or Purescript or some other statically-typed languages) would be a better fit. Ultimately, Clojurescript is a general purpose language, while Elm has a very specific use-case; if you fit into the Elm model, it can be nice. If you need to reach outside that model, it is a challenge. |
Large projects are absolutely doable but require more discipline and experience with the language from developer. For example you can start with a crude prototype and introduce clojure.spec later. When you do this is entirely your choice while in Elm you just have to write function signatures and types/structures definitions from the beginning.
ClojureScript has a very easy interop with JavaScript. Actually this is one of design goals. Consuming JavaScript library is super easy. For example most of virtual DOM libraries are built upon React. This is good because writing performant virtual DOM with older browsers support isn't an easy task.
The toolchain has code splitting and dead code elimination. It had it long before Webpack and can even optimize imported JS libraries code. As far as I know Elm still can't do it.
Another good thing is you can use Clojure(Script) both on the client and the server. This allows you to nearly skip data serialization/deserialization using powerful "transit" library. Actually you can use transit on server with several other languages. So it's not a lock in but it's much smoother with Clojure.
The libraries ecosystem is rich. For example Clojure(Script) has a mature WebSockets library (it's both server-side and front-end) with any kind of fallback you can imagine.