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.

Zurück
Zurück

Introduction to Web Programming in F# with Giraffe – Part 3

Weiter
Weiter

Innovation Incubator Round 1