Skip to content
AgileLogics
7 min read

Interfaces, Inheritance, and Object Expressions

Prerequisites: Classes and members

Interfaces

An interface is a set of members a type promises to provide:

type IShape =
    abstract Area: float
    abstract Name: string

type Circle(r: float) =
    interface IShape with
        member _.Area = System.Math.PI * r * r
        member _.Name = "circle"

type Square(side: float) =
    interface IShape with
        member _.Area = side * side
        member _.Name = "square"

let shapes: IShape list = [ Circle(1.0); Square(2.0) ]

for s in shapes do
    printfn "%s: %.2f" s.Name s.Area
circle: 3.14
square: 4.00

F# implements interfaces explicitly: to call Area on a Circle, you need an IShape value. Putting them in an IShape list converts them automatically; elsewhere, upcast with :> ((Circle 1.0 :> IShape).Area).

Inheritance

[<AbstractClass>]
type Animal(name: string) =
    member _.Name = name
    abstract Sound: string
    default _.Sound = "..."
    member this.Speak() = $"{this.Name} says {this.Sound}"

type Dog(name) =
    inherit Animal(name)
    override _.Sound = "Woof"

type Fish(name) =
    inherit Animal(name)

let animals: Animal list = [ Dog "Rex"; Fish "Nemo" ]
for a in animals do
    printfn "%s" (a.Speak())

match animals[0] with
| :? Dog -> printfn "the first animal is a dog"
| _ -> printfn "not a dog"
Rex says Woof
Nemo says ...
the first animal is a dog
  • abstract declares a member; default gives it a default implementation; override replaces it in a subclass.
  • :? tests the runtime type, and :?> downcasts (it throws if the cast is wrong, so prefer the pattern).

In idiomatic F#, deep inheritance hierarchies are rare. A discriminated union is usually better for a closed set of variants (all shapes you know about), and interfaces for an open set (shapes other people can add).

Object expressions

An object expression creates an object implementing an interface on the spot, without declaring a class:

type ILogger =
    abstract Log: string -> unit

let consoleLogger prefix =
    { new ILogger with
        member _.Log message = printfn "[%s] %s" prefix message }

let run (logger: ILogger) items =
    for i in items do
        logger.Log $"processing {i}"

run (consoleLogger "app") [1; 2]

let byLength =
    { new System.Collections.Generic.IComparer<string> with
        member _.Compare(a, b) = compare a.Length b.Length }

let words = System.Collections.Generic.List<string>([ "ccc"; "a"; "bb" ])
words.Sort byLength
printfn "%A" (List.ofSeq words)
[app] processing 1
[app] processing 2
["a"; "bb"; "ccc"]

Object expressions are great for small adapters, test doubles, and .NET interfaces like IComparer and IDisposable.

Interfaces on records and unions

Records and unions can implement interfaces too:

type IDescribe =
    abstract Describe: unit -> string

type Product =
    { Name: string; Price: decimal }
    interface IDescribe with
        member this.Describe() = $"{this.Name} costs {this.Price}"

let p = { Name = "Tea"; Price = 3.5m }
printfn "%s" ((p :> IDescribe).Describe())
Tea costs 3.5

Practice

Define an interface IDiscount with a method Apply: decimal -> decimal. Create two discounts with object expressions — 10% off, and 5 off with a minimum price of 0 — and apply both to 30 and to 3.

Show solution
type IDiscount =
    abstract Apply: decimal -> decimal

let percentOff (p: decimal) =
    { new IDiscount with
        member _.Apply price = price * (1m - p / 100m) }

let amountOff (a: decimal) =
    { new IDiscount with
        member _.Apply price = max 0m (price - a) }

for d in [ percentOff 10m; amountOff 5m ] do
    printfn "%M %M" (d.Apply 30m) (d.Apply 3m)
27.0 2.7
25 0