devxlogo

Working with Objects in F#

Working with Objects in F#

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[]type Shape() =    abstract Perimeter : floattype Circle(r, x: int, y: int) =    inherit Shape()    override self.Perimeter =        Convert.ToDouble (2*r) * System.Math.PItype 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 seqwith    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 seqwith    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.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:demo1Demo1Program.fs:line 127   at .$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.Drawingtype 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 seqwith    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.Formslet 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[]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.

See also  AI's Impact on Banking: Customer Experience and Efficiency
devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist