Basics 25 - Classes and Interfaces

Learning-FSharp/Ch25-OOP-Classes/Program.fs

Check out all the comments that are placed in the source file.

The objective of this article is to give you an overview of classes and interfaces in F# through various examples.

Before We Begin

Throughout this series, I have and will continue to speak against OOP. You may wonder why am I covering classes and interfaces. Largely for 3 reasons:

  1. Some features of F# can't be used without classes; the prime example being computation expressions

  2. You need to use existing classes and interfaces that belong to .NET Framework or third-party libraries

  3. You may need to implement areas of a project in F# that should be exposed in C# in OOP fashion

My goal is to give an overall idea of the playing field. I am not covering classes, inheritance and interfaces in depth here. I use these to the absolute minimum possible and stick to functional design.

Basic Syntax for Creating Classes

// The basic template for a class.
// Notice the () after Student1, the type name.
// Student1() is the default constructor for type (class) Student1.
type Student1() =
    class

    end
  1. A class must have the default constructor

  2. The default constructor is part of the type declaration, Student1() in this case

Classes with Additional Constructors and Members

// Class/type Student2.
// The default constructor takes two arguments name and age.
type Student2(name: string, age: int) =
    class
        // This area is actually default constructor's body
        do printfn "Creating Student2 with name: %s" name
        do printfn "Creating Student2 with age: %d" age

        // After the body of the default constructor comes
        // the area for members

        // Additional constructors
        // Must invoke the default constructor
        new() =
            printfn "%s" "new() Invoked."
            Student2("Unknown", 0)

        new(name: string) =
            printfn "%s" "new(name) Invoked."
            Student2(name, 0)

        new(age: int) =
            printfn "%s" "new(age) Invoked."
            Student2("Unknown", age)

        // An instance method.
        // Notice name and age are available.
        // this is the name used to refer to the current instance.
        // You can name it this or anything else.
        member this.NamePlusAge() = $"{name} {age}"

        // Notice this is replaced with current.
        member current.AnotherNamePlusAge() = $"{name} {age}"

        // A static method
        static member Create name age = Student2(name, age)
    end
  1. The default constructor is Student2(name: string, age: int)

  2. Three additional constructors are created: new(), new(name: string) and new(age: int)

  3. Notice that all additional constructors must call the default constructor

  4. The area between the beginning of the class body and the area for class members is the body of the default constructor

  5. Instance members:

    1. Unlike C#/Java, you are free to refer to the current/calling instance by any name

    2. member this.NamePlusAge() = refers to the current instance by this

    3. member current.AnotherNamePlusAge() = refers to the current instance by current

  6. Static members are created with static member

Basic Syntax for Creating Interfaces

// The basic template for an Interface.
type INamePlusAge =
    interface
        // A method for derived types to implement.
        abstract member NamePlusAge: unit -> string
    end
  1. Use abstract member declaring members in the interface

  2. These members must be defined the type implementing the interface

Class Implementing an Interface

// Example of a class implementing an interface.
type Student3(name: string, age: int) =
    class
        // Default constructor body.
        do printfn "%s %d" name age

        // Area for members.

        // Implementation of interface method.

        interface INamePlusAge with
            member this.NamePlusAge() = $"{name} {age}"
    end
  1. Implement interface with interface <Interface Name> with

  2. Define all the members that are part of the interface

Record Implementing an Interface

// Example of a record implementing an interface
type StudentRecord =
    { Name: string
      Age: int }

    interface INamePlusAge with
        member this.NamePlusAge() = $"{this.Name} {this.Age}"
  1. Same as the class

A Generic Interface and Class Implementation

// Here's an interface with generic type T.
type IGenericIntarfec<'T> =
    interface
        // Methods for derived types to implement.

        abstract member Get: unit -> 'T
        abstract member GetToString: unit -> string
    end

// A class implementing IGenericIntarfec<'T>.
type MyClass<'T>(t: 'T) =
    class
        interface IGenericIntarfec<'T> with
            member this.Get() = t
            member this.GetToString() = t.ToString().ToUpper()
    end
  1. Here's an interface with a generic type 'T

Let Bindings in Class for Private Fields and Functions

// Example of let bindings within a class.
// Use let bindings for creating provate fields and functions.
type ClassWithLetBindings(name: string, age: int) =
    class
        // 2 private fields and a private function.
        let _name = name
        let _age = age
        let agePlus num = _age + num

        // A private static field.
        static let _a = "A"

        // A private static function.
        static let _add2Nums x y = x + y

        member this.NamePlusAge() = $"{name} {age}"
        member this.YetAnotherNamePlusAge() = $"{_name} {_age}"
        member this.AgePlusNum num = agePlus num
    end
  1. You can use let bindings to create private fields and functions

  2. Similarly, you can use static let to create private static fields and private static functions

Class Properties with Backing Stores

// Example of properties with backing stores.
type ClassWithProperties(name: string, age: int, rollNo: string) =
    class
        let _name = name
        let mutable _age = age
        let mutable _rollNo = rollNo

        // A read-only property.
        member this.Name = _name

        // A write-only property.
        member this.RollNo
            with set (value) = _rollNo <- value

        // A read-write property.
        member this.Age
            with get () = _age
            and set (value) = _age <- value
    end
  1. Here's an example of properties that are linked with backing stores (fields)

Class Properties without Backing Stores

// Example of properties with no backing stores.
type ClassWithPropertiesWithNoBackingStore(name: string, age: int, rollNo: string) =
    class
        // A read-only property.
        member val Name = name

        // 2 read-write properties.
        member val Age = age with get, set
        member val RollNo = rollNo with get, set
    end
  1. Here's an example of properties without backing stores (fields)

Creating Instances of Classes

// new is optional
let student1_1 = Student1()
let student1_2 = new Student1()

// Invoking difference constructors
let student2_1 = Student2()
let student2_2 = Student2("Name")
let student2_3 = Student2(10)
let student2_4 = Student2("Name", 10)

// Interface pointing to class and record
let iface_1: INamePlusAge = Student3("Name", 10)
let iface_2: INamePlusAge = { Name = "Name"; Age = 10 }

// Calling interface method

let x = iface_1.NamePlusAge()
let y = iface_2.NamePlusAge()

// If you need to call interface methods,
// can't be done directly from class or record

let student3_1 = Student3("Name", 10)

// student3_1.NamePlusAge <- will result in compile error

// Use casting operating :>
let namePlusAge = (student3_1 :> INamePlusAge).NamePlusAge()

// Generic class instance
let g_1 = MyClass<int>(10)
let g_2 = MyClass<string>("Ten")

// Casting to interface

let x = (g_1 :> IGenericIntarfec<int>).Get()
let y = (g_2 :> IGenericIntarfec<string>).Get()

If you have reached so far, congratulations.

Keep reading!