Here Be Functions
A foray into functional programming, well, into elm really.
April 30, 2021 | 9 min. read
After a few years of writing JavaScript (JS), well, React, Iāve been slowly gravitating towards a āfunctionalā approach to writing code. I say āfunctionalā because JavaScript is not a functional language. It does not enforce functional paradigms. These guarantees, I think is the real draw to writing in a functional language. If your code compiles, itās not only syntactically correct - something youād get from almost any compiled language, itās also algebraically sound: the code wonāt run into, null
exceptions , or unhandled errors. Guaranteed! (terms and conditions apply)
I wanted to give a ādefinitionā of functional programming but I couldnāt find an authoritative one. It appears, like many things, āit dependsā. For the sake of this post, these what I think important are:
- no side effects/immutability: no āvariablesā, only new values.
- first-class functions: pass functions as parameters and return values and functions. A lot more languages support this, in fact, all the languages I mainly write (Go, Python, Js) have first class functions.
-
algebraic data types (and a strong type system): with a lot of discipline, itās possible to write javascript and satisfy the previous points. But, JavaScript is dynamically and loosely typed, ādata typesā isnāt even a thing.
- Typescript does support discriminated unions, but the compiler can not guarantee (ie force) total coverage of a union - also a broken type guard will bring it crumbling down. This is an important property of pattern matching in functional languages, it ensures every case is covered, otherwise the code is invalid. Rust has pattern matching, and I itās part of why I liked Rust.
// discriminated union in TypeScipt
type Value =
| Number
| String
Iām no expert so Iāll add a wiki link to functional programming.
What language to speak
I already know of some functional languages. I think Haskell
is probably the ādefaultā (or at least famous for being) functional language.
Unfortunately, I have past, painful experience of attempting to learn Haskell. I wouldnāt risk restarting my functional journey with Haskell. There are many other options, Clojure
, F#
Erlang
, and Elixir
etc. Elixir is particularly interesting - especially because of the Phoenix framework: which is the Ruby on Rails for Elixir. Elixir is based on the Erlang VM and so itās really great at real-time applications. Discord, for example, runs a lot of Elixir. Phoenix is impressive, but I didnāt want to learn Phoenix, I wanted to learn functional programming. I couldnāt think of a small real-time idea that I could do in a short time that would justify learning a new language and framework. I know I can learn it, but lacking an exciting idea to work on, I also didnāt want to risk losing motivation.
Elm
Elm is unique for frontend. It does not āspeakā JavaScript, at all. JavaScript is still the compilation target but the compiled output has guarantees that I donāt know any other JS compiler provides. This is possible because Elm does not allow calling JavaScript functions (no FFI). That means all the guarantees Iād mentioned earlier are still here. I decided I would create a copy of āGuess my wordā. A simple version where the computer picks a random word and the user has to keep guessing until they find the word. The āhintsā is that every guess is placed on wether itās alphabetically before or after the hidden word. First, I went into reading the documentation/guide https://guide.elm-lang.org. This is really good documentation. If you want to learn Elm, that guide is the best reference. In fact, it should be all you need (to start). That being said, here are some key concepts in elm that stood out to me:
Declaring variables:
You donāt. Next! ->
Functions:
Well, this is really the entire language. There are named and anonymous functions:
-- regular "greet" function
-- there are no variables, but values, like the "hello" String below.
greet name = "hello " ++ name
-- wrap an intermediate function call in parenthesis
-- anonymous greet function - easily pass to higher order functions like map
List.map (\name -> "hello " ++ naame) ["John", "James"]
-- map is curried here, say_hello runs greet over a string list of names
say_hellos names =
List.map greet names
Calling a function is its name followed by all the arguments, separated with spaces, no parenthesis, no commas. It immediately becomes an obvious improvement once youāre many levels deep in a function composition. There are no intermediate variables, you pass the result from one function to another function to another function and keep composing all the way down (or up? š¤)
Types
The type system is where, I think, function programming really shines and makes the, somewhat weird (but eventually intuitive) paradigm worthwhile.
Elm has the usual types youād expect, String,
Int
, Float
, List
, Bool
Thereās type annotations for functions:
-- the compiler would have inferred the annotation below but it's usually better to create an explicit contract/api.
say_hellos: List String -> List String
say_hellos names =
List.map greet names
Records Kind of like Shapes/JavaScript Objects:
type alias User = { name : String, age : Int }
Immutability
For a function that updates age
, always returns a new user record :
-- updating a record uses a pipe |. Like a javascript spread and update.
setAge user newAge = { user | age = newAge }
Data Types
type NewUser = Full User | Partial String
Pattern Matching
With NewUser
type above:
In JavaScript, you can, (and probably will) forget to check wether a value is a User
or a String
. Try to use the age
, and then fun things happen.
But in Elm, with the declared type, you will never run into this problem.
Hereās what a greetUser
function would look like:
greetUser user =
case user of
Full u ->
-- here, u is guaranteed to be a User
greet u.name
Partial name ->
-- string
greet name
It is verbose, but having the compiler ensure itās impossible to āunwrapā a NewUser
without pattern matching over it eliminates a whole bunch of errors that would occur in so many imperative languages. And thereās no escape hatch for this, or ātrickingā the compiler (like you can in TypeScript etc)
No Side Effects
The idea of not having side effects can sometimes be hard to understand. I definitely didnāt fully comprehend it until working with elm and using the elm/http
package. Network requests are non-deterministic operations. You literally never know what could happen. This makes the concept hard to fit into a functional paradigm because you canāt just āpassā a network function call to another function. That network ācallā may never return (not really, but still). Many languages use async calls, others wait and timeout etc. In elm, thereās commands. any function that has to interact with the āoutsideā is non-deterministic, must be a command. This makes sense with a network call. But, itās also the case with random
. You need things like a seed (like the current time), a sort of side-effect.
Another interesting fact about elm: thereās no random access in lists! This almost broke my brain why I found out. You canāt do something like get [1,2,3,4,5] 1
, to get 2
out of the list.
elmās lists are linked lists. Thereās no method or concept of going to an arbitrary point in constant time. I ended up just installing a package that chooses a random item from a list. Itās still linear but at least I donāt have write all the code to handle that.
To think I would need to install a package just to choose an item from a list. Crazy.
The elm Architecture makes the following easier to follow
Building - something
Armed with what I consider to be enough knowledge of elm
. I started on with the word guessing app I described.
It works like so:
- Load the page, with no data
- call an endpoint to fetch json data of all words (itās a 3MB file so it makes more sense to lazy load)
- choose a random word from all the words
- start the game, with an input field for the user to enter words
-
repeat: user enters word:
- if word == random word, user wins
- if word is not valid (in all words) reject
- if word not already entered, store in entered
- else reject word
Elm uses a ācomponentā-type way of building UI. In my case, I only used one main
component.
The component requires a Model
type that represents all the stats that the component can be in.
Hereās the model I created:
type Model
= Loaded GameState
| Loading
| Error
| Won GameState
type alias GameState =
{ words : List String
, enteredWords : List String
, magicWord : String
, guess : String
}
Within an elm
component: to trigger a state transition, a Msg
is sent, and elm
passes this msg
to the update
function. This is like actions and a reducer.
type Msg
= GotWords (Result Http.Error (List String))
| GotMagicWord ( Maybe String, List String )
| Guess String
| CheckGuess
| Reset
The game starts in the Loaded
state, then an http cmd
starts to get all words, it enters the Loading
state, then it either enters the Loaded
state with a GameState
that has words
; if successful or Error
state if not.
When entering the Loaded
state, the update function calls a cmd
that pulls a random word from all words - when the command returns, the game stays in the Loaded
state but a new GameState
is returned with magicWord
The Loaded
state is where the game playing occurs. Thereās no transition until the user wins (after CheckGuess
Msg
is handled) into the Won
state. A reset will return directly into Loaded
.
The update
function is fairly long since it handles all transitions, hereās a snippet of it:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotWords result ->
case result of
Ok words ->
( Loaded (GameState (List.map String.toLower words) [] "" ""), randomWord words )
Err _ ->
( defaultModel, Cmd.none )
-- there are other matches on msg
...
Views
The one part of elm
that is still growing on me is how to render views, elm
ās version of jsx
.
On the upside, itās just functions. On the downside, because of how much nesting happens in html
. It looksā¦, well it looks like this:
view model =
app []
[ container []
[ div []
[ h2 [] [ text "You Win!" ]
, img [ src "https://source.unsplash.com/random", Html.Styled.Attributes.width 300, Html.Styled.Attributes.height 300 ] []
, div [] [ button [ onClick Reset ] [ text "Restart" ] ]
]
]
]
The code above is one case
(in Won
state) from the view of the app,
Notice the onClick
handler on the button which expects Msg
which then gets sent to update
ā¦etc
Every html element is a function that takes two arguments: a list of its attributes (like src
in image, event handlers, etc; and a list of its children.
So: <h1>Hello</h1>
looks like: h1 [] [text "Hello"]
text
is a special function that sets the text of an element.
Result
Hereās the repo of the entire āappā. Didnāt bother hosting it.
But it runs! pinky swear! (I use elm reactor
the entire time)
The best part of all this is that I spent maybe an hour actually writing the entire code.
Sure, its trivial, but its a new language etc. Itās super nice to write code, and once all the red squiggles are gone, it just runs! I wired the entire thing up (with http requests) without looking at the browser because all the āedgeā cases were just glaring at me in the code. Not even TypeScript will give you that.
Final Thoughts
Functional programming is sweet - or, I should say elm
is sweet! tomayto, tomahto.
But more importantly, functional programming is not that foreign . It just takes some time working with it. I intend to eventually find a good use case for elixir
so I can really build some āgrown upā projects in it - because I am certain I am going to be super productive and satisfied working with it.
Last updated: April 30, 2021