Damola's blog

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


Adedamola Shomoye

By Adedamola Shomoye

Join the...