Fun with Charts and the REPL
PUBLIC SERVICE ANNOUNCEMENT: This post uses WinForms to display charts. It’s fine, but might pose some cross platform issues. You might like to consider XPlot as an alternative.
Apart from immutable by default, expressions instead of statements, easier parallel code, less noise, higher order functions, expressive type systems and borderline magic type inference what has functional programming ever done for us?
My new favourite part of the whole story is the REPL, and the freedom it (and the features above) gives us to experiment with code.
Power up an FSharp script file and use NuGet to get FSharp.Charting and FSharp.Collections.ParallelSeq. Once you’ve installed the packages, load them in the script file, and open the necessary namespaces.
#load "packages/FSharp.Charting.0.90.10/FSharp.Charting.fsx" #r "packages/FSharp.Collections.ParallelSeq.1.0.2/lib/net40/FSharp.Collections.ParallelSeq.dll" open System open System.IO open FSharp.Charting open FSharp.Collections.ParallelSeq
Now we’re ready to go. We’re going to throw various numbers of dice and then chart the results. Let’s focus first on throwing one dice.
let random = System.Random() let randomDice = Seq.initInfinite(fun _ -> random.Next(1,7))
This is an infinite sequence of individual dice rolls. For Monopoly we’d roll two dice at a time, for Yahtzee we’d roll 5. So our next job is to roll a number of dice at the same time. We can use Seq.take for this.
let throwAtATime n = Seq.initInfinite(fun _ -> (Seq.take n randomDice) |> Array.ofSeq)
We’ll be adding the ability to save dice rolls to file and load them again, so here are a few helper functions that’ll make things a little easier later.
let NumsToText (nums: int) = nums |> Array.map (fun d -> d.ToString()) let Join (separator: string) (array: string) = String.Join(separator, array) let TextToNums (text: string) = text.Split(',') |> Array.map Int32.Parse
NumsToText converts the throws (integers) into an array of strings. Join wraps the String.Join function to allow us to partially apply it (and use it with pipelining). TextToNums splits a comma separated string of throws (read from a file) and converts it back into an array of ints. I’ve also wrapped the File.WriteAllLines and File.ReadLines so that we can use them in pipelined code.
let WriteToFile file lines = File.WriteAllLines(file, lines) let ReadFromFile file = File.ReadLines(file) |> Seq.map TextToNums
Whether we generate throws randomly or load them from a file, we have them in the same format, a Sequence of Arrays of integers. Lets write a function that takes that data structure and plots it as a chart. Here’s a function to plot the throws using FSharp Charting. Note that we use a ParallelSeq to speed things up a little.
// Plot To Screen let Plot (throws: seq<int>) = throws |> PSeq.map Array.sum |> PSeq.countBy id |> PSeq.sortBy fst |> Chart.Line
We sum the dice in a given throw. Then we group by the total, sort by total and Chart the results.
As expected the result is a bell-curve, although this little experiment shows how easy it is to visualise data in situations where you might not necessarily know the pattern in advance. We’ll see some more charting shortly, but lets quickly look at how the same data structure can be persisted to a file.
// Save To File let Save file (throws: seq<int>): Unit = throws |> Seq.map NumsToText |> Seq.map (Join ",") |> Seq.toArray |> WriteToFile file
And that’s it, we now have a suite of function that will allow us to do some cool things with very little effort. I love being able to use a simple data structure and a few helper functions and then use the REPL to “play” with the data. It allows us to answer questions about the data more quickly, but more importantly than that, the experimentation suggests questions we might not have otherwise thought to ask. Here are a few examples of getting stuff done in the REPL in just a few lines of code.
//Generate 1000000 random throws of 6 dice and save to file throwAtATime 6 |> Seq.take 1000000 |> Save @"C:\Temp\dice.txt" // Generate 1000000 random throws of 6 dice and plot a graph throwAtATime6 |> Seq.take 1000000 |> Plot // Read From file and plot a graph ReadFromFile @"C:\Temp\dice.txt" |> Plot // Read From file and save to another file (No, I don't know why you would either). ReadFromFile @"C:\Temp\dice.txt" |> Save @"C:\Temp\dice2.txt"
As you play with data, questions come to mind. Like, can we visually see what more dice does to the bell curve? Of course we can.
let threeDice = throwAtATime3 |> Seq.take 100000 let tenDice = throwAtATime10 |> Seq.take 100000 Chart.Combine [Plot threeDice; Plot tenDice]
Are you pondering what I’m pondering? Could we just use Seq.map to generate a whole slew of samples and plot them all together? Let’s see.
// Generate samples for 1 to 10 dice and plot them all [1..10] |> Seq.map (fun n -> throwAtATime |> Seq.take 1000000) |> Seq.map Plot |> Chart.Combine
Here we’ve seen a simple use of FSharp Charting. We’ve covered reading from and writing to a file. We’ve also seen how a couple of helper functions and paying attention to function signatues allow for some very readable code.