Login | Register   
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#

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


advertisement
In the article "An Introduction to F# for Functional Programming" you saw an example of working with basic shapes and calculating the total perimeter of a drawing based on those shapes (see Listing 1). You did that by enumerating the kinds of base shapes that can exist (these were rectangles and circles) in your Shape class, providing a constructor for a square (a new shape) in terms of a rectangle, and adding an instance method to calculate the perimeter for a given shape.

There are times though when you want to extend the kind of shapes you are working with. What about triangles, hexagons or arbitrary polygons, for instance? While it is possible to enumerate these as special forms on a Shape type like you did in your first implementation, having to incorporate the perimeter logic into a single place is not ideal. Instead, you want to switch to a more traditional OO approach where the various shapes "know" how to calculate their own perimeter.

This article shows how to use primary object-oriented techniques from languages such as C# and VB: implementation by inheritance and by interfaces. It also covers F#-specific object-oriented features such as object expressions, constructed classes, and named and optional arguments.



Abstract Base Classes and Inheritance
In the shapes example, a common approach is to encapsulate the desired "shared" functionality in an abstract base class and to require that subclasses provide their own implementations.

Author's Note: remember, all code in this article is assumed to be in #light mode (see the Getting Started section from the Introduction to F# article).



open System [<AbstractClass>] type Shape() = abstract Perimeter : float type Circle(r, x: int, y: int) = inherit Shape() override self.Perimeter = Convert.ToDouble (2*r) * System.Math.PI type Rectangle(x1, y1, x2, y2) = inherit Shape() static member Square(x, y, a) = DerivedRectangle(x, y, x+a, y+a) override self.Perimeter = 2*(x2-x1)+2*(y2-y1) |> Convert.ToDouble

The definition of Drawing is unchanged, although now Shape refers to the base class:

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

Because the Drawing constructor takes a sequence of base objects you will need to cast the instances of subclasses that you pass to it. For instance, here is a quick test in F# Interactive:

> let d = seq { for i in 1 .. 20 -> if i % 3 = 0 then Circle (i, i, i) :> Shape elif i % 3 = 1 then Rectangle (20, 20, 20+i*2, 20+i) :> Shape else Rectangle.Square (20, 20, i) :> Shape } |> Drawing > val a : Drawing > a.TotalInkNeeded;; val it : float = 1123.840674 >

Note how each shape instance was created without the "new" keyword and with an upcast to the Shape base class. Using the "new" keyword is optional, but the compiler warns you to use it when you attempt to create objects that implement the IDisposable interface—simply to indicate that various resources may be owned by those objects.

Creating and Implementing Interfaces
Implementation by inheritance works by building a hierarchy of classes. In the process, it makes distant subclasses overly complex and large. As the hierarchy of classes moves further away from a base type, they typically become increasingly specialized. That increasing specialization often requires those subclasses to override nearly all the base class methods, which makes extending the class hierarchy cumbersome.

An alternative approach is to abstract away certain required characteristics as interfaces, allowing any class that implements those interfaces (classes that otherwise are not part of the inheritance hierarchy) to be compatible.

Reworking the previous example, you can create an interface that requires that each shape should be able to calculate its perimeter:

type IShape = abstract member Perimeter : float

Here, the single abstract member and the lack of a constructor (note that there are no parameters—not even () to the type name) signifies that the type defined is an interface type. You can now implement this interface with multiple shape types. Here are rectangles, with a static constructor for squares:

type Rectangle(x1, y1, x2, y2) = static member Square(x, y, a) = Rectangle(x, y, x+a, y+a) interface IShape with member self.Perimeter = 2*(x2-x1)+2*(y2-y1) |> Convert.ToDouble

Circles are just as easy:

type Circle(r, x: int, y: int) = interface IShape with member self.Perimeter = Convert.ToDouble (2*r) * System.Math.PI

Object Expressions
One key advantage of interfaces is the ability to create compatible objects "on-the-fly," that is without declaring a type for them. You can do this via object expressions:

let line (x1: int, y1: int, x2, y2) = { new IShape with member self.Perimeter = let xlen = Convert.ToDouble(x2-x1) let ylen = Convert.ToDouble(y2-y1) Math.Sqrt(xlen**2.0 + ylen**2.0) }

Here, line is a function that takes the two end points of a line and creates an implementation of IShape with the perimeter (here the length) calculation implemented.

To adapt your Drawing type to the interface-based approach, you will need to change its constructor to take a sequence of IShape objects instead of Shapes:

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

Then you can try out the new types and the line object expression in F# Interactive:

> let b = seq { for i in 1 .. 20 -> if i % 4 = 0 then Circle (i, i, i) :> IShape elif i % 4 = 1 then Rectangle (20, 20, 20+i*2, 20+i) :> IShape elif i % 4 = 2 then Rectangle.Square (20, 20, i) :> IShape else line (i, i, i+i, i+i) } |> Drawing > val b : Drawing > b.TotalInkNeeded;; val it : float = 924.7728644 >

Note how you have to cast each implemented class to an IShape, except the result of line, which itself is anIShape.

Constructed and Ordinary Classes
You may have noticed that in the definition above for circles you had to explicitly annotate the x and y coordinates. If you removed these extra annotations you would get a compiler error telling you that the constructor was too generic (a later usage would "pin" these types and remove the errors). This is because these coordinates are not used in the type; therefore, the compiler inferred a generic type for them. Instead of enriching the circle class with functionality that uses these coordinates (which you are going to do shortly), you can simply tell the compiler that they are integers to clear the value restriction error.

So far, the so-called constructed classes syntax has been used. This is shorthand syntax for defining classes with a single constructor (although you can add multiple constructors also), a convenient and concise notation for most circumstances. You could have just as easily used the regular class syntax, which gives you more control over what your class publishes, but is slightly more verbose:

type AnotherCircle = val mutable Radius: int val mutable X: int val mutable Y: int new (r, x: int, y: int) = { Radius = r; X = x; Y = y } new (r) = { Radius = r; X = 0; Y = 0 } interface IShape with member self.Perimeter = Convert.ToDouble (2*self.Radius) * System.Math.PI

This class has two constructors and it exposes the Radius/X/Y fields, which are initialized upon constructing an AnotherCircle instance; this can be read or set thereafter. You can control the visibility of these fields by adding the private/protected/public visibility modifiers to their definition (fields are assumed to be public by default). You can also disable the setters by removing the mutable keyword.


Comment and Contribute

 

 

 

 

 


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

 

 

Sitemap