United Kingdom: +44 (0)208 088 8978

Leveraging FSharp.Plus for generic functions

Matt shows how you can use F#+ to avoid boilerplate.

We're hiring Software Developers

Click here to find out more

The .NET standard library has a common pattern of having static TryParse methods on types that take in a string and pointer. If the string can be parsed as the appropriate type, the appropriate value is written to the pointer and true is returned. Otherwise, false is returned. One such function is System.DateTime.TryParse(s: string, result: byref<System.DateTime>) : bool. F# makes working with functions using byref parameters a bit nicer by allowing you to treat the function as though its signature were string -> bool * System.DateTime, which makes code like this possible:

match System.DateTime.TryParse "2023-11-10" with
| true, dt -> printfn $"UK representation is {dt:``dd/MM/yyyy``}, but in the US it's {dt:``MM/dd/yyyy``}"
| false, _ -> printfn "Couldn't parse date"

However, we can further improve developer experience by returning options rather than booleans. In our codebases, this often leads to writing lots of tryParse functions wrapping the corresponding TryParse methods in the standard library. For example

module Int =
    let tryParse (s: string) =
        match System.Int32.TryParse(s) with
        | true, i -> Some i
        | false, _ -> None

which can be used as follows

Int.tryParse "one" |> Option.map (fun i -> i + 1)  // => None
Int.tryParse "1" |> Option.map (fun i -> i + 1)    // => Some 2

If you read Prash's blog post on static duck typing, this might have got you wondering whether you can use statically-resolved type parameters to avoid all the wrapper functions. You can! And some other people have already thought about this.

tryParse from F#+

The creators of F#+ ("F# plus") have put together a library that

builds upon FSharp, using generic programming techniques to help you avoid boiler plate code.

It's available as a NuGet package.

One example of how they aim to cut boilerplate is their generic tryParse function, which does the above conversion from of static TryParse methods in a generic way. Because it's generic, you can use it for many different types:

open FSharpPlus

tryParse "one" |> Option.map (fun i -> i + 1)  // => None
tryParse "1" |> Option.map (fun i -> i + 1)    // => Some 2

let x: bool option = tryParse "truthy"  // => None
let y: bool option = tryParse "true"    // => Some true

tryParse "2023-10-11" |> Option.map (fun d -> d > System.DateTime(2023, 01, 01))
// => Some true

tryParse and non-standard types

The type of tryParse is val tryParse: value: string -> 'T option (requires member TryParse). As you can see, all that it needs is a static method called TryParse on the type you are trying to return. This means that you can use tryParse with your own types so long as you add an appropriate static method:

open FSharpPlus

type MyRecord =
    {
        X: int
    }
    static member TryParse s =
        if String.startsWith "X = " s then
            s.Substring(4)
            |> tryParse
            |> Option.map (fun i -> { X = i })
        else
            None

match tryParse "X = 100" with
| Some r -> printfn $"Matched! X is %i{r.X}"
| None -> printfn "Failed to match!"

In the above code you can see another function that F#+ adds: String.startsWith.

Other generic functions

F#+ adds plenty of other generic functions. You can take a look at the documentation if you want to see them all, but I'll show the generic map function:

map System.Char.ToUpper "hello"     // => "HELLO"
map (fun x -> x + 1) [ 1; 2; 3 ]    // => "[ 2; 3; 4 ]"
map (fun x -> x + 1) [| 1; 2; 3 |]  // => "[| 2; 3; 4 |]"

F#+ does plenty more

Other features that F#+ offers include extensions to standard types, generic operators, computation expressions for abstract types and lenses. The documentation has plenty of examples.

A word of warning

As Prash mentioned is his post, heavy use of statically-resolved type parameters can significantly increase compilation time, so they should be used with caution. In addition, I don't think writing a few non-generic tryParse functions is a big deal. That said, it allows you to work at a higher level of abstraction, and definitely goes some way to helping you achieve the goal of minimising boilerplate code, so it might be something that you want to check out!

As always, thanks to the contributors for making an awesome F# library!