Some disclaimers up front. I am not a professional developer; I'm a self-taught programmer who's found himself in the unlikely position to be the Director of Technology for a school. Possessed of these skills, it's often the case that I see value in building a solution to our problems rather than purchasing one. To do this means successfully passing a rigorous cost-benefit analysis in which "build" beats "buy." It also means I have to build in such a way that I am not the only person who can maintain applications that become mission-critical.
I'd like to tell you that I therefore follow every best practice: thorough documentation, writing tests for everything. I'm not there yet. I'm still learning.
About a year ago I learned about Elm. Having dabbled in Haskell and written a React Redux app, I was completely enamored with functional programming and The Elm Architecture, in particular. So when the opportunity to rewrite an in-house scheduling app came about, I knew I wanted to use Elm. It wasn't a particularly complex application, just a way to create recurring events on a six-day rotation rather than daily, weekly, or monthly. The perfect size for a foray into a new language.
But hang on, what about that maintainability thing? Wouldn't introducing a completely alien language be a nightmare for anyone besides me who had to look at the code?
That brings me to the first thing I learned.
Elm makes some clean code
I am in love with the way Elm code reads. The update
function takes a set of messages sent by the view
and updates the model
(state of the app/module). It looks like this:
update : Action -> Model -> (Model, Cmd Action)
update action model =
case action of
NoOp ->
model ! []
RequestCalendars ->
model ! [ getCalendarList clientId ]
ReceiveCalendars calendars ->
{ model | calendars = calendars } ! [ getLetterDays letterDayCalendar ]
RequestLetterDayEvents ->
model ! [ getLetterDays letterDayCalendar ]
If you've never read a functional language, that might look completely bananas. The first thing to know is that the top line is the function definition. It tells us what types come in to the function, and what type comes out. Then we name the arguments in the second line, which is equivalent to function update(action, model){
.
The action
argument is of a special type that I've described myself. It describes the kinds of messages the view (what users see/interact with) can send to update
. Beneath that, you see a case/of
statement, which is similar to a switch
statement in JS. Each line after an ->
indicates what update
should return in the case of that given action
.
Action
, in fact, is part of the second thing I learned.
I love strict typing
My programming journey began in Python, then moved to PHP, then JavaScript. So for a long time I had a bit of apprehension about static typing, to say nothing of strict typing. Then I learned Haskell, and discovered a new way to think about programming. Procedural languages treat programs as sets of instructions to execute in order—the classic "recipe" example. And that's all fine, and is often the absolutely correct way to conceive of a problem space. By contrast, functional languages like Haskell and Elm ask the programmer to consider the "shape" of information coming into a function and the shape that should emerge from a function. Applying the right operations to the information between input and output produces the desired result. Rather than thinking of steps in order, you begin thinking of enclosed transformations. In doing so, you must be completely clear on your data types between input and output. That's where strict typing comes in. By defining functions first in terms of what comes in and goes out, there's never any ambiguity about what's happening or what's being passed around your code.
Consider the following mega-contrived example:
Suppose I wanted to write a function that takes in an array of numbers and returns them squared. Easy enough, right? Here is such a function in JS:
function squareEach(nums) {
let res = [];
for (const n of nums) {
res.push(n * n);
}
return res;
}
Okay, I know there's a cleaner way. Here you go:
function squareEach(nums) {
return nums.map( n => {
return n * n;
});
}
Problem is, I could only know nums
's type through comments. And while comments are good, nothing in comments will stop me from writing a really bad bit of code that ends up sending undefined
or something worse to squareEach()
.
Now, here's the same function in Elm:
squareEach : List Int -> List Int
squareEach nums =
List.map (\n -> n * n) nums
First I define squareEach
, telling Elm (and anyone reading my code) that it will take a list of integers and return the same kind of thing. Then I implement it, using a lambda (anonymous function) and List.map
to emulate the same behavior as Array.map()
in the second JavaScript example.
Now suppose I write some function elsewhere that tries to pass, say, a String
to squareEach
. The Elm compiler will catch it and provide an error for me. I can't even compile stupid type mistakes.
And that's thing #3 I learned.
Love recompiling
I fought with the Elm compiler a lot. Some mornings, I just had to stand up and walk away from the computer, since I could not get the compiler to agree to build my code. Invariably, my thinking was in error and taking the time to correct my mistakes the proper way resulted in cleaner, more elegant code. Spending copious amounts of time reading documentation helped. It isn't a great feeling, constant screens of red text in my terminal as Elm code fails. But you know what's an incredible feeling?
No runtime errors.
That's the tradeoff: rather than finding a ton of errors in production because an unexpected value slipped passed me, even with tests, hammering away against compiler errors results in a fairly stable application. No surprise undefined
s to wreck your runtime. This isn't to say that Elm makes failproof apps—any language is only as good as its user. However, the number of issues caught by the compiler vastly reduces live debugging. Adding good tests to this mix is an even greater guarantee of stability.
Well, I say "stable," but...
Elm ain't done yet
There's no question this language is going places, and I've never had more fun putting together a frontend. Nevertheless, there are a few pieces still missing from the core
library, like reasonable Date/Time
tools. I used Justin Mimb's excellent date-extra package to handle date/time parsing. But I shouldn't have to install a third-party dependency for ISO Strings, or to compare (<
, >
) Date
s.
On the flip-side, the community packages are of a high quality. Just know that if you're making a non-trivial app with Elm, you'll end up installing a couple of third-party dependencies, or writing a bunch of your own helper code.
So if you want to change the way you think about programming, I highly recommend checking out a functional language. And if you want to fall in love with frontend again, I highly recommend Elm.
Top comments (2)
There is something interesting about dev to. It's one of the places where Elm people write the most.
Very good explanations for the JS crowd. Well done.