Create a console application in F# interactive

Microsoft F# Fsharp

In my previous posts (Using immutable data structures in F# and Using functions as values in F#) I discussed about some basic data structure in F#. Now, it is time to do something a bit more exciting based on what we know so far. You find the source code of this post and a lot more on my Github.

F# (or C#, Visual Basic or any other .NET language) is a thin layer on the .NET framework

The .NET platform contains many libraries and all of them can be used from F#. We’ll look at several examples, mainly in order to work with files and create the UI for our application. We’ll come across several other .NET libraries in the subsequent chapters, but after reading this one you’ll be able to use most of the functionality provided by .NET from your F# programs, because the technique is often the same.

We’ll develop an application for drawing pie charts. The application loads data from a comma-separated value (CSV) file and performs pre-processing in order to calculate the percentage of every item in the data source. Then it plots the chart and allows the user to save the chart as a bitmap file. We could use a library to display the chart, but by implementing the functionality ourselves, we’ll learn a lot about F# programming and using .NET libraries from F# code.

Running the F# application for drawing pie charts. The chart shows distribution of the world population among continents.

Writing and testing code in FSI

The application works with a series of elements containing a title to be displayed in the chart and a number. It will load the data from a simplified CSV file, which contains a single element per line.

Asia, 3634
Australia/Oceania, 30
Africa, 767
South America, 511
Europe, 729
North America, 307

This is the F# code:

Listing 1. Parsing a row from the CSV file (F# Interactive)
open System

let convertDataRow(csvLine:string) =
    let cells = List.ofSeq(csvLine.Split(','))
    match cells with
    | title::number::_ ->
        let parsedNumber = Int32.Parse(number)
        (title, parsedNumber)
    | _ -> failwith "Incorrect data format!"

After starting F# Interactive, we import functionality from the System namespace. We need to open the namespace because the code uses the Int32.Parse method. This has to be imported explicitly, whereas the functions from the core F# libraries, such as List.ofSeq, are available implicitly.

The function convertDataRow ❶ takes a string as an argument and splits it into a list of values using a comma as a separator. We’re using the standard.NET Split method to do this. When invoking an instance method on a value, the F# compiler needs to know the type of the value in advance. Unfortunately, the type inference doesn’t have any other way to infer the type in this case, so we need to use type annotation to explicitly state that the type of csvLine is a string ❷.

The Split method is declared using the C# params keyword and takes a variable number of characters as arguments. We specify only a single separator: the comma character. The result of this method is an array of strings, but we want to work with lists, so we convert the result to a list using the ofseq function from the F# List module.

Once we have the list, we use the match construct to test whether it’s in the correct format. If it contains two or more values, it will match the first case (title::number::_). The title will be assigned to a value title, the numeric value to number, and the remaining columns (if any) will be ignored. In this branch we use Int32.Parse to convert a string to an integer and return a tuple containing the title and the value. The second branch throws a standard .NET exception.

If you look at the signature, you can see that the function takes a string and returns a tuple containing a string as the first value and an integer as the second value. This is exactly what we expected: the title is returned as a string and the numeric value from the second column is converted to an integer. The next line demonstrates how easy it is to test the function using F# Interactive ❸. The result of our sample call is a tuple containing “Testing reading” as a title and “1234” as a numeric value.

As a next step we’ll implement a function that takes a list of strings and converts each string to a tuple using convertDataRow

Listing 2. Parsing multiple lines from the input file (F# Interactive)
let rec processLines(lines) =
    match lines with
    | [] -> []
    | currentLine::remaining ->
        let parsedLine = convertDataRow(currentLine)
        let parsedRest = processLines(remaining)
        parsedLine :: parsedRest

let testData = processLines["Test,123"; "Test,456"];;

As you can see, the function is declared using the let rec keyword, so it’s recursive. It takes a list of strings as an argument (lines) and uses pattern matching to test whether the list is an empty list or a cons cell. For an empty list, it directly returns an empty list of tuples ❶. If the pattern matching executes the branch for a cons cell ❷, it assigns a value of the first element from the list to the value currentLine and list containing the remaining elements to the value remaining.

The code for this branch first processes a single row using the convertDataRow function and then recursively processes the rest of the list. Finally the code constructs a new cons cell: it contains the processed row as a head and the recursively processed remainder of the list as a tail. This means that the function executes convertDataRow for each string in the list and collects the results into a new list.

To better understand what the processLines function does, we can also look at the type signature printed by F# Interactive. It says that the function takes a list of strings (string list type) as an argument and returns a list containing tuples of type string * int. This is exactly the type returned by the function that parses a row, so it seems that the function does the right thing. We verify this by calling it with a sample list as an argument ❸.

Calculating with the data

In the first version of the application, we’ll simply print labels together with the proportion of the chart occupied by each item (as a percentage).

To calculate the percentage, we need to know the sum of the numeric values of all the items in the list. This value is calculated by the function calculateSum

Listing 3. Calculating a sum of numeric values in the list (F# Interactive)
let rec calculateSum(rows) =
    match rows with
    | [] -> 0
    | (_, value)::tail -> 
        let remainingSum = calculateSum(tail)
        value + remainingSum

This function exhibits the recurring pattern for working with lists yet again. Writing code that follows the same pattern over and over may suggest that we’re doing something wrong (as well as being boring—repetition is rarely fun). Ideally, we should only write the part that makes each version of the code unique without repeating ourselves. This objection is valid for the previous example, and we can write it in a more elegant way. We’ll learn how to do this in upcoming chapters. You’ll still need both recursion and pattern matching in many functional programs, so it’s useful to look at one more example and become familiar with these concepts.

For an empty list, the function calculateSum simply returns 0. For a cons cell, it recursively sums values from the tail (the original list minus the first element) and adds the result to a value from the head (the first item from the list). The pattern matching in this code demonstrates one interesting pattern that’s worth discussing. In the second branch ❶, we need to decompose the cons cell, so we match the list against the head::tail pattern. The code is more complicated than that, since at the same time, it also matches the head against pattern for decomposing tuples, which is written as (first, second). This is because the list contains tuples storing the title as the first argument and the numeric value as the second argument. In our example, we want to read the numeric value and ignore the title, so we can use the underscore pattern to ignore the first member of the tuple. If we compose all these patterns into a single one, we get (_, value)::tail, which is what we used in the code.

If we look at the function signature printed by F# Interactive, we can see that the function takes a list of tuples as an input and returns an integer. The type of the input tuple is 'a * int, which means that the function is generic and works on lists containing any tuple whose second element is an integer. The first type is not relevant, because the value is ignored in the pattern matching. The F# compiler makes the code generic automatically in situations like this using a feature called automatic generalization.

The last command ❷ from listing 2 prepared the way for the test in listing 3: why enter test data more than once? Having calculated the sum to test the function, we finally calculate the percentage occupied by the record with a value 123. Because we want to get the precise result (21.24 percent), we convert the obtained integer to a floating point number using a function called float.

Creating a console application

Writing a simple console-based output for our application is a good start, because we can do it relatively easily and we’ll see the results quickly. In this section, we’ll use several techniques that will be important for the later graphical version as well. Even if you don’t need console-based output for your program, you can still start with it and later adapt it into a more advanced, graphical version as we’ll do in this chapter.

We’ve already finished most of the program in the previous section by writing common functionality shared by both the console and graphical versions. We have a function, processLines, that takes a list of strings loaded from the CSV file and returns a list of parsed tuples, and a function, calculateSum, that sums the numerical values from the data set.

Listing 4. Putting the console-based version together (F# Interactive)
open System
open System.IO

let convertDataRow(csvLine:string) =
    let cells = List.ofSeq(csvLine.Split(','))
    match cells with
    | title::number::_ ->
        let parsedNumber = Int32.Parse(number)
        (title, parsedNumber)
    | _ -> failwith "Incorrect data format!"

let rec processLines(lines) =
    match lines with
    | [] -> []
    | currentLine::remaining ->
        let parsedLine = convertDataRow(currentLine)
        let parsedRest = processLines(remaining)
        parsedLine :: parsedRest

let rec calculateSum(rows) =
    match rows with
    | [] -> 0
    | (_, value)::tail -> 
        let remainingSum = calculateSum(tail)
        value + remainingSum

let lines = List.ofSeq(File.ReadAllLines(@"C:\02-ReadDataForChart\data.csv"))

let data = processLines(lines)

let sum = float(calculateSum(data))

for (title, value) in data do
    let percentage = int((float(value)) / sum * 100.0)
    Console.WriteLine("{0,-18} - {1,8} ({2}%)", 
                      title, value, percentage)

Listing 4 starts by opening the System.IO namespace, which contains .NET classes for working with the filesystem. Next, we use the class File from this namespace and its method ReadAllLines ❶, which provides a simple way for reading text content from a file, returning an array of strings. Again we use the ofseq function to convert the array to a list of strings. The next two steps are fairly easy, because they use the two functions we implemented and tested in previous sections of this chapter—we process the lines and sum the resulting values.

Let’s now look at the last piece of code ❷. It uses a for loop to iterate over all elements in the parsed data set. This is similar to the foreach statement in C#. The expression between keywords for and in isn’t just a variable; it’s a pattern. As you can see, pattern matching is more common in F# than you might expect! This particular pattern decomposes a tuple into a title (the value called title) and the numeric value (called value).

In the body of the loop, we first calculate the percentage using the equation and then output the result using the familiar .NET Console.WriteLine method.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.