Login | Register   
LinkedIn
Google+
Twitter
RSS Feed
Download our iPhone app
TODAY'S HEADLINES  |   ARTICLE ARCHIVE  |   FORUMS  |   TIP BANK
Browse DevX
Sign up for e-mail newsletters from DevX


advertisement
 

Working with Objects in F# : Page 2

Learn how to use common object-oriented techniques in F# so that various shapes know how to calculate their own perimeter.


advertisement
A key advantage of using constructed classes is that you can embed a construction sequence that describes the steps required to construct an object of the given type. This construction sequence is a series of let bindings (and possibly do statements) that are in scope for the non-static members of the class. The following code refuses to create a polygon with fewer than three pairs of defining coordinates. (Note that you might just as easily log each polygon creation, keep a global counter of polygons created and refused, etc.):

type Polygon(coords) = do if List.length coords < 3 then failwith "Polygon must have at least 3 sides" let length (x1:int) (y1:int) x2 y2 = let cFloat (y: int) = Convert.ToDouble(y) Math.Sqrt(cFloat(x2-x1)**2.0+cFloat(y2-y1)**2.0) let last lst = coords |> List.rev |> List.hd let _, perimeter = List.fold_left (fun ((x1, y1), accum) (x2, y2) -> (x2, y2), length x1 y1 x2 y2 + accum) (last coords, 0.0) coords interface IShape with member self.Perimeter = perimeter

If the coordinates pass muster, you can calculate the perimeter of the polygon within the construction sequence. You do that by folding over the polygon's coordinates (starting at the last one), calculating and then summing the length of each side (the distance between two pairs of coordinates). Because the code performs the perimeter calculation each time you create a new polygon, you can access the result the Perimeter (through IShape) property in constant time. You can quickly test this new class in F# Interactive:

> let p = Polygon([(10,20); (30, 40); (20, 30)]);; val p : Polygon > (p :> IShape).Perimeter;; val it : float = 56.5685424 >

Adding Indexers to Your Classes
In essence, OO programming is about adding dot-notation to your values. You can further enhance the dot-notation you offer on your classes by adding indexers. Indexers provide a concise way to access (read or write) elements in a conceptual container. For instance, you may want to provide a mechanism to read the coordinates in a polygon via an index (starting at zero). You can easily implement this by enhancing the definition above as follows:


type Polygon(coords) = ... member self.Item with get(idx) = if Seq.length coords <= idx then failwith "Index out of bound" else Seq.nth idx coords interface IShape with member self.Perimeter = perimeter

Here, the Item member provides the underlying mechanism for the .[idx] syntax sugar to work:

> let p = Polygon([(10,20); (30, 40); (20, 30)]);; val p : Polygon > (p :> IShape).Perimeter;; val it : float = 56.5685424 > p.[0];; val it : int * int = (10, 20) > p.[3];; Microsoft.FSharp.Core.FailureException: Index out of bound at FSI_0059.Polygon.get_Item(Int32 idx) in c:\demo1\Demo1\Program.fs:line 127 at <StartupCode$FSI_0063>.$FSI_0063._main() stopped due to error

Named and Optional Arguments
You can make your code more readable by naming arguments in your calls. For instance, you may construct a new rectangle this way:

Rectangle(x1=10, y1=10, x2=20, y2=30)

The main advantage of naming arguments (besides readability) is that you can list the arguments in arbitrary order, making prototype changes in your functions easier. The compiler warns you if you forgot to include an argument or if you assigned a given parameter more than once.

Optional arguments enable you to omit certain parameters and typically assume default values for them. You can make an argument optional by prefixing it with a question mark (?). As an example, you might want to equip your various shape objects with the ability to draw themselves on a canvas. You can extend the IShape interface accordingly:

open System.Drawing type IShape = abstract member Perimeter : float abstract member Draw : Graphics -> unit

Then you change the definition for circles by adding an optional color parameter (note that the preceding code fragment opens the System.Drawing namespace to bring the Color type into scope):

type Circle(r, x: int, y: int, ?color: Color) = let Color = match color with | None -> Color.Black | Some c -> c interface IShape with member self.Perimeter = Convert.ToDouble (2*r) * System.Math.PI member self.Draw g = use pen = new Pen(Color) g.DrawEllipse(pen, x-r, y-r, x+r, y+r)

Optional arguments are passed as F# option types, so the type of the color parameter will be Color option. In the construction sequence you check whether an argument was supplied for the color, and if not you default to black (or any other color of your choice). Remember that the Color binding in the construction sequence is visible in all non-static members, and indeed you will use it in the Draw method to draw a circle with the given color.

You can also patch up the rest of the shapes you worked with previously in a similar fashion (see Listing 2).

To complete the example, you can now enhance your Drawing class to draw its shapes:

// A drawing is simply a sequence of shapes. type Drawing = Drawing of seq with member self.TotalInkNeeded = match self with | Drawing shapes -> shapes |> Seq.sum_by (fun s -> s.Perimeter) member self.Draw g = match self with | Drawing shapes -> shapes |> Seq.iter (fun s -> s.Draw g) end

To test your code, the following snippet takes a drawing and displays it on a form:

open System.Windows.Forms let drawing = seq { for i in 1 .. 20 -> if i % 4 = 0 then Circle (x=i, y=i, r=i, color=Color.Red) :> IShape elif i % 4 = 1 then Rectangle (x1=20, y1=20, x2=20+i*2, y2=20+i) :> IShape elif i % 4 = 2 then Rectangle.Square (20, 20, i) :> IShape else line (i, i, i+i, i+i) } |> Drawing [<STAThreadAttribute()>] let _ = let form = new Form(Text="Example") form.Paint.Add (fun args -> let g = args.Graphics drawing.Draw g) Application.Run(form)

In this scenario, you used named and optional parameters for constructing circles and rectangles. The snippet responsible for drawing them created the main form and mutated it to have a given title in one step; this is a common shorthand notation for multiple property assignments that typically follow a UI object instantiation; in fact you can list further property-value pairs by separating them with commas.

The essence of the display logic is a two-line event handler—you simply bind to the Paint event a new handler that retrieves the form's graphics context and uses it to call the Draw method on the drawing, which in turn draws each shape in the drawing.

Conclusion
In this article you saw the basic object-oriented techniques that you can employ when developing object-oriented applications in F#. You saw how you can create abstract and derived classes to form class hierarchies via inheritance, how you can write "abstract" classes that implement various interfaces using constructed classes, and how you can implement object interfaces with object expressions that are specific to F#. You also used named and optional arguments on constructed class instances and added indexer notation to one of your classes. Finally, you developed a simple UI application to display a collection of shapes on a form.

These techniques augment your arsenal of functional programming constructs and enable you to interface with C# or VB code, implement .NET interfaces, or simply work with UI elements that are traditionally object-oriented.



Adam Granicz is the CEO of IntelliFactory, the primary place for F# development, training and consulting services, and the coauthor of Expert F#, the most comprehensive guide on F# coauthored with Don Syme, the designer of the language. He has done research in functional programming languages and holds a Masters degree from the California Institute of Technology.
Comment and Contribute

 

 

 

 

 


(Maximum characters: 1200). You have 1200 characters left.

 

 

Sitemap