When I first started at thoughtbot, my primary experience was in Rails and
TypeScript. However, the first project I joined was written in Elm and Scala.
Both are pretty different from my “native” languages: they are strictly
typed and utilize functional programming patterns. But, getting
onboarded with Elm was much easier than with Scala. There were many reasons for
this, one of them being that Elm has a famously descriptive and easy-to-use
compiler that gave me very direct messages about what I needed to fix. Despite
this, though, I often found myself wondering why things had to be done in such
specific ways. What was a Cmd
? What was a Msg
? Why did I even need them? I
resolved that the best way to find out was to build something every 90’s kid
dreamed of having.
If you guessed a Pokedex, then you either really wanted one too or you just read the title to my post. Either way, good job!
It turns out that what goes into a Pokedex is perfect for a beginner app. All you need to be able to do is search for the name of a Pokemon and display the information you want about that Pokemon. Conveniently - as if I planned it or something - there is also a well laid out and detailed API called the PokeApi.
With that in mind, my next step (apart from reading the Elm docs, of course), was to figure out what I wanted on the page. In addition to a search bar, I decided that I was going to also make a “Get Random Pokemon” button, which would introduce me to how Elm handles random numbers - since, spoiler alert, random numbers are handled differently in functional programming languages.
So, written out, here are the steps I followed to make the app:
- Set up the configuration to get a simple “hello world” page.
- Build a “Get Pokemon” button that only returned the PokeAPI response from one URL.
- Expand from a “Get Pokemon” to “Get Random Pokemon” button.
- Build a simple decoder that takes the API response and returns just a name.
- Add onto that decoder to get more deeply nested values from the response and build a Pokemon record.
- Move on to making a search bar, using Elm’s
onInput
functionality.
It seems like this would be slow, but it worked well to better understand some idiosyncrasies in Elm. Of course, there were some gotchas to this process that were unique to Elm that made things slower. However, there were also some victories to using this elegant, strictly-typed language that really made me feel like I was catching ‘em all.
Starting the app and getting a “Hello World” page
After installing Elm, all you need to do is go to your project folder and type
elm init
. That’s it. o(≧▽≦)o
Typing elm init
gets a basic folder structure started - it will prompt you to
make an elm.json
file which keeps track of the packages you use (like
package.json
does for npm
projects). It will also make an src
folder where
it will look for files you’ve created in your Elm project.
From here all I needed to do was set up a server and a Main.src
file.
Depending on how you set up your project, the compiler might also give you
further steps to follow. As I’ve mentioned at the beginning of this post, Elm is
thankfully really good in this department. You will almost never be left
scratching your head at a response from the compiler. Here’s an example of the
elm init
output in the terminal:
❯ elm init
Hello! Elm projects always start with an elm.json file. I can create them!
Now you may be wondering, what will be in this file? How do I add Elm files to
my project? How do I see it in the browser? How will my code grow? Do I need
more directories? What about tests? Etc.
Check out <https://elm-lang.org/0.19.0/init> for all the answers!
Knowing all that, would you like me to create an elm.json file now? [Y/n]:
Personally, I used nginx
for the server, since I’ve used it for previous
projects and knew how to set it up quickly. There are other options here like
Create Elm App, Elm
Live, and Parcel.
However, if you are doing something super simple, you can also just run elm
reactor
with no extra packages
needed.
To get a Main.elm
file started, I followed the example on this
page, since I knew my end goal
was to serve the result of a decoded JSON response and nothing super fancy. I
cut out everything but one Model
variant to have a result like this:
import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
-- MAIN
main =
Browser.element
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
}
-- MODEL
type Model
= HelloWorld
init : () -> (Model, Cmd Msg)
init _ =
(HelloWorld, Cmd.none)
-- UPDATE
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
(HelloWorld, Cmd.none)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
-- VIEW
view : Model -> Html Msg
view model =
div []
[ h2 [] [ text "Hello world!" ]]
From there, all I had to do was run elm make Main.src
and start up my server.
TL;DR: Elm makes starting up very straightforward, and there are many ways to serve and tweak your configuration to make it even easier for you and your team.
Fetching raw data from an API
Elm also makes this process pretty straightforward. I needed to make sure my
view
had a button with an onClick
event handler. If you come from something
like React or Angular, onClick
acts very much the same to what you know,
though instead of calling a function right there, Elm sends a “message” to its
update
function. In other words, instead of making a new function called
getMeAPokemon()
that handles all of the logic, I needed to create some new
“messages” and “commands” in the update
function of my app.
What does this mean? This is a part of the Elm
architecture. Of all things, this was
the hardest to wrap my head around at first. What makes Elm different from the
pack, and what might take some extra mental chewing, is that you send “messages”
around in your app when things change states, and paired with those messages are
“commands”. These commands are sometimes side-effects that do something
outside of your app. In this app, as you will see below, it is to complete an
HTTP request. Sometimes these commands are just… well… nothing. This is
reflected as Cmd.none
, which means that the process being completed has no
side-effect.
Here, the onClick
triggers a Msg
, which is sent to the update
function.
The Msg
waves its arms saying “Hey, this button was clicked!“ The update
function then knows what to do with this information based on the case
statement. The case
statement then directs you to the Cmd
that makes a GET
request to the PokeAPI and returns its response. When I get that response (a
Result
), another Msg
waves its arms saying “Now I’ve got something from the
API”. This Msg
(RandomPokemonResponse
) is then sent back to the update
function and, like before, it has a branch in the case
statement with the
appropriate course of action.
What makes RandomPokemonResponse
only slightly different than the first Msg
we saw is that the Result
tags along with it. This is so the Result
can be
dealt with as well: showing the user the info about the Pokemon if the Result
is "good” or showing an error when the Result
is “bad”. That’s it. Hopefully
this peels back some of the noise around the architecture, which definitely
confused me the first time I used it. (Even while I was trying to write this
blog post!) Here is the code below:
type Msg
= RandomButtonClicked
| RandomPokemonResponse (Result Http.Error String)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
RandomButtonClicked ->
-- Loading is a Model, like HelloWorld except it has the page say "Loading..."
( Loading
, Http.get
{ url = "https://pokeapi.co/api/v2/pokemon/ditto/"
, expect = Http.expectString RandomPokemonResponse
}
)
RandomPokemonResponse result ->
case result of
Ok fullText ->
( Success fullText, Cmd.none )
The view
function has a bit of HTML (written out in Elm, which is a point we
will get to later) that looks like this:
div []
[ text "Search for a Pokémon:"
, button [ onClick RandomButtonClicked ][ text "Random Pokemon"]
]
TL;DR: Unless you’ve written a Redux reducer, this is likely not an architecture you’ve seen before. This pattern may be what you spend the longest getting used to.
Getting a Random Pokemon
From here, I changed the button to get a random Pokemon instead of just Ditto all the time. This required some finagling, since Elm is a functional programming language. It requires functions to be pure.
Getting a random number is inherently impure: it gets a different number every
time you call it. Pure functions don’t act like that - if you give a pure
function an input x
and it returns y
, it will always return y
with input
x
. To get around this, I needed to lean heavily on the aforementioned Elm
architecture to do what I wanted. You can see the extent of my changes to make
this happen in the git diff of Main.elm
:
type Msg
= RandomButtonClicked
+ | ReceiveRandomId Int
| RandomPokemonResponse (Result Http.Error String)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
RandomButtonClicked ->
( Loading
- , Http.get
- { url = "https://pokeapi.co/api/v2/pokemon/ditto/"
- , expect = Http.expectString RandomPokemonResponse
- }
+ , Random.generate ReceiveRandomId generateRandomNumber
)
+ ReceiveRandomId id ->
+ buildRandomPokemonResponse id
+
RandomPokemonResponse result ->
case result of
Ok fullText ->
( Success fullText, Cmd.none )
Err _ ->
( Failure, Cmd.none )
+buildRandomPokemonResponse : Int -> ( Model, Cmd Msg )
+buildRandomPokemonResponse randNumber =
+ ( Loading
+ , Http.get
+ { url = "https://pokeapi.co/api/v2/pokemon/" ++ String.fromInt randNumber
+ , expect = Http.expectString RandomPokemonResponse
+ }
+ )
+
+
+generateRandomNumber : Random.Generator Int
+generateRandomNumber =
+ Random.int 1 150
Overall, when working with Elm, apart from the new architecture, you will find that you don’t always need to know bucketloads about functional programming. In a lot of ways this is great! You can just get up and running with it. Occasionally, though, there are inklings that leak through … like random numbers or HTTP requests, which are two examples you’ll see in my demo app.
When you start doing more intense things in your apps like mapping or dealing
with “contexts” (e.g., Maybe
),
you’re gradually eased in to some of the more hairy topics that languages like
Scala just throw you into. This is one of the many reasons why quite a few
people at thoughtbot recommend learning Elm if you want to get your feet wet
with functional programming or strictly typed languages:
Not only do you have your hand held by the compiler, but the names of more complex concepts are relatively friendly compared to other languages.
TL;DR: Elm is a functional programming language that doesn’t entirely feel like one. If you’re new to functional programming, start with Elm!
Building a Decoder Incrementally
This is notoriously something that gives people a lot of trouble in strictly typed languages. In Elm, this is no exception. I burned a lot of time working on decoders in my app. I was lucky that both Josh and Joël have written about this, which gave me a good foundation to work off of. Eventually, what I learned is to build a Decoder incrementally. Start small, then begin adding more detail as you go. In my case, I just started with the name of the Pokemon. Then, I went about adding things from more nested and complex structures within the JSON body, like moves and abilities.
I found that by starting small and thinking of each piece as a tiny building
block, I was less likely to make a mistake. It also made me really think about
what I wanted my Pokemon
type to look like. What information was actually
important to impart to the user? I found that going through the time to make a
decoder for something large (with possibly unnecessary fields) in Elm would take
a great deal longer, so the likelihood of me putting extra time into something
a user might not want was significantly less. I ended up building a much more
elegant and sparce data structure than I would have in something like TypeScript
or Ruby.
type alias Pokemon =
{ name: String
, abilities : List Data
, moves : List Data
}
type alias Data =
{ name : String
, url : String
}
Then, I was able to make a decoder for each attribute in the Pokemon data
structure and use that to make a larger decoder for the Pokemon itself. Here, I
got to use a map3
function, which is another neat trick from functional
programming that Elm makes accessible to new programmers.
pokeDecoder : D.Decoder Pokemon
pokeDecoder =
D.map3 Pokemon nameDecoder abilitiesDecoder movesDecoder
TL;DR: No matter the language, decoders are no fun when you first start out. With Elm, though, you are encouraged by the nature of the language to consider more simplistic data structures. This arguably makes programmers happier when working with the code. Overall, the ability to compose and reuse decoders becomes a boon the more complex your app gets. In addition, with Elm’s decoders, the shape of your data is guaranteed after it’s been decoded! No more “shotgun decoding” required (looking at you, JavaScript).
Building a Search Bar
To try something new, instead of making a “Submit” button, I used Elm’s
onInput
functionality, which sends out its associated Msg
when a user types
something into the input. In this case, when a user types something into the
search bar, I have a Msg
that corresponds to a branch in the update
function’s case
statement, which sends the user’s input to HTTP.get
via
buildSearchedPokemonResponse
. In there, if their input matches a Pokemon,
information will come back about that Pokemon. If not, they will get an error
message. For reference, here’s the update
function mentioned above:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
RandomButtonClicked ->
( Loading
, Random.generate ReceiveRandomId generateRandomNumber
)
ReceiveRandomId id ->
( Loading, pokemonRequestById id )
ReceivePokemonResponse result ->
case result of
Ok pokemon ->
( SuccessWithPokemon pokemon, Cmd.none )
Err _ ->
( Failure, Cmd.none )
UserTypedInName name ->
( Loading, pokemonRequestByName name )
Of all the features I had planned for this app, I thought this one would take me
the longest. However, with the way the Elm architecture is built, it took me
about 2 minutes to wire everything up. Here is the search
function in the
view
section of my app:
search : String -> Html Msg
search query =
div [ class "lines__header-wrapper" ]
[ div [ class "search__wrapper" ]
[ input
[ placeholder "Search for a Pokemon"
, type_ "search"
, value query
, onInput UserTypedInName
]
[]
]
]
For first-time Elm users, writing your HTML using Elm’s HTML
library is definitely a
bit strange, but doing so allows you to extract your view to different, smaller
blocks (sort of like the way I broke down the decoders). You can swap in
different “views” to your main view
function depending on what Model
you
return in your update
function. This allows you to conditionally render chunks
of your view
code and encourages you to make reusable “components” for your
page … sounds sorta like React, doesn’t it?
The difference between React and Elm, though, is that Elm’s reusable components
are actually just functions! React seems to be heading in this
direction,
but it’s not as strict as Elm (yet) — components in React can still be
classes that extend Component
. However, in Elm, you extract reusable code
only into functions - since Elm is a functional programming language from the
ground up, unlike JavaScript/TypeScript.
You can also add CSS to your app, either by rolling your own stylesheet and just
popping it into the index.html
file like you would in basic webpages, or you
can use one of Elm’s CSS packages to manage everything for you. I just rolled my
own since styling wasn’t my main focus here, but
elm-css
seems to be one of the more popular ones.
TL;DR: Elm basically has its own version of JSX and encourages you to
organize your view
into components. This makes for a highly reusable and
organizable UI, which React developers will appreciate.
Wrap Up
Elm is a language that is extremely conscious of first-time users. Getting up and running in it is much less difficult compared to starting a project using TypeScript - though Rails still takes the cake overall. In addition, I felt much more comfortable dipping my toes into functional programming practices using this language than a more abstract one like Scala. The added benefit of a strictly typed system and a helpful compiler made me feel much more focused and able to complete parts of my app faster.
Overall, there are a couple things to get used to when it comes to Elm. The first and most important is the architecture. If you plan on doing a project in Elm for the first time, I would read up on that to get an idea of what design considerations Elm expects of you. As I’ve mentioned above, that was what took me the longest to get used to. The other is building decoders - this is not something you have to do in a Rails or even a TypeScript project. Though, depending on how strict your settings are in the latter, you might add types to the responses you receive from an API, which is one step closer to the kind of work put into building an Elm decoder.
I would highly recommend anyone new to strictly typed languages or functional programming to check out Elm as a way to build your first project. I found it a joy to work with and I’m excited to do more with it.
PS: If you want to see the entirety of my project, you can visit the repo. I’ve also created a gist with all of the learning resources I’ve used / currently using. Have fun!