January 19, 2012 9:49pm
Real-World Functional Programming, Part 1 (Chapter 4)

Chapter 4 was actually quite fun! This chapter goes through a simple application that reads data from a csv file and produces a pie chart. The chapter starts off using the F# interactive console all the way to creating a F# application that creates a simple GUI.

Some notes while building the application
  • By the use of pattern matching you can create generic functions. In C# you need to specify the function as generic but by the use of pattern matching F# will create functions that are automatically generalized. see calculateSum
  • F# does not insert automatic conversions between numeric types as in C#, you need to directly use int(), float(), etc.
  • You can use pattern matching the in for in construct like

for (title, value) in data do

  • You can use the built in sprintf or String.Format to format strings, if you can use the built in F# method it is usually preferable to do so.
  • Make sure you read the book when typing up the examples in Visual Studio, I was banging my head against the desk for 10-15 minutes until I realized it was RectangleF not Rectangle
  • module Module1
    
    open System
    open System.Drawing
    open System.Windows.Forms
    open System.IO
    
    
    let convertDataRow(csvLine:string) =
        let cells = List.ofSeq(csvLine.Split(',')) //creates a list where the first first 
    
    //element is the name and the last is a number
        match cells with //do a pattern match on the list
        | title :: number :: _ -> //match the tuple that has a first element a next/last element and any other elements are ignored
            let parsedNumber = Int32.Parse(number) //parse the first element value
            (title, parsedNumber) //create a new tuple that is of type string and int
        | _ -> failwith "Incorrect data format!"
    
    
    let rec processLines(lines) =
        match lines with //do a pattern match on the lines
        | [] -> [] //if the line is empty do nothing
        | currentLine :: remaining -> // if the lines are not empty, 
    
    //grab the first element into currentLine and the remaining lines into that variable
            let parsedLine = convertDataRow(currentLine) //parse the line into a tuple (string, int)
            let parsedRest = processLines(remaining) //rec call the processLines function with the rest of the lines remaining
            parsedLine :: parsedRest //build a list of all the parsed lines.
    
    
    
    let rec calculateSum(rows) =
        match rows with
        | [] -> 0
        | (_, value) :: tail ->
            let remainingSum = calculateSum(tail)
            value + remainingSum
    
    let mainForm = new Form(Width = 620, Height = 450, Text = "Pie Chart") //same way of doing 
    
    //var mainForm = new Form {Width = 620};
    
    let menu = new ToolStrip()
    
    let openButton = new ToolStripButton("Open")
    let saveButton = new ToolStripButton("Save", Enabled = false) //same way of 
    
    //doing var saveButton = new ToolStripButton("Save") { Enabled = false};
    
    ignore(menu.Items.Add(openButton)) //calling menu.Items.Add() returns the index of the added element, to make sure the result 
    
    //is ignored by F# use ignore()
    ignore(menu.Items.Add(saveButton)) //this takes any return value and makes it return unit (void)
    
    let boxChart =
        new PictureBox
            (BackColor = Color.White, Dock = DockStyle.Fill,
            SizeMode = PictureBoxSizeMode.CenterImage)
    
    mainForm.Controls.Add(menu)
    mainForm.Controls.Add(boxChart)
    
    let random = new Random()
    let randomBrush() =
        let r,g,b = random.Next(256), random.Next(256), random.Next(256)
        new SolidBrush(Color.FromArgb(r,g,b))
    
    let drawPieSegment(gr:Graphics, title, startAngle, occupiedAngle) =
        let brush = randomBrush()
        gr.FillPie(brush, 170, 70, 260, 260, startAngle, occupiedAngle)
        brush.Dispose()
    
    let font = new Font("Times New Roman", 11.0f)
    
    let centerX, centerY = 300.0, 200.0
    let labelDistance = 150.0
    
    let drawLabel (graphics:Graphics, title, startAngle, angle) =
        let labelAngle = float(startAngle + angle/2)
        let ra = Math.PI * 2.0 * labelAngle  / 360.0
        let x = centerX + labelDistance * cos(ra)
        let y = centerY + labelDistance * sin(ra)
        let size = graphics.MeasureString(title, font)
        let xPoint = float32(x) - size.Width / 2.0f
        let yPoint = float32(y) - size.Height / 2.0f
        let rc = new PointF(xPoint, yPoint)
        
        let boundingBox = new RectangleF(rc, size)
        graphics.DrawString(title, font, Brushes.Black, boundingBox)
    
    let drawStep(drawingFunction, graphics:Graphics, sum, data) =
        let rec drawStepUntil(data, angleSoFar) =
            match data with
            | [] -> () //returns unit ie void
            | [title, value] -> //only matches a tuple of tile, value with no tail element
                let angle = 360 - angleSoFar
                drawingFunction(graphics, title, angleSoFar, angle)
            | (title, value) :: tail -> //matches a tuple head of title/value WITH a tail element(s)
                let angle = int(float(value) / sum * 360.0)
                drawingFunction(graphics, title, angleSoFar, angle)
                drawStepUntil(tail, angleSoFar + angle)
        drawStepUntil(data, 0)
    
    
    let drawChart(file) =
        let lines = List.ofSeq(File.ReadAllLines(file))
        let data = processLines(lines)
        let sum = float(calculateSum(data))
    
        let pieChart = new Bitmap(600, 400)
        let graphics = Graphics.FromImage(pieChart)
        graphics.Clear(Color.White)
        drawStep(drawPieSegment, graphics, sum, data)
        drawStep(drawLabel, graphics, sum, data)
    
        graphics.Dispose()
        pieChart
    
    let openAndDrawChart(e) =
        let dialog = new OpenFileDialog(Filter="CSV Files|*.csv")
        if(dialog.ShowDialog() = DialogResult.OK) then
            let pieChart = drawChart(dialog.FileName)
            boxChart.Image <- pieChart
            saveButton.Enabled <- true
    let saveDrawing(e) =
        let dialog = new SaveFileDialog(Filter="PNG Files|*.png")
        if(dialog.ShowDialog() = DialogResult.OK) then
            boxChart.Image.Save(dialog.FileName)
    
    //TODO: Drawing ofthe chart and UI
    [<STAThread>]
    do
        openButton.Click.Add(openAndDrawChart)
        saveButton.Click.Add(saveDrawing)
        Application.Run(mainForm)

    I will say as exciting as this application was to produce I am still struggling with the way you create applications. I am very curious what is the best practice when you start to create separate modules for your applications. Right now everything is a function, which makes sense but looking at the code you have one file with just a bunch of functions! (I guess i am still thinking in the O-O style mindset.

    1. markcoleman posted this
    Blog comments powered by Disqus