Writing efficient and reliable code with F# Type Providers

F# type providers are just awesome because they help to write very efficient and reliable code.

In this post I will show this by implementing a simple, but real-world-like scenario with some F# type providers.

Type providers provide the types, properties and methods to get access to external data sources of various kinds without having to write a lot of boiler-plate code. This makes coding very efficient.

Additionally they offer static types that represent external data and that the compiler will check at compile time. This makes coding very reliable.

So let’s look at the scenario that we are going to implement…

Scenario

We are going to implement a command line tool with the following features:

  • Import email addresses and names from a CSV file into a MS SQL database
  • Delete all emails from the database

The user should provide the CSV file path as a command line argument.

The user should specify the connection string in the application’s configuration file.

Implementation

Main types

We will begin with creating a new F# console application project with Visual Studio and then by adding the following types:

type Email = Email of string

type Name = Name of string

type MailingListEntry = {
    Email:Email
    Name:Name }

Accessing the Database

For database access we will use the SqlDataConnection type provider, which is a native F# feature.

We have to add references to FSharp.Data.TypeProviders, as well as System.Data, and System.Data.Linq.

Now we can set-up the type provider with the connection string of the database, which we will substitute later with the connection string from the application’s configuration file.

module DataAccess =
    open Microsoft.FSharp.Data.TypeProviders

    [<Literal>]
    let ConnectionString = @"Data Source=(LocalDb)\V11.0;Initial Catalog=MailingListDb;Integrated Security=True"
    type DbSchema = SqlDataConnection<ConnectionString>

When writing the mapper from the domain type to the type that represents the table, we get strong types and autocompletion:

alt text

Here is the code for inserting a list of MailingListEntry and for deleting all rows from the table:

let insert cs list =
    let db = DbSchema.GetDataContext(cs)
    list
    |> List.map (fun e -> toDto e.Email e.Name)
    |> db.MailingList.InsertAllOnSubmit
    |> db.DataContext.SubmitChanges

let delete cs =
    let db = DbSchema.GetDataContext(cs)
    db.MailingList
    |> db.MailingList.DeleteAllOnSubmit
    |> db.DataContext.SubmitChanges

This code is also strongly typed and we get IntelliSense support:

alt text

What if the database schema changes…

If the database schema changes and our database related code is not updated, our program will not compile.

This is really great because we don’t have to think or care about database and code getting out of sync anymore. The compiler will just point it out to us if there has been a schema change.

This way we can always have very high confidence in our data access code. The code becomes very reliable.

E.g. even if we only change the name of a column:

alt text

… we get a compilation error:

alt text

Reading from a CSV file

To read data from the CSV file we will use the CSV Type Provider.

For installation via NuGet we run the following command in the Package Manager Console:

Install-Package FSharp.Data -Version 2.2.5

This is the format of the CSV file:

email, name
john.doe@example.com, John Doe
jane.doe@example.com, Jane Doe

Here is the complete code for reading the email addresses and names from the file and again we get nice IDE support:

alt text

Reliability because of type inference

Again we can have very high confidence that our CSV data access code works because the TP obtains the type names from the header (first row) and infers the types from the values (subsequent rows).

If the structure looked like this e.g.:

email, name
3.14, John Doe
2.72, Jane Doe

… the type of Email would be inferred to be decimal and we would get the following error:

alt text

Parsing command line arguments

Now we have to parse the command line arguments to specify the correct command (Import or Delete) as well as the path to the CSV file.

Argu is not a real type provider, but it has some similarities. Mainly it makes parsing CLI arguments really easy and it transforms the provided arguments into a strongly typed list that we can pattern match against.

Installation:

Install-Package Argu

This is how we can define the arguments for our program:

module Arguments =
    open Argu

    type CliArguments =
        | Import of fileName:string
        | Delete
    with 
        interface IArgParserTemplate with
            member s.Usage = match s with Import _ -> "import csv data" | Delete -> "delete all entries"

With this setup the following command line inputs will be parsed correctly:

.\MaillingList.exe --import ..\..\mailinglist.csv
.\MaillingList.exe --delete

Now the function getCmds converts the input and returns a list of type CliArgument:

let getCmds args =
    let parser = ArgumentParser.Create<CliArguments>()
    let results = parser.Parse args
    results.GetAllResults()

Reading from the application’s configuration file

The last type provider that we will use is FSharp.Configuration.

Installation:

Install-Package FSharp.Configuration

After we set it up with the name of the application’s configuration file name again we get autocompletion when retrieving the connection string:

alt text

Again if the connection string is missing in the configuration file, the application won’t compile.

Putting it all together

Now we can put everything together.

First we inject the connection string by partially applying the database access functions to the connection string:

type Settings = AppSettings<"app.config">
let private import list = DataAccess.insert Settings.ConnectionStrings.MyMailingListDb list
let private delete() = DataAccess.delete Settings.ConnectionStrings.MyMailingListDb

Then we write a function that handles a single command of type CliArgument:

let private handle cmd =
    match cmd with
    | Import fileName -> readFromCsvFile fileName |> import
    | Delete          -> delete()

Finally we need a function run that retrieves the list of commands and handles each of them:

let private run args =
    getCmds args
    |> List.iter handle

[<EntryPoint>]
let main argv = 
    run argv
    0    

Then we can call run from the main method.

Here is the complete program in less than 80 lines of code:

type Email = Email of string

type Name = Name of string

type MailingListEntry = {
    Email:Email
    Name:Name }

module DataAccess =
    open Microsoft.FSharp.Data.TypeProviders

    [<Literal>]
    let ConnectionString = @"Data Source=(LocalDb)\V11.0;Initial Catalog=MailingListDb;Integrated Security=True"
    type DbSchema = SqlDataConnection<ConnectionString>

    let private toDto (Email email) (Name name) =
        DbSchema.ServiceTypes.MailingList(Email = email, Name = name)

    let insert cs list =
        let db = DbSchema.GetDataContext(cs)
        list
        |> List.map (fun e -> toDto e.Email e.Name)
        |> db.MailingList.InsertAllOnSubmit
        |> db.DataContext.SubmitChanges

    let delete cs =
        let db = DbSchema.GetDataContext(cs)
        db.MailingList
        |> db.MailingList.DeleteAllOnSubmit
        |> db.DataContext.SubmitChanges

module CsvAccess =
    open FSharp.Data

    type MailingListData = CsvProvider<"mailinglist.csv">

    let readFromCsvFile (fileName:string) = 
        let data = MailingListData.Load(fileName)
        [for row in data.Rows do
            yield { Email = Email row.Email; Name = Name row.Name}]

module Arguments =
    open Argu

    type CliArguments =
        | Import of fileName:string
        | Delete
    with 
        interface IArgParserTemplate with
            member s.Usage = match s with Import _ -> "import csv data" | Delete -> "delete all entries"

    let getCmds args =
        let parser = ArgumentParser.Create<CliArguments>()
        let results = parser.Parse args
        results.GetAllResults()

module Program =
    open FSharp.Configuration
    open Arguments
    open CsvAccess

    // inject connection srtring
    type Settings = AppSettings<"app.config">
    let private import list = DataAccess.insert Settings.ConnectionStrings.MyMailingListDb list
    let private delete() = DataAccess.delete Settings.ConnectionStrings.MyMailingListDb

    let private handle cmd =
        match cmd with
        | Import fileName -> readFromCsvFile fileName |> import
        | Delete          -> delete()

    let private run args =
        getCmds args
        |> List.iter handle

    [<EntryPoint>]
    let main argv = 
        run argv
        0

Conclusion

We have implemented a simple console application for managing the import of email addresses from a CSV file into a database.

We have used the following type providers

and the library Argu.

The implementation with the help of those libraries is really easy and efficient because we don’t need to write a lot of boiler-plate code.

Also our code is very reliable because the external data is type checked at compile time.

Please note that the application will still crash at run-time if the external data sources do not match the expected structure. Adding run time error handling would be a topic for another article. But concerning reliability at compile time this is probably the best we can get.

The code is available on GitHub.

  • Thanks for the post. Very consice yet powerful introduction to TypeProviders.

    Do CSV files require the header line always or only in the example file used to guide TypeProvider?

    • Hi,

      if the CSV file has no header you can specify the HasHeaders property to false on initialization. Then you can access the columns by Column1 and Column2 like this:

      type MailingListData = CsvProvider<“mailinglist.csv”, HasHeaders = false>

      let readFromCsvFile (fileName:string) =
      let data = MailingListData.Load(fileName)
      [for row in data.Rows do
      yield { Email = Email row.Column1; Name = Name row.Column2}]

      Here: https://fsharp.github.io/FSharp.Data/library/CsvProvider.html you can find more information on the configuration of the CSV Type Provider.

      Sorry for the late reply. Somehow email notification didn’t seem to work.

  • Pingback: F# Weekly #13, 2016 | Sergey Tihon's Blog()