Understanding Suave In F# Using Integers
The best moments in programming are those when you finally understand a concept you have struggled with for a long time. It is very exciting because you have acquired a new way of thinking. However, once you internalize these concepts and they become “so obvious” to you, you tend to forget about the time you struggled with them and it ultimately makes you a really really bad teacher when explaining these concepts to others.
This was the case with me when I tried to explain how and why Suave works to a F# newcomer and failed miserably: I assumed it would be as “obvious” for the other person just as it is for me now.
Today, after that experience I want to give it another try and go slowly, step by step in explaining the concept and hopefully I will do a better job this time.
Note that this article is about how and why Suave works, not how to write apps in Suave. So this is more about the underlying techniques and the Suave DSL.
The Suave DSL
Suave provides a DSL that lets write web apps, it looks like this:
Beautiful! If you have done any web programming, you might have a vague idea of what this app will do, when the server receives a
GET request at path “/hello” it will return “Hello GET” back to the client and so on and so forth. We will try to understand how this DSL works from first principles.
Motivation: Understanding The Problem
Let us think for a moment about what a web server always does, roughly from a high level perspective: it listens for requests, filters them here and there, often based on the path or request method and then it returns a response. In summary, a web server
- 1) Listens for requests
- 2) Filters requests
- 3) Returns responses
Your application code is mostly concerned with (2) and (3). Listening for requests (1) is what the framework does for you internally. Simplifying the definition of web server even more: given a request, return a response.
Now it is starting to sound a lot like a function! The input is a request and the output is a response.
This is of course oversimplifying the server and ignoring some technical details, for example the fact that you write to the response stream and not necessarily create a new one and the fact that such web server function must be able to run concurrently etc. but hopefully you get the idea: the problem of building web applications can be reduced to a problem of writing the web server function.
However, if you want to write such function for a realistic web application, it would be GIGANTIC! It would have to do everything: the routing logic, handling headers, handling security, resulting in many many branches and it would be hard to re-use parts of the function in different places. Let alone unit testing such function would be an unpleasant experience. To write a function in pseudo-code that resembles the web application from the first code snippet, it would look like this:
We definitely don’t want to write our web apps like this.
Motivation: The Solution
A gigantic function, what could possibly be the solution to simplify it? Well, it is the good old divide and conquer technique: like with any large function that has too many concerns, you break it down into smaller simpler functions and then combine them to make bigger ones. This is exactly what Suave provides: small parts that you can combine together to make up larger parts that ultimately become your web application.
In the next sections we will look at how to write such small functions and how to combine them to make larger ones. For the sake of simplicity, we will do so using integers instead of web requests or responses, it all follows the same principle.
Small functions: filters and converters
We will take a look at two categories of functions:
- Functions that filter their input
- Functions that convert/transform their input
The best way to explain these is by examples, filtering functions look like this:
As you can see, they check their input, if the input matches some predicate (like being greater than 5, less than 10 etc.) then it returns that input, otherwise, we will return
The type of the filtering function is important:
int -> int option keep it in mind.
You might ask: “why not just use a boolean as a result of the function?” Well, we use
Option<'t> so that if the input passes the predicate, we can use it later from the output, for example, you will be to do this:
Now to the fun part: suppose I want a new filtering function that matches with integers greater than five and those less than ten. I could just write it out from scratch:
That works fine but I already implemented two functions
lessThanTen that have the filtering logic implemented so I want to re-use them. This means I want to make a new filter function that passes both predicates, one way to do that would be as follows:
Ok, this is not too bad, let the input go through the first function, if it passes, let it go through the second function and if that passes too, return the result. Short-circuiting in between (returning None) when one of the predicates fails.
Now try to think about what we did for a second, we were able to turn two filtering functions into a new function that combines their logic without knowing how they are implemented. The only criteria we needed is the fact that filtering functions have type:
int -> int option and what is better, the newly created function
greaterThanFiveAndLessThanTen also has the same type
int -> int option so we use it and combine it with other functions.
We can abstract the logic of combining two filtering functions:
Now we will rewrite and simplify
greaterThanFiveAndLessThanTen using combine:
Because the result of
combine is a filter function too, the result also becomes “combinable” or you could call it “chainable”, you will often hear the word “combinator”, these all refer to the kind of functions that you can combine in different ways to create new functions:
combinedFilter is still just a function and you can use it as such:
combinedFilters 10 // => None
combinedFilters 8 // => Some 8
We can also make use of fact that you can define custom operators in F# and use a funny operator for
combine then we can simplify
Does it look familiar? This is starting to look a lot like the Suave DSL, we will come to that soon.
However, there is something I am not happy with: the functions
lessThanTen have hardcoded numbers in their predicates, we can extract those as parameters:
Much better! You can now write more combinators from existing ones
We can also implement a filter made out of a list of filters that tries every filter and stops at the first one that matches the predicate: of course I am talking about the
Given a list of filters, try to find the first filter that matches and return the result of that filter, if no filter matched, then return None.
Now you can write bigger filtering functions:
Who would have thought that writing web apps is just like filtering integers ;)
As we have seen, filter functions only check their input and propagate the input unchanged to next filter function if it passes the predicate. We can now turn to the second category of functions which is the functions that convert and manipulate their input, because we still want them to be combinable, we want them to have the type
int -> int option . Let us see some examples:
We can use these “transformers” with the other filtering functions to make larger functions that do filtering and converting:
So far we have been using integers and functions of type
int -> int option to create larger functions but this doesn’t have to be the case. Nothing is stopping us from writing similar functions for strings or other types as we will see next, but first we have to generalize
choose to work with any type
‘t -> ‘t option instead of
int -> int option :
No change to the code was needed, I just made the types generic. Now let us play with strings for a bit:
Back to Suave
We have seen how to work with functions of type
't -> 't option now we want to apply this knowledge to building a mini Suave DSL. In the beginning we talked about how a web server can be thought of as function
type WebServer = HttpRequest -> HttpResponse
How does that fit in with our type of
‘t -> ‘t option ? Well, we can put both request and response in a single type that we will call
HttpContext so that our server type can become:
type WebServer = HttpContext -> HttpContext option
Now let’s make mock types as if we were building Suave ourselves:
We can now start building our small filter and transformations functions using the principles we have learned so far:
path do the filtering, the functions
OK convert or write to the response of the
HttpContext . Because all of these “parts” / “combinators” have the type
HttpContext -> HttpContext option you can combine them like we did before:
This is the exact same code snippet from the Suave docs, but now implemented with our own homemade filtering functions.
Keep in mind that Suave is a full-fledged web server that implements these functions much more efficiently than I am doing here, also, Suave doesn’t exactly uses
HttpContext -> HttpContext option but a similar definition with the type:
HttpContext -> Async<HttpContext option>
Which is almost the same, except filtering the request and writing to the response happen asynchronously, making Suave, besides an enjoyable web framework, also a very scalable web server.
There you have it, an explanation on how the Suave DSL works, I hope you learned something new today and that wasn’t all too boring to go through. Let me know what you think and whether you would like me to write more about other concepts from programming and web development.