5

Function Composition in F# with Unfriendly Functions

 3 years ago
source link: https://www.softwarepark.cc/blog/2021/2/19/function-composition-in-f-with-unfriendly-functions
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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": "[email protected]",
            "Age": 24
        },
        {
            "Name": "Doris",
            "Email": "[email protected]",
            "Age": 31
        },
        {
            "Name": "John",
            "Email": "[email protected]",
            "Age": 48
        },
        {
            "Name": "Clarice",
            "Email": "[email protected]",
            "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.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK