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.

devx-admin

devx-admin

Share the Post:
Apple Tech

Apple’s Search Engine Disruptor Brewing?

As the fourth quarter of 2023 kicks off, the technology sphere is abuzz with assorted news and advancements. Global stocks exhibit mixed results, whereas cryptocurrency

Revolutionary Job Market

AI is Reshaping the Tech Job Market

The tech industry is facing significant layoffs in 2023, with over 224,503 workers in the U.S losing their jobs. However, experts maintain that job security

Foreign Relations

US-China Trade War: Who’s Winning?

The August 2023 visit of Gina Raimondo, the U.S. Secretary of Commerce, to China demonstrated the progress being made in dialogue between the two nations.

Pandemic Recovery

Conquering Pandemic Supply Chain Struggles

The worldwide coronavirus pandemic has underscored supply chain challenges that resulted in billions of dollars in losses for automakers in 2021. Consequently, several firms are

Game Changer

How ChatGPT is Changing the Game

The AI-powered tool ChatGPT has taken the computing world by storm, receiving high praise from experts like Brex design lead, Pietro Schirano. Developed by OpenAI,

Apple Tech

Apple’s Search Engine Disruptor Brewing?

As the fourth quarter of 2023 kicks off, the technology sphere is abuzz with assorted news and advancements. Global stocks exhibit mixed results, whereas cryptocurrency tokens have seen a substantial

GlobalFoundries Titan

GlobalFoundries: Semiconductor Industry Titan

GlobalFoundries, a company that might not be a household name but has managed to make enormous strides in its relatively short 14-year history. As the third-largest semiconductor foundry in the

Revolutionary Job Market

AI is Reshaping the Tech Job Market

The tech industry is facing significant layoffs in 2023, with over 224,503 workers in the U.S losing their jobs. However, experts maintain that job security in the sector remains strong.

Foreign Relations

US-China Trade War: Who’s Winning?

The August 2023 visit of Gina Raimondo, the U.S. Secretary of Commerce, to China demonstrated the progress being made in dialogue between the two nations. However, the United States’ stance

Pandemic Recovery

Conquering Pandemic Supply Chain Struggles

The worldwide coronavirus pandemic has underscored supply chain challenges that resulted in billions of dollars in losses for automakers in 2021. Consequently, several firms are now contemplating constructing domestic manufacturing

Game Changer

How ChatGPT is Changing the Game

The AI-powered tool ChatGPT has taken the computing world by storm, receiving high praise from experts like Brex design lead, Pietro Schirano. Developed by OpenAI, ChatGPT is known for its

Future of Cybersecurity

Cybersecurity Battles: Lapsus$ Era Unfolds

In 2023, the cybersecurity field faces significant challenges due to the continuous transformation of threats and the increasing abilities of hackers. A prime example of this is the group of

Apple's AI Future

Inside Apple’s AI Expansion Plans

Rather than following the widespread pattern of job cuts in the tech sector, Apple’s CEO Tim Cook disclosed plans to increase the company’s UK workforce. The main area of focus

AI Finance

AI Stocks to Watch

As investor interest in artificial intelligence (AI) grows, many companies are highlighting their AI product plans. However, discovering AI stocks that already generate revenue from generative AI, such as OpenAI,

Web App Security

Web Application Supply Chain Security

Today’s web applications depend on a wide array of third-party components and open-source tools to function effectively. This reliance on external resources poses significant security risks, as malicious actors can

Thrilling Battle

Thrilling Battle: Germany Versus Huawei

The German interior ministry has put forward suggestions that would oblige telecommunications operators to decrease their reliance on equipment manufactured by Chinese firms Huawei and ZTE. This development comes after

iPhone 15 Unveiling

The iPhone 15’s Secrets and Surprises

As we dive into the most frequently asked questions and intriguing features, let us reiterate that the iPhone 15 brings substantial advancements in technology and design compared to its predecessors.

Chip Overcoming

iPhone 15 Pro Max: Overcoming Chip Setbacks

Apple recently faced a significant challenge in the development of a key component for its latest iPhone series, the iPhone 15 Pro Max, which was unveiled just a week ago.

Performance Camera

iPhone 15: Performance, Camera, Battery

Apple’s highly anticipated iPhone 15 has finally hit the market, sending ripples of excitement across the tech industry. For those considering upgrading to this new model, three essential features come

Battery Breakthrough

Electric Vehicle Battery Breakthrough

The prices of lithium-ion batteries have seen a considerable reduction, with the cost per kilowatt-hour dipping under $100 for the first occasion in two years, as reported by energy analytics

Economy Act Soars

Virginia’s Clean Economy Act Soars Ahead

Virginia has made significant strides towards achieving its short-term carbon-free objectives as outlined in the Clean Economy Act of 2020. Currently, about 44,000 megawatts (MW) of wind, solar, and energy

Renewable Storage Innovation

Innovative Energy Storage Solutions

The Department of Energy recently revealed a significant investment of $325 million in advanced battery technologies to store excess renewable energy produced by solar and wind sources. This funding will

Renesas Tech Revolution

Revolutionizing India’s Tech Sector with Renesas

Tushar Sharma, a semiconductor engineer at Renesas Electronics, met with Indian Prime Minister Narendra Modi to discuss the company’s support for India’s “Make in India” initiative. This initiative focuses on

Development Project

Thrilling East Windsor Mixed-Use Development

Real estate developer James Cormier, in collaboration with a partnership, has purchased 137 acres of land in Connecticut for $1.15 million with the intention of constructing residential and commercial buildings.

USA Companies

Top Software Development Companies in USA

Navigating the tech landscape to find the right partner is crucial yet challenging. This article offers a comparative glimpse into the top software development companies in the USA. Through a

Software Development

Top Software Development Companies

Looking for the best in software development? Our list of Top Software Development Companies is your gateway to finding the right tech partner. Dive in and explore the leaders in

India Web Development

Top Web Development Companies in India

In the digital race, the right web development partner is your winning edge. Dive into our curated list of top web development companies in India, and kickstart your journey to

USA Web Development

Top Web Development Companies in USA

Looking for the best web development companies in the USA? We’ve got you covered! Check out our top 10 picks to find the right partner for your online project. Your

Clean Energy Adoption

Inside Michigan’s Clean Energy Revolution

Democratic state legislators in Michigan continue to discuss and debate clean energy legislation in the hopes of establishing a comprehensive clean energy strategy for the state. A Senate committee meeting

Chips Act Revolution

European Chips Act: What is it?

In response to the intensifying worldwide technology competition, Europe has unveiled the long-awaited European Chips Act. This daring legislative proposal aims to fortify Europe’s semiconductor supply chain and enhance its