Function Composition in F# with Unfriendly Functions
Introduction
This is a short post looking at how to solve the problem of using F# unfriendly libraries in an F# function composition pipeline. One of my colleagues asked a question about this and I thought it might be interesting to look at some of the options available to us to solve it.
Setting Up
You can use any IDE but I will be using VSCode plus the wonderful ionide plugin.
Open VSCode in a new folder.
Open a new VSCode Terminal and create a new console app using:
dotnet new console -lang F#
In the VSCode Explorer mode, add a new folder called resources. Add a new file to the resources folder called employees.json.
Copy the following into the new file:
{ "Employees": [ { "Name": "Ted", "Email": "ted@nomail.com", "Age": 24 }, { "Name": "Doris", "Email": "doris@nomail.com", "Age": 31 }, { "Name": "John", "Email": "john@nomail.com", "Age": 48 }, { "Name": "Clarice", "Email": "clarice@nomail.com", "Age": 39 } ] }
Replace the code in program.fs with the following:
open System.IO open System.Text.Json open System.Text.Json.Serialization type Employee = { Name : string Email : string Age : int } type EmployeeList = { Employees: Employee array } // string -> EmployeeList let getEmployees path = path |> File.ReadAllText |> JsonSerializer.Deserialize<EmployeeList> [<EntryPoint>] let main argv = let message = getEmployees "resources/employees.json" printfn "%A" message 0 // return an integer exit code
Run the code by typing the following into the terminal and pressing Enter:
dotnet run
You should see some json in the terminal window.
Introducing the Problem
Whilst this works, what happens if I want to add some JsonSerializerOptions like this?
let defaultOptions = let options = JsonSerializerOptions() options.Converters.Add(JsonFSharpConverter(JsonUnionEncoding.NewtonsoftLike, allowNullFields = true)) options
You will need to download a nuget package:
dotnet add package FSharp.SystemTextJson
The Deserialize method has a version that takes a tuple of string and JsonSerializerOptions.
let getEmployees path = path |> File.ReadAllText |> JsonSerializer.Deserialize<EmployeeList>(?, defaultOptions)
This doesn't even compile. Even if the parameters were swapped, it still wouldn't work because we are dealing with a tuple rather than curried arguments. Luckily, this isn't a difficult thing to solve but there are a few ways that we could resolve it. We are going to look at three viable solutions.
Version 1 - Custom Wrapper Function
The general approach to solving these types of problems is a layer of indirection; In this case a wrapper function.
// JsonSerializerOptions -> string -> EmployeeList let deserialize<'T> options (data:string) = JsonSerializer.Deserialize<'T>(data, options)
We have wrapped our unfriendly method in a usable curried wrapper. Let's modify the getEmployees function to use this new function:
let getEmployees path = path |> File.ReadAllText |> deserialize<EmployeeList> defaultOptions
If you run it, you will see that it works as before.
We can take this even further by adding a partially applied version of the new wrapper function with the options already set, so that we only need to apply the last argument to make it run.
let deserializeWithDefaultOptions<'T> = deserialize<'T> defaultOptions
Again, we update the getEmployees function to use the new partially applied wrapper function:
let getEmployees path = path |> File.ReadAllText |> deserializeWithDefaultOptions<EmployeeList>
If we run this, we will still see the expected results.
Version 2 - Generic Higher Order Function
Another option is to create a generic, in both senses of the word, function that can handle all situations that require this specific functionality:
// ('a * 'b -> 'c) -> 'b -> 'a -> 'c let reverseTuple f x y = f(y, x)
This is the ultimate goal of pure functional programming: to find generic solutions to problems.
Now we can fit our new function into the pipeline where it will be ready to accept the last partially applied parameter through the pipeline:
let getEmployees path = path |> File.ReadAllText |> reverseTuple JsonSerializer.Deserialize<EmployeeList> defaultOptions
Having said that this is a good thing to do, it is nowhere nearly as readable at a glance as the previous solution. Purity does not always imply superiority.
Version 3 - Inline Function
If this is a one-off requirement, rather than creating additional partially applied functions, we could use an inline anonymous function instead:
let getEmployees path = path |> File.ReadAllText |> fun data -> (data, defaultOptions) |> JsonSerializer.Deserialize<EmployeeList>
This is a really simple and elegent solution and would probably be the first approach we should try. If we needed to use the same logic elsewhere, we would start to consider using a custom wrapper function instead.
Summary
In this post we have had a look at ways to solve the problem of fitting unfriendly functions into an F# composition pipeline. We haven't used anything that wasn't covered in my 12 part Introduction to Functional Programming in F# series.
If you have any comments on this post or suggestions for new ones, send me a tweet (@ijrussell) and let me know.