If you’re interested in learning about using railway-oriented programming in F#, you should be reading Railway oriented programming at fsharpforfunandprofit.com and The Marvels of Monads at microsoft.com. Stylish F# by Kit Eason has also been a help.
Functional programming is about functional composition and pipelineing functions from one to the next to get a result. Railway-oriented programming is about changing that pipeline to a track where if an operation succeeds, it goes forwards and if it fails, it cuts over to a failure track. F# already has built-in the Result object, a discriminated union giving success (Ok) or failure (Error) and the monadic functions, bind, map, and mapError.
I was interested in how these could be applied to a WebAPI endpoint. Let’s say we’re passing these results along a pipeline. What’s failure? In the end, it will be an IActionResult of some kind, probably a StatusCodeResult. 404, 401, 500, whatever. In the end, that’s an IActionResult, too, though with a status code of 200.
There’s an F#, functional web project that already does something like this, Suave.io, though it doesn’t look to be maintained anymore. Even so, they have some good, async implementations of the various railway/monadic functions like bind, compose, etc. I’ve tried to adapt them in to my own AsyncResult module.
Result
The result object is unchanged:
type Result<'TSuccess,'TFailure> =
| Ok of 'TSuccess
| Error of 'TFailure
Bind
First, bind, which takes some Result input and a function and, if the input is Ok, calls the function with the contents, and, if the input is Error, short-cuts and returns the error.
An async bind looks like this:
let bind f x = async {
let! x' = x
match x' with
| Error e -> return Error e
| Ok x'' -> return! f x''
}
Map
Next we have map. Say we have a function that takes some object and manipulates it returning another object. Map lets us insert that into our railway, with the function operating on the contents of the Ok result.
An async map looks like this:
let map f x = async {
let! x' = x
match x' with
| Error e -> return Error e
| Ok x'' ->
let! r = f x''
return Ok( r )
MapError
MapError is like map but instead we expect the function to operate on the Error result.
This is my async mapError:
let mapError f x = async {
let! x' = x
match x' with
| Error e ->
let! r = f e
return Error( r )
| Ok ok ->
return Ok ok
}
Compose
Next we have compose, which lets us pipe two bound functions together. If the first function returns an Ok(x) as output, the second function takes the x as input and returns some Result. If the first function returns an Error, the second is never called.
This is the async compose:
let compose f1 f2 =
fun x -> bind f2 (f1 x)
Custom Operators
We can create a few custom operators for our functions:
// bind operator
let (>>=) a b =
bind b a
// compose operator
let (>=>) a b =
compose a b
An Example WebAPI Controller
Let’s imagine a WebAPI controller endpoint that implements GET /thing/{id} where we return some Thing with the given ID. Normally we would:
* Check that the user has permission to get the thing.
* Get the thing from the database.
* Format it into JSON.
* Return it.
If the user doesn’t have permissions, we should get a 401 Unauthorized. If the Thing with the given ID isn’t found, we should get a 404 Not Found.
The functions making up our railway
Usually we want a connection to the database but I’m just going to fake it for this example:
let openConnection(): Async<IDbConnection> =
async {
return null
}
We might also have a function that, given the identity in the HttpContext and a database connection could fetch the user’s roles. Again we’ll fake it. For testing purposes, we’ll say the user is an admin unless the thing ID ends in 99
let getRole ( connection : IDbConnection ) ( context : HttpContext ) =
async {
if context.Request.Path.Value.EndsWith("99") then return Ok "user"
else return Ok "admin"
}
Now we come to our first railway component. We want to check the user has the given role. If he does, we return Ok, if not an Error with the 401 Unauthorized code (not yet a StatusCodeResult)
let ensureUserHasRole requiredRole userRole =
async {
if userRole = requiredRole then return Ok()
else return Error( HttpStatusCode.Unauthorized )
}
Next we have a railway component that fetches the thing by ID. For testing purposes, we’ll say that if the ID is 0 we’ll return an Option.None and otherwise return an Option.Some. Although I haven’t added it here, I could imagine adding a try/catch that returns an Error 500 Internal Server Error when an exception is caught.
let fetchThingById (connection: IDbConnection) (thingId: int) () =
async {
match thingId with
| 0 ->
// Pretend we couldn't find it.
return Ok( None )
| _ ->
// Pretend we got this from the DB
return Ok( Some ( { Id = thingId; Name = "test" } ) )
}
Our next railway component checks that a given object is found. If it’s Some, it returns Ok with the result. If it’s None, we get an Error, 404 Not Found.
let ensureFound ( value : 'a option ) = async {
match value with
| Some value' -> return Ok( value' )
| None -> return Error( HttpStatusCode.NotFound )
}
Next we’ll create a function that just converts a value to a JSON result (maybe pretending there might be more complicated formatting going on here):
let toJsonResult ( value : 'a ) =
async {
return ( JsonResult( value ):> IActionResult )
}
Finally, we’ll add a function to convert that HttpStatusCode to a StatusCodeResult (also overkill – we could probably inline it):
let statusCodeToErrorResult ( code : HttpStatusCode ) = async {
return ( StatusCodeResult( (int)code ) :> IActionResult )
}
When we end up, we’re going to have an Ok result of type IActionResult and an Error, also of type IActionResult. I want to coalesce the two into whatever the result is, regardless of whether it’s Ok or Error:
// If Error and OK are of the same type, returns the enclosed value.
let coalesce r = async {
let! r' = r
match r' with
| Error e -> return e
| Ok ok -> return ok
}
Putting it together
Here’s our railway in action:
// GET /thing/{thingId}
let getThing (thingId: int) (context: HttpContext): Async<IActionResult> =
async {
// Create a DB connection
let! connection = openConnection()
// Get the result
let! result =
// Starting with the context...
context |> (
// Get the user's role
( getRole connection )
// Ensure the user is an admin.
>=> ( ensureUserHasRole "admin" )
// Fetch the thing by ID
>=> ( fetchThingById connection thingId )
// Ensure if was found
>=> ensureFound
// Convert it to JSON
>> ( map toJsonResult )
// Map the error HttpStatusCode to an error StatusCodeResult
>> ( mapError statusCodeToErrorResult )
// Coalese the OK and Error into one IAction result
>> coalesce
)
// Return the result
return result
}
To summarize, we
* Get the user’s role, resulting in an Ok with the role (and no Error, though I could imagine catching an exception and returning a 500).
* See if the user has the role we need resulting in an Ok with no content or an Error(401).
* Fetch a Thing from the database, resulting in an Object.Some or Object.None.
* Check that it’s not None, returning an Error(404) if it is or an Ok(Thing).
* Mapping the Ok(Thing) into a Thing and turning the Thing into a JsonResult.
* or mapping the Error(HttpStatusCode) into a HttpStatusCode and turning the Error(HttpStatusCode) into a StatusCodeResult.
* Taking whichever result we ended up with, the JsonResult or StatusCodeResult and returning it.
If we run the website and call https://localhost:5001/thing/1
we get the JSON for our Thing.
{"id":1,"name":"test"}
If we call /thing/0
we get 404 Not Found. If we call thing/99
we get 401 Unauthorized.
There’s room here for some other methods. I could imagine wanting to wrap a call in a try/catch and return a 500 Server Error if it fails, for example.
The best part is that it’s a nice, readable, railway of functions. And our custom operators make it look good.
The code for this post can be found on GitHub.