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))
optionsYou 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> defaultOptionsIf 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> defaultOptionsHaving 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.