Hacker News new | ask | show | jobs
by jayshua 1219 days ago
I maintain a 50,000 line elm app. It's not the most used app in the world - has between 50-100 users. But it has had a sum total of 2 bugs since launch - both logic bugs in some complex financial projections.

Contrast with an elixir app of similar size that I also maintain. I fix on average 1-2 bugs per week, almost all of them are things that the elm compiler would have prevented like calling a function that was removed or renamed last month or not handling a case where a database request returned 0 records instead of 1.

Honestly though the biggest draw for me is the refactoring experience. For various reasons I had to do a major refactor of both code-bases. Not adding new features or anything, just restructuring the code and module layout to line up with the way the code actually functioned. Paying the tech debt essentially. I'd say both refactors were of roughly similar complexity, touching probably 75% of the files in each project.

The elm refactor took about 5 hours and worked as soon as I fixed all the compiler errors. I never had to fix a bug from that refactor - it just worked. And the code was way easier to understand after paying all that tech debt back.

The elixir refactor took 4 days, full time. 25ish hours. It also required me to write about 150 tests that weren't there before because the compiler wasn't able to point out code that would always fail at runtime. After deployment I had to fix a handful of bugs that still made it into production, despite how careful I was to not break anything.

Idk, being able to completely restructure the code without worrying about breaking anything really appeals to me for some reason.

1 comments

Your argument is more about static versus dynamic typing rather than anything Elm specific, as you can get all of that with TypeScript too, but without opting into Elm's entire framework. One reason why, in my opinion, TypeScript won, as I mentioned above, because it lets you do what you need to do without arbitrary restrictions by the compiler authors.
My experience has been due to the type system, I agree. (Also the compiler enforced semver.) I've never had the opportunity to use type script, so maybe it does offer similar benefits to Elm in that area.

That's extent my personal experience can push things as far as active production applications go, but it seems to me there's something a bit more in Elm beyond just the type system. Haskell and PureScript both have static typing, but I've had runtime errors in both those languages. Not as often as JS or C or even Rust, but they do happen. In Haskell because the 'error' function exists and so people use it. In PureScript because it has its version of Elm's native modules, permitting arbitrary functions to be implemented in JS. The JS can (and does) produce runtime exceptions.

It seems to me that the promise from TS, Haskell, PureScript, etc. is something like "with this language/type system runtime errors will rarely happen" whereas the promise from Elm is "with this language/type system runtime errors can never happen."

I think that transition from 'rarely' to 'never' has been a big part of the Elm project for a long time. It's been on their homepage for as long as I can remember. At least prior to the big 0.18 native modules controversy.

If the idea is "no runtime exceptions can ever happen" with the implicit addendum "no matter what you do" native modules have to go. They were a vector by which user code could crash the application, breaking the promise. In that sense the restrictions are not arbitrary, but instrumental in upholding a major design goal of the Elm project.

That said, I don't know how much value there is in the "can never crash" promise. It feels like a step in the right direction, but at the same time it seems all the benefits in production code that I've personally experienced and cited in my earlier comment don't fall under the edge cases Elm prevents with its stricter promise. The two or three exceptions I've encountered in PureScript as a result of its version of native modules existing were all in toy programs, so maybe it's just not a big deal in real production apps.

That makes sense but it also strikes me as what I feel Haskell is like; it is so wrapped up in the ivory tower and preventing runtime bugs that it's hard to get actual work done. In TS, I also have not had runtime crashes but I'm able to use the vast array of packages for JS (which usually have TS types too nowadays) and get done what I wanted to. If I had to do the same in Elm but with much fewer packages, I mean sure I could reinvent the wheel but it doesn't bring us business value.
The guy just said earlier that he managed to do a full refactor in very little time and had super few bugs in production. I think that's a prime example how you gain very big productivity boosts using elm.

I've used typescript and elm, the ridiculous amount of time that we spend fixing bugs in typescript dwarf the benefits of having a bigger ecosystem.

Elm was the first time I could release something and say that it was done. I had built an app in elm for 4 months behind closed doors and then went live and everything worked with the only bug being in an interop to JS (2 lines of code, I fixed it in less than an hour). It meant that I could continue developing new features as requests came in without stopping to fix things. In return, also meant I could easily predict how long time takes to make. The benefits you get are massive. The cost you pay is that you do things properly.

After that project, it had been a while since I had used Typescript, and so I wanted to give that a try again, thinking that it's basically elm if you are strict about the typing. I fired up a new react project, added some tests using react-testing-library which passed, typing passed and then I booted the app and it crashed. Later on I needed to plot a graph and imported a library for it. I followed the instructions and no graph showed up. This would never happen in elm, if it compiles it pretty much always works. Typescript offers nowhere near the same experience and I lost so much productivity trying to debug the earlier issues.

> imported a library for it. I followed the instructions and no graph showed up. This would never happen in elm, if it compiles it pretty much always works

except that in elm that library would not even exist, and you have to write the binding yourself, which is why AFTER you write the binding, everything should work.

Actually there was a graph library for elm which I did use and it worked perfectly.

The elm ecosystem is surprisingly rich for having such a small community. The libraries are also much better designed and much safer to use due to the pure nature of elm.