Recently I have been gathering information about what makes for a happy programming environment. The Elm language kept popping up as a leader in most of the metrics I am interested in. So on the weekend I gave Elm a try and was pleasantly surprised. Elm solves practical problems in a very elegant way, and the compiler is very friendly. I coded up a basic counter that represents numbers as n sided polygons to get a feel for the language.
Below are my notes on my first impressions compared to ClojureScript. Elm is still new to me, so apologies in advance if I make some false assumptions, and please add corrective comments.
Elm wins
-
Elm error messages are absolutely fantastic.
The tone used is friendly and inviting.
Plenty of context is provided.
Messages never exceed a page, so I don't have to scroll around.
Whitespace and formatting is used to delineate errors
and separate information about types, code, and suggestions.
The suggestions are surprisingly accurate.
Often the suggestion is exactly what needs to be fixed,
and when it isn't, the suggestion still provides good search terms to
research in the documentation.
All of these features conspire to allow me to very quickly identify the relevant information
and keep productive.
There are some examples of error reporting here: http://elm-lang.org/blog/compiler-errors-for-humans. You really have to try it to appreciate it, the examples don't convey just how effective the help is. - Pattern matching is strong with Elm. We get destructuring and case. Case is like core.match, but built into the language, idiomatic, and type aware. This encourages clear and concise function.
-
Elm has a literal assoc syntax.
{ bill | name = "Nye" }
is equivalent to(assoc bill :name "Nye")
Data structure literals assist thinking. Hashmaps and their ilk are a programmers bread and butter. Having a dedicated notation really helps focus in on the core abstraction. I really like having the update notation. - SVG works! Elm is not just wrapping the basics, it provides a comprehensive solution. It can pretty much do everything as far as I can see. The combination of FRP with SVG is powerful and fun to play with. SVG lends itself to the data-to-view philosophy in ways that canvas cannot. Having checked SVG is pretty handy, no more typos.
-
Excellent documentation guides and references.
Well written, complete, well presented.
The introduction is very pleasant to read and within a couple of hours
I was equipped to code and experiment.
https://guide.elm-lang.org - Type inference works great. I didn’t have to specify any types to do what I wanted to. There was some minor casting to be done.
- The View Model Update separation is clear and clean.
- While learning Elm I never felt stuck or frustrated. I was able to build a small project after reading half the introduction. Closest to stuck was getting lost in syntax… breaking out functions identified the real issues.
- Imports are much cleaner than namespaces. Git based modules has appeal. Elm's module system is interesting. Elm does have strict semantic versioning, which is intriguing. Perhaps it will bring sanity to dependencies. I don't expect to run into the weird dependency conflicts I see in Clojure. Compiler verified non-breakage? Yes please!
ClojureScript wins
- Code reloading (figwheel/test-refresh/ring-reload) is important to my workflow, and ClojureScript has more mature tooling for that. The author of Elm is thinking about how best to do code reload: https://github.com/elm-lang/elm-reactor/issues/193
-
Syntax. Lisp's lack of syntax makes for some useful editor optimizations.
For instance deleting/moving/copying entire functions or blocks of code
is very convenient using paredit because they are fully enclosed in parenthesis.
The syntax rules detracted slightly from the otherwise awesome error messages because I sometimes confused call signature errors (like leaving out a required empty list) with language features (such as creating ranges using [1..x]). On the other hand, the lack of parenthesis does have a pleasing minimalistic aesthetic. I expect to appreciate it more over time. - Editor support appears to be wide for Elm, with many plugins, but no where near as feature rich as Clojure. The Emacs plugin did not give a great experience, it appeared to only give syntax highlighting. Indentation never lined up to where I expected it to. The Light Table plugin suffered the same indentation woes. It is not clear to me how to get REPL interaction with either of them. However, this really was not a big deal. I was fine editing the code without much editor support thanks to the excellent documentation and error messages. I'm sure with a little elbow grease I can adapt my editor. If anything I would say that Clojure editor integration is way harder, but I've put this as a slight advantage to Clojure just because the integrations are quite mature and can do all sorts of whacky stuff.
- Front to back. Clojure has an advantage in being viable for all my coding needs. Elm appears to be restricted to the front end. I imagine this will change (maybe it already does target NodeJS???). Is it a general purpose language? Given it's heritage I think Elm will be general purpose one day if it isn't already.
- The package manager is not as powerful as Leiningen/Boot, there is no .m2 or plugins.
Overall Elm has some clear advantages, and I will definitely be using it for my next personal project (when I can dream something up!) I hope the Elm ecosystem continues to grow rapidly.
For reference, here is the code for creating an SVG polygon path with n sides:
view : Model -> Html Msg view model = div [] [ button [ onClick Dec ] [ text "-" ] , div [] [ text (toString model) ] , svg [ width "400", height "400", viewBox "0 0 100 100" ] (numeral model) , button [ onClick Inc ] [ text "+"]] numeral x = let dphi = 2.0 * pi / (toFloat x) phis = List.map (\i -> (toFloat i) * dphi) [1..x] points = List.map (\phi -> (toString ((cos phi) * 20.0 + 50.0)) ++ " " ++ (toString ((sin phi) * 20.0 + 50.0))) phis p = "M " ++ (List.foldr (++) "" (List.intersperse " L " points)) ++ " z" in if x == 0 then [ circle [ fill "none", stroke "black", r "20", cx "50", cy "50" ] []] else if x == 1 then [ path [ stroke "black", d "M 50 30, L 50 70"] []] else [ path [ fill "green", strokeWidth "5", stroke "black", d p] []]