Elm first impressions

Intro

So I spent about 20+ hours toying with Elm in total. The reason I want to share my opinion prematurely is because I'm not going to continue this journey any time soon. And without some sort of resume random thoughts on the subj. will continue to bubble in my head.

Half year ago I compared ClojureScript and Elm to decide between two platforms. Since that moment I've become much more disappointed about Clojure to a degree I no longer consider it as a language option for my projects.

Meanwhile my desire to explore Elm was growing and I've got some time to realize it this week.

Many folks write about the goodness of Elm and they are probably right. The language is ok, not perfect but quite decent. Everything in there were accessible due to my tiny Haskell experience. The main sources of frustration to me became the so called Elm architecture and the direction where project is going.

You should be warned that as a person who prefers to concentrate on the "bad parts", I'm talking mostly about drawbacks, quirks and things that bugs me. The world is full of developers praising and admiring every novelty thing so I want to counter-balance that from the other side. I'm not enjoying it – quite the opposite. And still that's how I behave.

My overall impression about Elm is mixed, though I have a big hope the team will improve over status quo. I wish them the best because the niche for Elm definitely exist. We have so many JS game engines yet all(?) of them are tasteless OOP. Meanwhile, let's begin our dissection.

The Good

Compiler. Error messages. Tooling. Everything "just works" which is a breeze after JS world where you can spend a week tuning Webpack without an actual progress in your task. Examples are good, documentation is not very thorough leaving you with questions which are, still, quite easy to address. There were no moments I was stuck with something. Which is already cool and surprising.

I can't say I completely follow the "Use before you understand" philosophy. But I appreciate different approaches, so be it. Let's assume it's also cool.

Html / Canvas functional API is something I'd love to see in JavaScript.

But again a lot of good things were said about Elm and I see no sense in repeating them. I'll better give you a potion of criticism leaving you to judge how valid it is. If not or if not in your case – then Elm is a language for you, don't hesitate to try or continue. I'm all for diversity and different opinions.

The Bad

First thing worth mentioning in this section is an Elm team (business) strategy. Their target audience is clearly a JS programmer and apparently a very shy one. The one who judges the language by literally counting syntactical constructs and leaving immediately if that is number is bigger than... I dunno what. Very curios conviction because no language I'm aware of can defeat JS by an amount of grammar overcomplication.

Just for example, a Haskell has a neat where binding syntax which allows you to read and write your code in order of decreasing abstractions.

-- checks if a credit card has a valid number
sumDigits = ...

isValid :: String -> Bool
isValid cardNumber = haveValidSum transformedDigits
                     where haveValidSum xs = mod (sumDigits xs) 10 == 0
                           transformedDigits = zipWith doubleOdd revertedDigits [0..]
                           revertedDigits = reverse $ map digitToInt cardNumber
                           doubleOdd d i = if even i then d else d * 2

Note how you can stop reading at whatever layer of abstraction you feel enough. I believe this approach is superior to ordinary top-down layout. High-level stuff should be on top of every section (file, function...), unless you used to read bottom-up.

Many people with a similar opinion were asking to add this feature but the corresponding thread was closed without much arguments. Perhaps to not "frighten" JS developer with an "extra" syntax. Oh well...

It's also inconsistent then to keep the module where syntax which is of the same nature.

module Foo where

But Elm does have it.

This "JS noob" targeting caused problems for Elm already. In it's early days, the major part of Elm community were haskellers who wanted to reuse their experience for frontend. They were quite benevolent first and ready to accept some really awkward decisions.

But gradually they start to dissolve from community. Switching to PureScript or whatever. At least, that's my impression backed up by observations I made checking GitHub repos.

Before I make an important technology decision I tend to lurk through available records to reassemble the History of the project. So I spent additional hours researching related issues, changelogs and google groups.

An attitude to an "overcompetent haskellers" was striking. They basically were told to not scare people out. I've got an impression JS "noob" is more welcomed in the community because he can attract more "noobs" where haskell "nerd" can only distract.

I can't argue that's a valid marketing plan and still something in me opposes to such "downshifting" approach. I don't want to learn from Ruby or Python because the most pretentious and loudly ignorant people I met were pythonistas and rubyists exactly.

I worked as a Python developer for two years. I have an experience to judge. But maybe it's just my experience. But, at least, I consider myself much-much closer to the
"JS noob" category that to a "Haskell pro". So (I think) I'm telling this without a personal bias.

The problem is that banning monads Elm ended up reinventing them. And banning infix functions Elm ended up in the search for a better syntax :/

People sold to "no monads this time" will be surprised to find them in Elm one day under a different term.

To recap: I can't agree that common JS developer is a shy incompetend newcomer, scared by a single math term. JS is super hard to deal with and frontend devs used to become cold-blooded survivalists paving their paths from IE5 to IE11 and from ES3 to ES2015.

Which leaves a serious question to the bets were made. Would you personally choose Accessible or Reasonable tool in the long run?

The Ugly

Consistency

I was among the first adopters of React and later CycleJS frameworks.
So I subconsciously compare every other solution to them. The building primitives in CycleJS are uniform

model :: {Observable} -> {Observable}
view :: {Observable} -> {Observable}
intent :: {Observable} -> {Observable}

-- in simple cases
model :: Observable -> Observable
view :: Observable -> Observable
intent :: Observable -> Observable

-- or even
model :: Observable -> Observable
view :: Observable -> Observable

Everything is a function from Observable to Observable or from a record to a record. Everything is perfectly composable. Unlike your common "MVC clone" in CycleJS you can vary the number of nodes if you need. And I relied on that freedom more than once.

Now in Elm it's complicated

model :: Signal | any
view :: Address Action -> Model -> Html
update :: Action -> Model -> Model

For apps with effects it's even worse.

...
update : Action -> Model -> ( Model, Effects Action )

With fixed number of nodes and predefined signatures.

So CycleJS succeeds to reassemble the whole dataflow with only two "new" concepts: Observable and Driver. Where Elm requires of you to understand five: Signal, Mailbox, port, Task, Effect.

And keep in mind that Mailbox was previously called Channel and meant a bit of different thing. Task and port were added recently but it seems there grows an opinion they don't perform as good as expected. Signals aren't in safety as well.
...

I can continue but you've got the point. Just always be ready to a new API change.
Ain't you flexing your brain cells doing that?!

Excessive Concepts

The worst part of Elm is conceptual disprepancy. Take main vs port. main is a sink to send HTML effects to, while port can be a sink or source to handle every other effect. Why so? – asked I myself. Why not just pass everything through ports? Or rather why not pass everything through main as CycleJS (and Haskell!) do. Did I miss something obvious?

Turned out – no.

Q: Why is main not a port?
A: Mostly because main existed before ports, but also because we didn't want to make it necessary to learn about ports to use main

Good to know.

Q: Why do we need to define a separate port to execute tasks (instead of returning these through main)?
A: For similar reasons, main existed before tasks, and in the simplest case you don't have any tasks. Also, to keep things separate.

Wait... so you've just basically accepted your design is a mess because of historical legacy? How such a young project advanced in that?!

As a sidenote, other implementations which can't use main often use ports instead, for example Elm on node and the react native project.

Ohmy.

I was surprised to see so much explicit Signal.send calls. Elm devs argue they are not imperative (because of laziness) but I disagree that laziness matters here. They are definitely not declarative (no inversion of control) hence they are imperative. So Elm fails to be fully reactive just like React for no explicit reason.

So Elm is full of design quirks and it's short history is full of breaking changes. Core devs know it but urge you to trust them. "This time we'll do it right".

If only I had an infinite lifetime. I venture to ask maybe "Haskell failed, we should learn from Ruby" and "Let's pretend computer science does not exist" are not such a bullet-proof strategies?

Infinite Monkey

Richard Feldman, the topmost Elm contributor, brings in an interesting idea:

True, but the alternative is encouraging people to look for fresh abstractions using Elm's unique set of rules. Consider that Monadic I/O itself was discovered because Haskell prohibited side effects. At first this made doing I/O difficult, but then a better way emerged...out of the necessity of trying to accomplish things without the usual tool of side effects to work with.

So we should gather more javascripters and wait while they invent something better than monads. Cool story. If only Turing awards were going to Angular hipsters instead of computer-science professors. The world would be different for sure.

Wormholes

Now there is a cool article of Paul Chiusano about "wormhole" antipattern. We can't completely remove wormholes in loop-locked frontend application because software code is one-dimensional. Paul's Chiusano Unison project aims to change this (among others things) btw. but the point I want to make is that "wormholes" are really an antipattern (an inevitable evil) and we should a) minimize the number of such cases b) control and organize them.

As Elm's main is not a port and has only a sink part how do we supposed to catch user DOM actions? In CycleJS they "drop out" from the driver sinks but in Elm?

Well, to catch them you need to:

  1. Create a mailbox (analogous to RxJS Subject)
  2. Trace it manually through the main
  3. Pass it to the HTML making a "wormhole" manually
main : Signal Html
main =
  Signal.map (view mb.address) model
Html.div []
  [ Html.div [] [ Html.text (toString model.count) ]
  , Html.button [ Events.onClick address Increase ] [ Html.text "+1" ]
  , Html.button [ Events.onClick address Decrease ] [ Html.text "-1" ]
  ]

This is miles behind the CycleJS architecture which not only discourages manual Subject creation but also separates view and action layers. And I consider this React-like complection of concerns a strong antipattern on it's own.

Modules

I'm an evil person with a bad character. Mistakes upset me.

FFI abscence and the overall dataflow architecture with manual wireups which Elm is greedy for are frustrating.

And yet another serious mistake Elm does is modules. JS toolkit integration is blocked. Meet your destiny of giant bundles man.

Was it hard to compile to JS one-to-one, module-per-module? I dunno but CoffeeScript did that for ages and PureScript does so probably not that much. Was it caused by runtime? Why build the language over a single non-replaceable architecture to begin with?

In Elm, the Elm itself is an app and your app is a plugin.
Are you comfortable with the idea?

I weren't touching advanced topics (for obvious reasons – I have no clue about them) but it seems there are even bigger problems in the scale. I'll just leave the most disturbing quote from here:

Because the signal world is so limited, most of your logic
necessarily lives elsewhere.

Yep. In task chaining, in monads which "do not exist".

Conclusion

Am I going to give Elm a second chance?

I'll give it a look after 1.0 release. The quirks I mentioned can be fixed so nothing is hopeless. But I don't have enough time for a lab rat volunteering. Both achievements and failures of Elm are significant so I'm going to wait for an official release and check a final stable API version.

I appreciate a decision to learn and borrow from other languages. ML in particular. While I don't see what competent JS programmer can grab from Ruby and Python (which are a hell of OOP and metaprogramming) the first does good marketing job and second has a good reputation :) So aside the engineering I can imagine them being inspirational.

A minimal amount of syntax knowledge is required to access Elm examples. Good frontend engineering solutions which you can copy and break apart are rare. We need more of them. More simple, distilled, awesome examples. More incremental ones. So learning Elm is valuable indeed.

Author: @ivankleshnin