Skip to content
Learn Haskell, without the bullshit.
Branch: master
Clone or download
Latest commit d878b99 May 19, 2019
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
exercises Add video for Exercise #4. May 19, 2019
library Fix indentation in docs. Apr 28, 2019
.gitignore Fix html & js. Mar 25, 2019
LICENSE Update some stuff. Mar 20, 2019
README.md New section on "Managing state in a Functional way". May 8, 2019

README.md

Zero Bullshit Haskell

Let's learn Haskell. For real this time. 🚀

Zero Bullshit Haskell Youtube channel Zero Bullshit Haddock documentation

 

Table of Contents

 

Exercises index

Beginner

work in progress

 

Introduction

Haskell is a beautiful language.

It's not an easy language to learn though. Especially if you have years of experience in object oriented/imperative programming, getting into Haskell can be a bit of a pain.

This is my attempt at rectifying that. I want more and more people to learn about functional programming and Haskell in particular.

What's Zero Bullshit Haskell

The goal is to have a learning resource that is simple and digestable, with a great deal of hands on exercises. Most Haskell resources tend to be super dense with a lot of theory — I want to approach things going the other way. I want to draw as many parallels as I can with the Javascript world because that's what most people are familiar with these days and introduce concepts and theory only when necessary.

I want you to get the bare minimum amount of information to get going with Haskell and complete the first few exercises. As you get more comfortable, further reading material will introduce new topics that will help you solve the problem at hand while polishing your existing solutions.

All the nasty and scary jargon is pushed as further as possible and presented only when absolutely necessary. If you complete all the exercises, you will have gained enough intuition beyond certain patterns that it'll only be a matter of naming them properly. This will help you strenghten your fp knowledge and understanding of all the beautiful concepts behind Haskell — but none of that will be smashed in your face from the get go. I know how challenging and overwhelming it can be for a beginner.

Exercises

Exercises revolve around creating a webserver. It can be a very very simple webserver that just outputs hello, all the way to a concrete REST api that can handle state and connect to external services. We'll get there.

You can fiddle with the exercises with very little effort, as they come with a test runner that runs in the browser and hits http://localhost:7879 — where your Haskell server will be listening. That's right, no need to install any extra bullshit. All you need to do is go to the exercises website and start firing some AJAX requests. Read Local dev environment and first exercise.

Perhpaps most importantly, all exercises have a reference node.js implementation. We'll discuss how each exercise could be solved using express, what the pitfalls of that implementation might be and how functional programming can help us in writing more robust code. Have a peak at Exercise #01 to get an idea of how it works.

We're going to be using a Haskell library that is very similar in spirit and simplicity to express. In fact, I wrote it specifically for this project and I made sure it's devoid of any bullshit whatsoever. It's not production ready of course — we will transition to a more robust and widely used library later on.

Scope

This is not a Haskell tutorial.

You will not learn how to define types, functions or any of the syntax. Don't worry though! I think it's manageable to figure this stuff out on your own. For instance, going through Learn Haskell in Y Minutes a couple of times should be enough to get you started (note there's some bullshit at the end: ignore all the scary words, just play around with some of the examples).

With that being said, I'd like to have some content that covers the basics as well, eventually. Let me know if you'd like to see that happen.

Zero Bullshit Haskell is about overcoming the fear of types and start writing real Haskell code that is simple to understand and doesn't look scary.

 

Install Haskell in two minutes

This is the first step, and it's just a curl away, but first a bit of clarification.

You can't really install Haskell as much as you can't install Javascript.

What you want to do, is install a Haskell compiler. The Haskell compiler that everybody uses is GHC: The Glorious Haskell Compiler. See that? It's called glorious, so it must be good.

GHC is like the Node.js of Haskell. For reasons we don't really care about, using GHC directly is a bit nasty. What we want is a tool that manages a Haskell project for us and that tool is called stack.

Install stack now and keep reading.

curl -sSL https://get.haskellstack.org/ | sh

The need for stack

You can think of stack as npm or yarn — it's effectively solving a lot of the same problems. If you've already installed some version of GHC on your system, don't worry. Everytime you initialize a stack project, it will download and configure an appropriate version of GHC for you to use, so that you're not stuck with a global install. Yes, it's nice like that.

A word on cabal

There was a time when stack didn't exist and people used a tool called cabal. Those people experienced the so called cabal hell and eventually created stack out of frustration. Recently, cabal has gotten a make over and it has gained back some of its original userbase.

Given that we have no idea what we're doing, we'll stick with stack because there is really nothing wrong with it and works great for what we need to accomplish here.

Under the hood, stack still uses cabal. You might also find a .cabal file in your projects. Leave that file alone (it's auto generated) and forget about cabal.

Doing things with stack

There are two commands that you're going to use all the time. To be honest, they are basically all you need.

  • stack build --file-watch
    Watch .hs files for changes and rebuild the project
  • stack run
    Build (if necessary) and run the project

 

What are types and why you should care

If you've read a Haskell blog post, joined some functional programming chat or attended a conference talk, chances are you experienced how much people like to talk about types. It's the number one thing the Haskell community likes to jerk off about.

That's fine, once you get familiar with this stuff, you grow a weird and obsessive interest in pushing the type system, which is the set of rules that GHC follows to check that your program is correct.

But if you're like me, you don't.

Using and abusing the type system

There is a fine line between making sure something is absolutely correct but unreadable/impossible to understand, and something that is less safe (that is, has less guarantees at compile time) but is an actual piece of code that makes sense.

I personally don't get much excited about crazy types, but I still get to write Haskell for a living.

To me, writing simple Haskell that leverages only the absolute minimum amount of core features that are available in the language, is a joy to write, maintain and coming back to. I don't care about fancy types and you won't find any in this series. This is as practical as it gets and there are fantastic resources out there when and if you'll want to get into that stuff. (Couple of personal suggestions: i-am-tom/haskell-exercises and Thinking with Types)

Show me the types

Here are a few sample types. You should familiarize yourself with all the different ways you can define a type.

data Color
  = Blue | Orange | Green | Yellow

favoriteColor :: Color
favoriteColor = Green

A Color can be one of Blue, Orange, Green or Yellow.

favoriteColor is of type Color and has value Green.

data Person
  = Person
      { name :: String
      , age :: Int
      , hair :: Color
      }

bob :: Person
bob = Person
        { name = "bob"
        , age = 97
        , hair = Orange
        }

A Person is a record. bob is of type Person .

data Product
  = Book String Int
  | Movie String

oneBook :: Product
oneBook = Book "Some book title" 293

oneMovie :: Product
oneMovie = Movie "Some movie title"

A Product can either be a Book, which is made of a title of type String and the number of pages of type Int. Or it can be a Movie, which only has a title of type String.

We should use types to help us, so a better way of modelling this could be:

data BookTitle
  = BookTitle String

data Pages
  = Pages Int

data MovieTitle
  = MovieTitle String

data Product
  = Book BookTitle Pages
  | Movie MovieTitle
  
oneBook :: Product
oneBook = Book (BookTitle "Some book title") (Pages 293)

oneMovie :: Product
oneMovie = Movie (MovieTitle "Some movie title")

Types are your friends

You model your problem/domain through types.

The first thing you should do before writing any actual implementation of your program is figuring out your types. If you get your types right, the implementation will follow naturally and GHC will be able to help you along the way because it knows exactly which types it's expecting. Your job is then to just fill in the holes.

The more you can prove about the correctness of your program at compile time, the less bugs you'll run into in production. Types allow you to keep the smallest context possible in your head and let GHC figure out what's going on. That's an invaluable asset when you want to reason about your program.

Type safety is all about that. We're making our code safer through types. You can write some pretty complex and weird code through fancy types but that's bullshit, especially at this stage, so we're not getting into any of that.

A well modeled but simple type is just as useful. Get your types right and the implementation will follow. This is one of the main reasons Haskell is so appealing.

 

Local dev environment and first exercise

All right, let's get down to business!

Make sure you have stack installed and create a new project — name it however you want.

stack new types-arouse-me

Now go into that folder and build. It might take a few minutes the first time.

stack build

While it's building, crack open stack.yaml in your freshly created project and tell it where to find the zero-bullshit library. This is the library you're going to need to complete the exercises. It's similar in spirit to express and sinatra in that it's super basic. I wrote it specifically for the Zero Bullshit Haskell project and believe me when I say I kept all the bullshit at bay!

# stack.yaml

extra-deps:
- github: alpacaaa/zero-bullshit-haskell
  commit: master
  subdirs:
    - library

(Yes, we're pulling from Github. This library might end up on Hackage some day).

Then open package.yaml to require the package as a dependency.

# package.yaml (recall this is basically package.json)

# Turn on some useful extensions while we're here!
# These should really be on by default. They just make
# the language nicer, don't worry about them.
default-extensions:
- OverloadedStrings
- DeriveGeneric
- DeriveAnyClass

dependencies:
- base # feel free to omit the version constraint
- zero-bullshit

Getting ready

Let's now take a peek at the first exercise.

Exercise #01 - Static String

I built all exercises so that they have an integration test that you can run straight from your browser. Head over to the Exercise #01 page to see the mocha test runner ready to go. When you run the tests, they will hit localhost:7879 which is where your server is going to be listening.

Obviously we have nothing running locally yet, so you should see a message saying that your server isn't running.

That's absolutely fine, we'll fix it in a minute. First, we want to run our vanilla stack-generated Haskell project just to check what's going on.

stack run
> someFunc

What's this someFunc business? It's just a poorly worded hello world message. But more importantly, we have compiled and ran a Haskell application!

Let's write some Haskell

There are currently two files in our project app/Main.hs and src/Lib.hs.

Most of the times, you'll be editing stuff in the src folder — that's where 99% of your code should be. app is just for executables, which are thin entrypoints to your application (we have a single executable right now).

If you open src/Lib.hs you'll see this:

module Lib
    ( someFunc
    ) where

someFunc :: IO ()
someFunc = putStrLn "someFunc"

The very last line is the one we care about and hopefully it makes sense — we're just printing some text to the console.

The first thing we want to do is import the Zero.Server module from the zero-bullshit package.

import qualified Zero.Server as Server

And then we want to have our server listen for requests, even though it isn't able to handle any at the moment. So we end up with something like this:

module Lib where

import qualified Zero.Server as Server

someFunc :: IO ()
someFunc = Server.startServer []

Now, give stack run another go and visit the integration tests page again. If your code is compiling and running correctly, the warning message should have disappeared!

That's right, the test runner has detected that our server is up and it's ready to run the tests. Let's give it a go, it will fail miserably.

Completing the first exercise

The test is making a GET request to /hello and is expecting a response with body hello. That shouldn't be too hard to implement. The main ingredients are simpleHandler and stringResponse.

I highly encourage you to watch the video where I go through this to understand what's going on. I'm pasting the solution here for reference (you'll find solution for other exercises linked in each exercise page).

helloHandler :: Server.Request -> Server.Response
helloHandler _
  = Server.stringResponse "hello"

-- Change `someFunc` to `run` because we're not savages.
-- You'll have to update this in app/Main.hs as well.
run :: IO ()
run
  = Server.startServer
      [ Server.simpleHandler Server.GET "/hello" helloHandler
      ]

Execute stack run again and give the test another go, it should work.

Congratulation on getting this far! Don't worry if you weren't able to follow along this exercise or any of the following ones. I plan to record videos where I go through each exercise in detail, uncovering new concepts and clearing up what we're doing differently compared to the node.js implementation. With time, you'll realize how many things are uncomfortable/dangerous in the Javascript world, although I must admit you probably think Haskell is the funny one right now. Stick around, it will be worth it!

 

Standard library and importing modules

Imagine you've just invented a new programming language. Now you need a standard library — a set of functions, types and a bit of blackbox magic baked into the compiler for developers to use.

Standard libraries are very hard to get right the first time. Once you make a bad decision, it's extremely difficult to fix or deprecate it because everyone is using that thing. Effectively, all programming languages I've learned over the years have a somewhat broken standard library. So it shouldn't come as a surprise that the Haskell's one isn't great either, but it's not that bad.

The Prelude

The standard library in Haskell is called Prelude.

It comes with the base package (you might have seen it listed as a dependency in your project, that's what it is). Anything in the Prelude module is imported by default, meaning you don't need an explicit import statement. Note that there are other modules in base and those do need importing (for example Data.Maybe). It might seem there's a lot of stuff in base — it is indeed a chunky package. But the day to day stuff you'll need is only a subset.

While not perfect, the stock Prelude will do just fine for our purposes. The main thing you have to watch out for is partial functions. In case of errors, these functions throw exceptions when they really shouldn't be. When you go through the exercises, you'll find warnings against usage of these functions and alternative safer implementations.

Qualified imports will keep you sane

It's often a good idea to qualify your imports.

import qualified Zero.Server as Server
-- In Javascript, this would be the equivalent of:
-- import * as express from 'express'

This way, you're not polluting the scope with a ton of functions and types. Most importantly, you'll always be able to tell where a function or type is coming from.

Sometimes it's handy to import specific types, or widely used functions, in which case it makes sense to use an unqualified import. But be careful, one of the things I struggled the most with as a beginner (among other things obviously) was having too many unqualified imports (it's just super confusing).

-- The `Text` type and the `fromMaybe` function are super common.
-- It makes sense to import them unqualified.

import Data.Text (Text)
import Data.Maybe (fromMaybe)

example :: Text
example = fromMaybe ...


-- Next, the qualified version.
-- `Text.Text` is really uncomfortable to type over and over again and it just adds noise.
-- This is one of the instances where I would stick with unqualified imports.

import qualified Data.Text as Text
import qualified Data.Maybe as Maybe

example :: Text.Text
example = Maybe.fromMaybe ...

 

How to debug Haskell code

TLDR; the closest equivalent to console.log is traceShowId.

import qualified Debug.Trace as Debug

{-
  In JS we would write:
  const sum = (a, b) => {
    console.log(a)
    console.log(b)
    return a + b
  }
-}

sum :: Int -> Int -> Int
sum a b
  = (Debug.traceShowId a) + (Debug.traceShowId b)

-- >>> sum 1 2
-- 1
-- 2

Not quite the same is it? That's because everything in Haskell is an expression. (Note that the Debug.Trace module is part of the base package, so there's nothing extra to install)

Everything is an expression

In Haskell there are no statements. At no point you can just say "do this" — your program is built as expressions composed together. So it gets tricky because console.log is very much a statement that you can basically drop anywhere and it works. But we have no such thing in Haskell so how can we work around that?

The trick is to "wrap" the value you want to log with any of the trace functions. The idea is that the trace function will print something to the console and give back the original value unchanged. This is cool because our expression evaluates to the same result whether it's wrapped in a trace function or not. In other words, these two implementations of sum are equivalent:

sum :: Int -> Int -> Int
sum a b
  = a + b

sumWithLog :: Int -> Int -> Int
sumWithLog a b
  = (Debug.traceShowId a) + (Debug.traceShowId b)

They produce exactly the same result. In fact, the type signature for traceShowId is

traceShowId :: Show a => a -> a
-- Ignore the `Show a =>` bit, we haven't really seen that yet.

Which means that whatever we give it (an Int in the sum function above) will be given back to us unchanged. Effectively, whether the trace function is there or not makes no difference!

Let's look at another example.

{-
  In JS

  const sumItems = (items) => {
    const reducer = (acc, item) => {
      console.log(item)
      return acc + item
    }

    const sum = items.reduce(reducer, 0)
    console.log("the sum is " + sum)
    return sum
  }

  sumItems([1,2,3,4])
-}

sumItems :: [Int] -> Int
sumItems items
  = Debug.trace ("the sum is " ++ show sum) sum

  -- trace :: String -> a -> a
  -- print the first argument (String) and return the second argument (a)
  where
    reducer item acc
      = acc + (Debug.traceShowId item)
    sum
      = foldl reducer 0 items

I appreciate this might look a bit unfamiliar, but if you apply the "wrapping" logic it will make sense. As many other things in Haskell, it will take you some time getting used to. The trick is to realize that everything is an expression in Haskell and we have to work with this constraint to fit an extremely imperative concept ("log this, log that") into a purely functional language.

But... side effects?!

You might not have realized it but: printing to the console is a side effect! How come none of the trace functions (all right, excluding traceIO) have IO in their signature??

The putStrLn function, which is used to print to the console, does correctly express in the type that it's impure and some side effects will occur.

putStrLn :: String -> IO ()

However, trace functions don't and that's because they are not type safe on purpose.

Remember that these functions are meant to be used to debug code, and most of the code we write will hopefully be pure. Let's pretend for a moment that trace functions did run in IO.

fictionalTrace :: String -> a -> IO a

sum :: Int -> Int -> Int
sum a b
  = a + b

-- If we now wanted to log something in the `sum`
-- function, we would have to modify its signature!

sumWithLog :: Int -> Int -> IO Int
sumWithLog a b
  = fictionalTrace ("the sum is" ++ show sum) sum
  where
    sum = a + b

This would be nuts. Not only you would have to edit your function everytime you wanted to log something, but all the places where that function is used will need to change as well! You would soon go insane and sure enough just write all of your functions in IO, just to avoid the pain.

That's not the code we want to write! After all, having IO everywhere wouldn't be much different than writing Javascript, because we would have lost the ability to distinguish pure functions from impure ones.

The takeaway is: yes, trace functions are a bit weird because they have side effects (printing to the console) but they don't run in IO. However that's on purpose, to make it more ergonomic for us developers in need of some sweet logging when fixing a bug.

For proper application logging you wouldn't use trace functions, just as much as you wouldn't use console.log. But that's a topic for another time.

 

Encoding and decoding JSON

We haven't mentioned JSON so far, but we better start looking into it so we can deal with the Real World. Going from JSON (decoding) and to JSON (encoding) in Haskell is not as easy as in Javascript, where everything is untyped. In Javascript we can just JSON.parse() and hope for the best, but we get no guarantees as to what we're actually getting back. Indeed, we hardly get any guarantees at all — that's why we're learning Haskell after all!

Before we go any further, we need to quickly introduce a couple of things: instances and constraints.

A brief interlude on instances

You might have seen type definitions followed by deriving , for example:

data Person
  = Person
      { name :: String
      , age :: Int
      }
  deriving (Eq)

bob :: Person
bob = Person "bob" 69

alice :: Person
alice = Person "alice" 99

-- We can use the equality operator only because
-- `Person` has an `Eq` instance
-- (which we derived through `deriving`)
samePerson
  = if bob == alice
      then "Weird, they're the same"
      else "Duh, of course they're different"

Here, we're defining a new data type Person. However, the definition alone gives us a pretty limited type, because we wouldn't even be able to compare (==) two different Persons by default! So if we want to enhance our type with equality, we need to tell the compiler that we'd like to derive an instance for Eq, so that comparing bob and alice becomes possible.

Note that the term instance in OOP means something different. In Haskell, an instance is just a way to describe what a type is or can do. In this case, the Person type supports equality.

All you need to understand is that we can derive instances for our types so that we can do more useful things with them.

An even shorter interlude on constraints

Instances alone wouldn't be that interesting — they become very useful when paired with constraints. You can constraint the usage of a function so that a type parameter must satisfy certain conditions. Constraints are declared on the left of the fat arrow (=>). For example:

-- You can only call this function IF the type has an `Eq` instance and a `Show` instance.
-- (`Show` is for turning values into Strings.)
compareAndPrint :: (Eq a, Show a) => a -> a -> String
compareAndPrint p1 p2
  = if p1 == p2
      then "They are the same person! " <> (show p1)
      else "Not the same person. You gave me" <> (show p1) <> " and " <> (show p2)

-- If we then derive `Show` as well as `Eq`,
-- the `Person` type will satisfy all the requirements of `compareAndPrint`.
data Person
  = Person
      { name :: String
      , age :: Int
      }
  deriving (Eq, Show)

-- >>> compareAndPrint bob bob
-- "They are the same person! Person {name = \"bob\", age = 69}"

Again, we can't rely on the Person type being showable by default, we need to derive a Show instance. And here's the super interesting thing: compareAndPrint can only work with types that can be compared and shown.

(Eq a, Show a) are constraints. They are necessary if we want to use == and show in our functions. An helpful way of looking at constraints is to read the full type signature in English:

compareAndPrint :: (Eq a, Show a) => a -> a -> String

If you want to use compareAndPrint , you have to give me two values of type a. But a can't be any type! I'm looking for a type that has at least (it can have more) instances for Eq and Show.

If that's clear, you have all you need to understand how JSON encoding/decoding works!

Side note, types in Prelude have been defined with common instances. So, for example, you could call compareAndPrint with Int and String because both satisfy the Eq and Show constraints.

compareAndPrint 1 1
compareAndPrint "yes" "no"
compareAndPrint True True

FromJSON and ToJSON

How do we turn the Person type into JSON and how do we read a JSON value back into a Person? Easy, we just derive some more instances!

First of all, we need to install the aeson package (it's the Haskell package to deal with JSON basically). As you know, installing a package means opening package.yaml and adding a new dependency.

# package.yaml

dependencies:
- aeson # get this bad boy in!
- base
- zero-bullshit

Now we can derive FromJSON and ToJSON on our Person type!

import GHC.Generics (Generic)
import qualified Data.Aeson as Aeson
import qualified Zero.Server as Server

data Person
  = Person
      { name :: String
      , age :: Int
      }
  deriving (Eq, Generic, Aeson.FromJSON, Aeson.ToJSON)

-- Notice the `Generic` instance.
-- It's required when you want to derive `FromJSON` and/or `ToJSON`.

Dope. How do we use them in practice? There are two functions available in the Zero.Server module that you'll end up using quite a lot: decodeJson and jsonResponse.

 

jsonResponse :: ToJSON a => a -> Response

If you give me a type a that can be turned into JSON, then I'll produce a Response with the JSON representation of that type.

 

decodeJson :: FromJSON a => String -> Either String a

If you give me a type a that can be parsed from a String (ie. the JSON encoded value), then I'll either give you an error (in case the JSON is invalid) or a value of type a.

In practice, if our server gets a request with some JSON encoded body, we can try to parse it into the type we expect.

myHandler :: Server.Request -> Server.Response
myHandler req
  = Server.stringResponse result
  where
    body
      = Server.requestBody req
    result
      = case Server.decodeJson body of
          Left err -> "Failed to decode request body as a Person. It must be something else"
          Right p  -> "Yay! We have a person named: " <> (name p)

Side note: where is that name function coming from? Recall that when you define a record in Haskell (Person in this case), one accessor function per field name gets automatically created as well. name and age are functions in scope here and we can use them to get values out of a record of type Person.

And, if we have a Person that we want to send as JSON, we can just do that (jsonResponse will also set the proper Content-type header).

bob :: Person
bob = Person "bob" 69

myHandler :: Request -> Response
myHandler req
  = jsonResponse bob

The first few Zero Bullshit Haskell exercises don't deal with JSON at all, but given that exercises are about writing webservers, then you need to understand how to work with JSON. Exercise #07 is all about putting what we just discussed into practice. If something isn't clear, join the discussion in the relevant issue!

 

Dealing with mutable state

Mutable state is one of the most common sources of bugs I regularly faced over the years. In Haskell everything is immutable (which is super nice) but sometimes we have to deal with mutable state nonetheless. How do we do that?

Modeling state the functional way

In Javascript, we can just define a global variable and get away with it. For example, here's how we could implement a counter that gets increased with every request.

var mutableCounter = 0

app.post('/increase-counter', (req, res) => {
  mutableCounter += 1
  res.send('Current count: ' + mutableCounter)
})

We could still define a global mutableCounter value in Haskell, but there wouldn't be any way to update it.

mutableCounter :: Int
mutableCounter = 0

counterHandler :: Server.Request -> Server.Response
counterHandler = ???
-- mutableCounter = mutableCounter + 1
-- No way the compiler would allow that!

Ok fine, GHC always has to ruin the party. Let's try to model a change of state as a function then. So far, our counterHandler just takes a Request and returns a Response. A good first step would be adding the current counter value as an input to the function, so that we can use it!

counterHandler :: Int -> Server.Request -> Server.Response
counterHandler currentCount
  = Server.simpleHandler $ "Current count: " ++ show (currentCount + 1)

This is fine, but currentCount is still an immutable value, so we can't do anything with it. Sure we can now return the current value to the client, but we can't update it.

What if we could let the server manage state for us instead? Right now, our handler only returns a Response, but there's no reason why we couldn't return extra information. In fact, we can tell the server that along with the Response, we now also return an updated version of the state! The server will somehow take that new value and do the actual mutation, we're not concerned with the details. The impure state update is hidden in library code and we can keep on writing nice pure functions. Turns out we don't really need mutable variables after all. :)

counterHandler :: Int -> Server.Request -> (Int, Server.Response)
counterHandler currentCount
  = (newCount, response)
  where
    newCount
      = currentCount + 1
    response
      = Server.simpleHandler ("Current count: " ++ show newCount)

Note how we now return a tuple with two elements (the new state, the Response) instead of just the Response.

This is fantastic. All of our values are still immutable, we're just using functions to model state updates. The great thing about this is that counterHandler is still super easy to test. You feed it some state and check if the state you get out is the correct one. Have you ever written a test for a function that dealt with global mutable state (like in the JS example above)? I did, and then proceeded to learn Haskell.

A slight detour in OTP land

OTP is the set of indispensable libraries that you use to build Erlang/Elixir programs. Erlang is a functional language with immutable variables and, as you can imagine, wouldn't allow you to mutate any state.

One of the core abstraction in Erlang is the gen_server. A gen_server allows you to model a server by providing callbacks that hook into the server lifecycle, so that you can implement your logic there.

Here's how the callback to handle requests looks like:

 handle_call(Request, From, State)

It takes a Request, the process id of who's making the request and the current state of the server. What do you think the return value is?

{reply, Reply, State1}

You guessed it, once again the trusty tuple!

Along with the Response (Reply in this case) we also return an updated version of the state, here referred to as State1 to indicate it's going to be different than State.

Let the runtime handle state for you

We almost have all we need to build stateful request handlers. Let's drop some type signatures to understand what we're missing. In the zero-bullshit package, we create stateful handlers with the function statefulHandler.

simpleHandler
  :: Method
  -> String
  -> (Request -> Response)
  -> Handler

statefulHandler
  :: Method
  -> String
  -> (state -> Request -> (state, Response))
  -> StatefulHandler state

This signature is similar to the familiar simpleHandler that we've used up until now. We know what Method and String are, so let's ignore those. The callback is interesting though (the function between parens). Note also how the return types are different. We wouldn't be able to pass a StatefulHandler directly to startServer, which only accepts Handler.

Let's bring back counterHandler and compare it to the callback we have here.

                 (state -> Request -> (state, Response))
counterHandler :: Int   -> Request -> (Int,   Response)

Wait, it's exactly the same thing! state is what we call a type variable. We have seen a few of those already, when decoding JSON for example. We could have used a instead of state and it would have worked just the same, but it's good to be explicit with type variables when you can.

So state is a type, any type for that matter, it's not constrained in any way so you can use whatever you want (we're using an Int here).

The last bit is StatefulHandler state. You might be wondering why state is there. Why can't our type just be StatefulHandler? The reason is we want to track the type of state an handler is compatible with. This is to make sure we don't mix handlers that work with Ints with handlers that work with Strings for example. If it's still confusing, keep on reading and remember that state is just a type variable (meaning it can be replaced by Int, String, Person whichever type you want).


How do we get an Handler from a StatefulHandler state so that we can pass it to startServer? We use handlersWithState.

handlersWithState :: state -> [StatefulHandler state] -> Handler

The server needs to know what the initial state is going to be, so that's the first argument we need to pass to handlersWithState. And then we can just pass a list of StatefulHandler provided that they all work with the same state type. That's why we need the type variable!

handlersWithState is a polymorphic function. It works with any type of state. Do you want Int? That's fine. Want String? Fine as well. But it must match the type of the StatefulHandlers you provide.

We can use type variables in type constructors to carry extra information at the type level. In this case, StatefulHandler state carries more information than just StatefulHandler because we can talk about the state as well. The better you model your types, the more guarantees you get at compile time. You're reducing the number of invalid programs that the compiler will accept and get free errors (at compile time) when your types don't line up.

That's where a type system is incredibly useful. If our program doesn't make sense at the type level, we're not going to be able to compile it and have bugs sneak into production code.

Putting it all together, we can use our counterHandler like this:

initialState :: Int
initialState = 0

counterHandler :: Int -> Server.Request -> (Int, Server.Response)
counterHandler currentCount
  = (newCount, response)
  where
    newCount
      = currentCount + 1
    response
      = Server.simpleHandler ("Current count: " ++ show newCount)

main :: IO ()
main
  = Server.startServer
      [ Server.handlersWithState initialState [counterHandler]
      ]

This is how we model state in a functional way.

At the end of the day, there's still going to be some mutable variable that gets updated under the hood, but the benefit of using this approach is we only get to deal with the nice api on top. When writing a gen_server in Erlang you don't have to think about how to manage state, everything is taken care of, just supply a callback. The same goes for the zero-bullshit server, you just have to pick a state of your liking, handle the Request and return an updated version of the state together with a Response.

You can’t perform that action at this time.