United Kingdom: +44 (0)208 088 8978

Incremental development with FSI

Matt takes a look at how F# interactive can be used to develop code incrementally.

We're hiring Software Developers

Click here to find out more

Developers sometimes talk about developing within a REPL, but what does that entail and why might you want to do that? I'll explore that topic in this post.

A quick primer on FSI

Briefly, a REPL is a "read-eval-print loop". It's a program which can read code given to it, evaluate the expressions, print out the results, and repeat (loop). The REPL for F# is F# interactive (often abbreviated to FSI). F# interactive keeps named values in its environment that can be accessed later. This allows you to, for example, bind some values, then later use those values as arguments to previously-bound function.

You can read more about it in the F# documentation.

Incremental development

One useful thing you can do with FSI is build up a function in increments, testing each increment before working on the next and adding increments until the function is complete. This is particularly well-suited to pure calculation functions with some complexity.

Worked example

This idea is best illustrated with an example.

Problem description

Let's say that we have a collection of products whose prices we know for a previous year (the year might be different for each product). We know the year-on-year inflation that applied to this category of products in the past, and we have some inflation predictions given by experts for future years.

Given this information, we can calculate the price for the products in a given future year as follows:

  • For each product, consider the years after the year that we know the price for up to and including the target year.
  • Successively inflate the price by the relevant amount for each year, determined as follows.
  • For years where the inflation is known, simply use that inflation.
  • For years where the inflation is predicted, use the average of the predicted inflations as the assumed inflation.
  • To keep things simple, we will assume that we always have either historical or predicted inflation for every year between the year for which we know the product price and the target year.

Increments

We can build up a solution in increments. First we define some types and scaffold the function, so that we know where we're trying to get to:

type YearOnYearInflation = {
    Year: int
    Inflation: decimal
}

type Product = {
    Name: string
    Year: int
    Price: decimal
}

let inflatePrices historicalInflations inflationPredictions products targetYear =
    ()

Next we call the function with some sample data, including some logging so that we can check our code.

let historicalInflations = [
    { Year = 2021; Inflation = 2m }
    { Year = 2022; Inflation = 3m }
    { Year = 2023; Inflation = 5m }
]

let inflationMultiplierLookup =
    historicalInflations
    |> Seq.map (fun (i: YearOnYearInflation) -> i.Year, 1m + i.Inflation / 100m)
    |> dict

printfn $"%A{inflationMultiplierLookup}"

After evaluating the above code in FSI, the following is printed:

seq [[2021, 1.02]; [2022, 1.03]; [2023, 1.05]]

All looks good so far!

A reasonable next step is to actually inflate the prices of products using the lookup.

let products = [
    { Name = "Widget"; Year = 2020; Price = 10.00m }
]

let targetYear = 2023

let inflatedProducts =
    products
    |> Seq.map (fun product ->
        [ product.Year + 1 .. targetYear ]
        |> List.fold (fun product nextYear ->
            let nextPrice = product.Price * inflationMultiplierLookup[nextYear]
            { product with Year = nextYear; Price = nextPrice }) product)
    |> Seq.toArray

After comparing the expression that FSI prints to reusults manually derived using a calculator, we see that things seem to be working correctly 💪

val inflatedProducts: Product[] = [|{ Name = "Widget"
                                      Year = 2023
                                      Price = 11.03130000M }|]

We should probably check that things work properly with multiple products with prices in different years.

let products = [
    { Name = "Widget"; Year = 2020; Price = 10.00m }
    { Name = "Gizmo"; Year = 2021; Price = 5.00m }
]

After sending the updated products to FSI, and re-evaluating inflatedProducts, we see that they do:

val inflatedProducts: Product[] =
  [|{ Name = "Widget"
      Year = 2023
      Price = 11.03130000M }; { Name = "Gizmo"
                                Year = 2023
                                Price = 5.407500M }|]

We haven't yet implemented the prediction logic, so let's tackle that next. First, let's calculate some yearly averages:

let inflationPredictions = [
    { Year = 2024; Inflation = -1m }
    { Year = 2025; Inflation = 2m }
    { Year = 2024; Inflation = 0m }
    { Year = 2025; Inflation = 1m }
    { Year = 2024; Inflation = -2m }
]

let predictionAverages =
    inflationPredictions
    |> Seq.groupBy (fun (p: YearOnYearInflation) -> p.Year)
    |> Seq.map (fun (year, inflations) -> {
        Year = year
        Inflation = inflations |> Seq.averageBy (fun i -> i.Inflation)
    })
    |> Seq.toArray
val predictionAverages: YearOnYearInflation[] =
  [|{ Year = 2024
      Inflation = -1M }; { Year = 2025
                           Inflation = 1.5M }|]

Finally, we need to modify inflationMultiplierLookup to use the averages too:

let inflationMultiplierLookup =
    Seq.append historicalInflations predictionAverages
    |> Seq.map (fun (i: YearOnYearInflation) -> i.Year, 1m + i.Inflation / 100m)
    |> dict

let targetYear = 2025

Again, after re-evaluating inflatedProducts after the above, we get confirmation that all looks good!

val inflatedProducts: Product[] =
  [|{ Name = "Widget"
      Year = 2025
      Price = 11.0848018050000M }; { Name = "Gizmo"
                                     Year = 2025
                                     Price = 5.43372637500M }|]

Using some sample data, we've built up all the pieces that we need to implement the function. The last thing to do is to put them into the right place (and strip off the |> Seq.toArray lines that were only there to get FSI to show the results):

let inflatePrices historicalInflations inflationPredictions products targetYear =
    let predictionAverages =
        inflationPredictions
        |> Seq.groupBy (fun (p: YearOnYearInflation) -> p.Year)
        |> Seq.map (fun (year, inflations) -> {
            Year = year
            Inflation = inflations |> Seq.averageBy (fun i -> i.Inflation)
        })

    let inflationMultiplierLookup =
        Seq.append historicalInflations predictionAverages
        |> Seq.map (fun (i: YearOnYearInflation) -> i.Year, 1m + i.Inflation / 100m)
        |> dict

    products
    |> Seq.map (fun product ->
        [ product.Year + 1 .. targetYear ]
        |> List.fold (fun product nextYear ->
            let nextPrice = product.Price * inflationMultiplierLookup[nextYear]
            { product with Year = nextYear; Price = nextPrice }) product)

Benefits of incremental development

There are some benefits with this approach compared with testing everything at the end:

  • There's no need to launch an application to test your code.
  • We gain some confidence that the foundations are solid before building more code on top.
  • Problems are surfaced quickly, and it's easy to pinpoint which step is wrong if you're confident in all of the rest.

Summary

To sum up:

  • F# interactive is a REPL for F#, allowing you to evaluate small bits of F# code and see the results.
  • You can use it to build up functionality incrementally.
  • Doing so allows you to spot problems quickly, and gives you confidence that the code you've written is sound.

Consider making use of the REPL when writing non-trivial calculation functions in F#. It can be a great way to work!