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!