It's been just over 2 years since I wrote Elm 0.19 Broke Us 💔. Where did we go from there and how did it compare?
Elmish and F #
For our latest project, we chose Elmish and F#. Elmish provides Elm-like abstractions for F#. Fable translates F# to JS. And for view rendering it uses React. Elmish is general enough that it also works in other UI contexts. There are libraries that adapt it to desktop and mobile applications.
F# was mostly a smooth transition for us. The language is in the same ML family as Elm so the syntax differences are quite minor. The biggest hangup was probably the capitalization differences. We were already using F# on the back-end, so there was at least a little familiarity with it from front-end devs. (Who are now back-end devs too.)
Unnecessary Worries
Some of the big worries that we had about transitioning turned out more theoretical than actual. Here are the ones we maybe focused too much on before switching.
Auto-formatting
Our normal coding workflow depended pretty heavily on the official Elm auto-format. We would often just vomit (especially UI) code into the editor wherever and hit save to watch it snap into its right place. So we thought this would be a big downside since there was no official or obvious path to do this in F#.
Developing a product in F# Elmish for almost 2 years now, I can say that it didn't turn out to be a big deal. One of the major factors is that newlines count as whitespace in F#. Getting all those separators rightly placed is one of the main benefits of Elm auto-format and with F# that requirement is just gone. Secondly, we had the freedom to express code in ways which are more convenient for our usage. We experimented and eventually chose this form.
let view model dispatch =
div [] [
div [] [
str "blah"
]
]
Without leading symbols (list bracket, comma/semi-colon) in child content, this makes code at any level more friendly to being moved around. And instead of hitting Ctrl-S to auto-format, we just hit the indent or outdent keys a few times to line it up.
And lastly, Elm auto-format created tons of extra vertical whitespace in some common cases. Especially let
and case
blocks, which are excruciatingly over-spaced sometimes. We would catch ourselves avoiding let
to avoid a space explosion. (case
was unavoidable so you just accept it.) So this isn't a thing anymore. And in fact F# has an abbreviated let
syntax, so we use it a lot more.
This doesn't quite equate with Elm's auto-format, but overall I would say it is equivalent or better quality of life.
Dispatch function
In Elm, when you want to trigger a Msg from a DOM event, you just tell it what message you want to send. Example: onClick Clicked
. But Elmish gives you a dispatch function, and in the view's DOM event you have to invoke dispatch with your messages in order to send it, which is a side effect. In F# this looks more like this: OnClick (fun _ -> dispatch Clicked)
.
I suppose the main concern with this approach was devs doing weird side-effecty things and making views less maintainable. But that concern has not materialized so far. Another minor issue is that DOM event handlers are longer to type since you pretty much always use a lambda function and call dispatch in it.
I asked the maintainer of Elmish about this, who said they intended to write a layer on top of Elmish.React to match what Elm does. But it just never seemed necessary. And by keeping the dispatch function approach, it made Elmish portable to more UI targets without modification.
Likewise, we could have written our own library to translate Elmish's way into the Elm way, but it just did not seem worth it. So ultimately we just accept that DOM event handlers are slightly longer to write. We write helper functions for any especially tedious ones. And although it is less elegant to read, there is a minor upside of less separation from our source material (the DOM). In Elm, you have to write DOM decoders for custom event behavior. But not in F#.
I never found much benefit in decoding DOM values for DOM events anyway. If there was an error, it was silently ignored (I assume in order to have no runtime exceptions) and I usually wanted to see it to know I wired up something wrong.
Side effects everywhere
F# allows side effects anywhere as a pragmatic language design choice. That means you can run side effects in update
and F# will not complain. Doing so undercuts the value of the MVU pattern, and obviously things are going to slip through, right? This didn't turn out to be a problem for us. But only because I invested the time to design and designate a specific area where side effects happen.
To do this I added a function and data type to the MVU pattern. The function is perform
and the data type is Effect
. Looking at it from an Elm perspective, Effect
is roughly equivalent to Cmd
. And perform
is roughly equivalent to JS code you invoked through ports. But with some major quality of life differences.
Adding in
perform
andEffect
was done purely in user space. It only took a couple of tiny adapter functions to make it work with Elmish.
First, Effect is defined by you and can be value equatable. That means not only can you test that the model was updated correctly. But you can also test that the correct side effects were invoked with the correct options. By just using an equality check -- no mocking. 🤯 You can't even do that in Elm. Since Effect is yours to control, unlike Cmd, it is extensible to do whatever side effects you need.
Slight downside, that also means you need to create effects for common scenarios that would already be built into Elm like HTTP requests. On the plus side, there are plenty of libraries that provide F# idioms on top of common JS APIs. For example, we use Fable.SimpleHttp to make HTTP requests.
Secondly, the perform
function is written in F#. The reason that is significant is because Elm forces you to write side effects in JS (if you need something other than what is built-in). And the data has to go through an encoding/decoding process to pass between JS and Elm. Not anymore with perform
. And if you need to call a JS function as part of your side effect, JS interop in F# is trivial in most cases. Open a namespace and directly call a JS function as though it were in F#. Synchronous, asynchronous, try/catch -- it is all available to you.
You can also export F# code so that it can be called as a JS module. Or import JS modules into F#.
All tolled, side effects everywhere was the largest fear with switching from Elm to F#. But the flexibility provided by being able to call side effects from F# brought the single biggest quality of life improvement.
Downsides
Beginner Experience
Elm is not just a language. It is also a platform and a set of tools. Its documentation therefore can take an end-to-end approach and is quite good for beginners. And our company hires beginners and teaches them to program.
On the other hand our F# solution is a mix of different techs (Fable, Elmish, React, F#), each with their own varying levels of documentation. And mostly they are not aimed at complete beginners, nor are they always cohesive with each other. Even F# introductions usually assume you are coming from OO languages.
I setup some things for them, but invariably they have to be exposed to a lot more of the JS ecosystem horrors earlier than with Elm. (Like most front-end tech.) Which can be overwhelming and imposter-syndrome-triggering as a new dev. It happens in Elm too, but to a lesser degree.
Q & A
I pretend you asked a question and then answer it.
Are you rewriting Elm apps into F#?
Not as a goal unto itself. We still have Angular 1.x apps here and there, so if we didn't rewrite those yet... As long as the 0.18 toolchain keeps working, we can keep maintaining it.
Do you regret switching to F#?
No. On balance it is an improvement from my perspective.
Do you regret posting the complaint about Elm?
On the whole, no. There was the expected vitriol which was no fun, but it also seemed to put a voice to the way others were feeling. And they commented to say so.
Who was at fault according to you?
Ok, I see where this is going... But I will answer your question. Nobody was at fault. I see it as just an unfortunate combination of circumstances.
Don't you think you will just find fault in any community you are a member of?
I refuse to join any club that would have me as a member.
- Groucho Marx
Jokes aside, the whole reason you take part in a community is to help advance whatever that community is doing. As part of "helping", there are always some members who disagree with current direction or want "bad" features. But most communities I've been in handled those situations... differently.
Has Elm "improved" according to you since then?
I hope so! I have not kept up with it at all. Are we done with this line of questioning yet?
Is there a template you recommend for Elmish?
I pieced ours together so there isn't one that I have direct experience with. However the SAFE Stack 2.0 templates seem like good starting points. I was glad to see the addition of the minimal template.
Top comments (0)