Introduction to Functional Programming in F# – Part 10

Introduction

In this post we are going to see how we can utilise some of the object programming features that F# offers. F# is a functional-first language but sometimes it is beneficial to use objects, particularly when interacting with code written in other, less functional, .Net languages or when you want to encapsulate some internal data structures and/or mutable state.

F# can do pretty much anything that C#/VB.Net can do with objects. We are going to concentrate on the core object programming features; class types, interfaces, encapsulation and equality.

Setting Up

Open VSCode and add a new folder. Add three new files (FizzBuzz.fs, RecentlyUsedList.fs and Coordinate.fs).

Solving the Problem

We will start in fizzbuzz.fs where we will be implementing FizzBuzz using object programming.

Add a module to the top of the page:

module FizzBuzzExample

Class Types

Now we are going to create our first class type:

type FizzBuzz() =
    member _.Calculate(value) =
        [(3, "Fizz");(5, "Buzz")]
        |> List.map (fun (v, s) -> if value % v = 0 then s else "")
        |> List.reduce (+)
        |> fun s -> if s <> "" then s else string value

Points of interest:

  • The brackets () after the type name are required. They can contain argument as we will see soon.

  • The member keyword defines the accessible members of the type.

  • The underscore is just a placeholder - it can be anything. It is convention to use _, __ or this.

Now that we have created our class type, we need to instantiate it to use it.

let doFizzBuzz =
    let fizzBuzz = FizzBuzz()
    [1..15]
    |> List.map fizzBuzz.Calculate

We don't use 'new' to create an instance of FizzBuzz. You only use 'new' when using an object that implements IDisposible and then we would use 'use' instead of 'let' to create a code block.

At the moment, we can only use '[(3, "Fizz");(5, "Buzz")]' as the mapping but it is easy to pass the mapping in through the constructor;

type FizzBuzz(mapping) =
    member _.Calculate(value) = 
        mapping
        |> List.map (fun (v, s) -> if n % v = 0 then s else "")
        |> List.reduce (+)
        |> fun s -> if s <> "" then s else string n

Notice that we don't need to assign the constructor argument to a binding to use it.

Now we need to pass the mapping in as the argument to the constructor in the doFizzBuzz function:

let doFizzBuzz =
    let fizzBuzz = FizzBuzz([(3, "Fizz");(5, "Buzz")])
    [1..15]
    |> List.map fizzBuzz.Calculate

We can move the function code from the member into the body of the class type as a new inner function:

type FizzBuzz(mapping) =
    let calculate n =
        mapping
        |> List.map (fun (v, s) -> if n % v = 0 then s else "")
        |> List.reduce (+)
        |> fun s -> if s <> "" then s else string n

    member _.Calculate(value) = calculate value

You cannot access the new calculate function from outside the class type. You don't have to do this but I find that it makes the code easier to read, especially as the number of class members increases.

Interfaces

Interfaces are very important in object programming as they define a contract that an implementation must offer. Let's add an interface to our fizzbuzz example:

type IFizzBuzz =
    abstract member Calculate : int -> string

Now we need to implement the interface in our FizzBuzz class type:

type FizzBuzz(mapping) =
    let calculate n =
        mapping
        |> List.map (fun (v, s) -> if n % v = 0 then s else "")
        |> List.reduce (+)
        |> fun s -> if s <> "" then s else string n

    interface IFizzBuzz with
        member _.Calculate(value) = calculate value

Nice and easy but you will see that we have a problem; The compiler has highlighted the Calculate function call in our doFizzBuzz function.

let doFizzBuzz =
    let fizzBuzz = FizzBuzz([(3, "Fizz");(5, "Buzz")])
    [1..15]
    |> List.map (fun n -> fizzBuzz.Calculate(n)) // Problem

There is an error because F# does not support implicit casting, so we have to upcast the instance to IFizzBuzz:

let doFizzBuzz =
    let fizzBuzz = FizzBuzz([(3, "Fizz");(5, "Buzz")]) :> IFizzBuzz //Upcast  
    [1..15]
    |> List.map (fun n -> fizzBuzz.Calculate(n)) // Fixed

An alternative would be to upcast as you use the interface function:

let doFizzBuzz =
    let fizzBuzz = FizzBuzz([(3, "Fizz");(5, "Buzz")])
    [1..15]
    |> List.map (fun n -> (fizzBuzz :> IFizzBuzz).Calculate(n))

Just because you can, it doesn't mean you should

The code above is designed to show how to construct class types and use interfaces. If you find yourself constructing interfaces for single functions, ask yourself if you really, really need the extra code and complexity or whether a simple function is enough.

Next we move on to a more complex/realistic example - a recently used list.

Encapsulation

We are going to create a recently used list as a class type. We will encapsulate a mutable collection within the type and provide an interface for how we can interact with it. The recently used list is an ordered list with the most recent item first but it is also a set as each value can only appear once in the list.

First we need to create an interface in RecentlyUsedList.fs:

type IRecentlyUsedList =
    abstract member IsEmpty : bool
    abstract member Size : int
    abstract member Clear : unit -> unit
    abstract member Add : string -> unit
    abstract member Get : int -> string option

By looking at the signatures, we can see that IsEmpty and Size are read-only properties and Clear, Add and Get are functions. Now we can create our class type and implement the IRecentlyUsedList interface:

type RecentlyUsedList() =
    let items = ResizeArray<string>()

    let add item =
        items.Remove item |> ignore
        items.Add item 

    let get index =
        if index >= 0 && index < items.Count 
        then Some items.[items.Count - index - 1]
        else None

    interface IRecentlyUsedList with
        member _.IsEmpty = items.Count = 0
        member _.Size = items.Count
        member _.Clear() = items.Clear()
        member _.Add(item) = add item
        member _.Get(index) = get index

A ResizeArray is the F# synonym for a standard .Net mutable generic list. Encapsulation ensures that you cannot access it directly (unless you use reflection etc) via the public interface.

Let's test our code in FSI (run each line seperately):

let mrul = RecentlyUsedList() :> IRecentlyUsedList

mrul.Add "Test"

mrul.IsEmpty = false // Should return true

mrul.Add "Test2"
mrul.Add "Test3"
mrul.Add "Test"

mrul.Get(0) = Some "Test" // Should return true

Now let's add a maximum size (capacity) to our IRecentlyUsedList interface:

type IRecentlyUsedList =
    abstract member IsEmpty : bool
    abstract member Size : int
    abstract member Capacity : int
    abstract member Clear : unit -> unit
    abstract member Add : string -> unit
    abstract member Get : int -> string option

You will notice that the compiler is complaining that we haven't implemented all of the interface, so let's fix it, add the capacity as a constructor argument, and add code to the Add function to ensure the oldest item is removed if we are at capacity when adding a new item:

type RecentlyUsedList(capacity:int) =
    let items = ResizeArray<string>(capacity)

    let add item =
        items.Remove item |> ignore
        if items.Count = items.Capacity then items.RemoveAt 0
        items.Add item

    let get index =
        if index >= 0 && index < items.Count 
        then Some items.[items.Count - index - 1]
        else None

    interface IRecentlyUsedList with
        member _.IsEmpty = items.Count = 0
        member _.Size = items.Count
        member _.Capacity = items.Capacity
        member _.Clear() = items.Clear()
        member _.Add(item) = add item
        member _.Get(index) = get index

All done. Let's test a recently used list with capacity of 5 using FSI:

let mrul = RecentlyUsedList(5) :> IRecentlyUsedList

mrul.Capacity // Should be 5

mrul.Add "Test"
mrul.Size // Should be 1
mrul.Capacity // Should be 5

mrul.Add "Test2"
mrul.Add "Test3"
mrul.Add "Test4"
mrul.Add "Test5"
mrul.Add "Test6"
mrul.Add "Test7"
mrul.Add "Test"

mrul.Size // Should be 5
mrul.Capacity // Should be 5
mrul.Get(0) = Some "Test" // Should return true

Encaspulation inside class types works really nicely. Now we move on to the issue of equality. Most of the types in F# support structural equality but class tyes do not.

Equality

Let's create a simple class type to store a GPS Coordinate in Coordinate.fs:

type Coordinate(latitude: float, longitude: float) =
    member _.Latitude = latitude
    member _.Longitude = longitude

To test equality, we can write some simple checks we can run in FSI:

let c1 = Coordinate(25.0, 11.98)
let c2 = Coordinate(25.0, 11.98)
let c3 = c1
c1 = c2 // false
c1 = c3 // true - reference the same instance

To support non-referential equality, we need to override GetHashCode and Equals, implement IEquatable and if we are going to use it with other .Net laguages, we need to handle = using op_Equality and allow null literals. This is an example of what we need:

open System

[<AllowNullLiteral>]
type GpsCoordinate(latitude: float, longitude: float) =
    let equals (other: GpsCoordinate) =
        if isNull other then
            false
        else
            latitude = other.Latitude
            && longitude = other.Longitude

    member _.Latitude = latitude
    member _.Longitude = longitude

    override this.GetHashCode() =
        hash (this.Latitude, this.Longitude)

    override _.Equals(obj) =
        match obj with
        | :? GpsCoordinate as other -> equals other
        | _ -> false

    interface IEquatable<GpsCoordinate> with
        member _.Equals(other: GpsCoordinate) =
            equals other

    static member op_Equality(this: GpsCoordinate, other: GpsCoordinate) =
        this.Equals(other)

We have used two built-in functions (hash and isNull) and we use pattern matching in Equals(obj) to see if the obj can be cast as a GpsCoordinate.

If we test this, we get the expected equality:

let c1 = Coordinate(25.0, 11.98)
let c2 = Coordinate(25.0, 11.98)
c1 = c2 // true

We have only scratched the surface of what is possible with F# object programming. We will look at other features in a future post. If you want to find out more about this topic (plus many other useful things), I highly recommend that you read Stylish F# by Kit Eason.

Conclusion

In this post we have started to look at object programming in F#. In particular, we have looked at scoping/visibility, encapsulation, interfaces and equality. The more you interact with the rest of the .Net ecosystem, the more you will need to use object programming.

In the next post we will primarily look at recursion.

If you have any comments on this series of posts or suggestions for new ones, send me a tweet (@ijrussell) and let me know.

Zurück
Zurück

Introduction to Functional Programming in F# – Part 11

Weiter
Weiter

My Workflows at Trustbit During the Quarantine