How to Write Simple, Clear F# Option Pipelines with Option.orElseWith
Date: 2024-06-05 | create | tech | fsharp |
Last week I shared how to get the IP Address of a web request in F# / Giraffe. It involves checking various pieces of state on the request to find one that is actually filled. This fits nicely into an option pattern as the value is either Some or None.
I got some feedback that there were more succinct patterns for pipelining optional values like these. So in this post we'll explore Option.orElseWith
and how it can make option pipelines clearer in F#.
How Option.orElseWith Works
The beauty of pipelining (sometimes called railroading) is that we can write data flows simply and succinctly. Data starts at the top, flows through our pipeline, and we get the resulting value back at the end.
While this ultimately can lead to more succinct code than standard imperative operations, this is a very different paradigm and thus can be confusing at first. For example a common point of confusion is how to implement early returns in pipelined code - in imperative code we would simply say if condition return value
but pipelines don't quite work that way.
Option.orElseWith
is a method for building such a pipeline for option values. It essentially is a match statement that says:
- If value is Some -> return that Some value
- if value is None -> run the provided function to get a new value
Because it can be created in the form of option -> option
we can pipeline this together making our code more succinct than writing that match statement ourselves at each stage of the pipeline.
Example: (available on Replit)
let value =
None
|> Option.orElseWith (
fun () -> Some 123 // This gets piped through!
)
|> Option.orElseWith (
fun () -> None
)
Improving Option Pipelines with Option.orElseWith
The above example is pretty simple so I wanted to show some real code refactored with it to show how it can make real option pipelines much simpler and clearer.
The following code attempts to get an IP Address for a web request in F# / Giraffe. To do so it needs to check several places where the value may or may not be set. This code is using a few patterns:
- null -> option
- Simple if/else early returns (no monads!)
Code:
let getRequestIpAddress
(ctx: HttpContext)
: string option
=
let httpForwardedForRaw = ctx.Request.HttpContext.GetServerVariable("HTTP_X_FORWARDED_FOR")
let httpForwardedFor =
match httpForwardedForRaw with
| v when isNull v -> None
| v -> Some v
if Option.isSome httpForwardedFor
then httpForwardedFor
else
let remoteAddressRaw = ctx.Request.HttpContext.GetServerVariable("REMOTE_ADDR")
let remoteAddress =
match remoteAddressRaw with
| v when isNull v -> None
| v -> Some v
if Option.isSome remoteAddress
then remoteAddress
else
let remoteIpRaw = ctx.Connection.RemoteIpAddress
let remoteIp =
match remoteIpRaw with
| v when isNull v -> None
| v -> Some (v.ToString())
remoteIp
This code works but it's pretty verbose. We're doing the null -> option
match statement and early returns multiple times and each is a bit wordy.
This new code makes the options pipeline more succinct by utilizing some standard library helpers for options operations:
- Option.orElseWith - Explained in this post
- Option.ofObj - a null -> option helper
Shoutout to Peter and Ian on Twitter for the suggestions!
New Code:
let getRequestIpAddress
(ctx: HttpContext)
: string option
=
ctx.Request.HttpContext.GetServerVariable("HTTP_X_FORWARDED_FOR")
|> Option.ofObj
|> Option.orElseWith (
fun () ->
ctx.Request.HttpContext.GetServerVariable("REMOTE_ADDR")
|> Option.ofObj
)
|> Option.orElseWith (
fun () ->
ctx.Connection.RemoteIpAddress
|> Option.ofObj
|> Option.map (fun v -> v.ToString())
)
Lines of code are not a very good proxy for code quality. But here I think it's fair to say the code is at least as readable as the old code with much less tedious logic.
Next
I've been building web apps with F# for the past few years now and I'm still stumbling on new ways to do things. If anything it makes me appreciate the language more for all the nice things that are built-in.
Q: What's your favorite way to pipeline / railroad in F#?
If you liked this post you might also like:
Want more like this?
The best / easiest way to support my work is by subscribing for future updates and sharing with your network.