Search code examples
c#inheritanceinterfacef#access-modifiers

F# class implements interface as private member, why?


I decided to practice with F# today and found one interesting thing. I shall provide the similar code both in C# and F#, but with rather different behavior using DAO (Data access object):

C# version:

interface IPoint2D
{
    int X { get; set; }
    int Y { get; set; }
}

class Point2D : IPoint2D
{
    private int _x = 0;
    private int _y = 0;

    public int X { get { return _x; } set { _x = value; } }
    public int Y { get { return _y; } set { _y = value; } }
}

F# version:

type IPoint2D =
    abstract member X: int
    abstract member Y: int

type Point2D(x: int, y: int) = 
    let _x = 0
    let _y = 0

    interface IPoint2D with
        member this.X = x
        member this.Y = y

The 1st difference, which is very visible, that using C#, I must declare members as public for implementation contract.

But, why is F# allowing to implement the interface as private member? What's the sense?

enter image description here


Solution

  • F# requires objects to be upcast in order to directly access their interfaces. let x = (firstPoint :> IPoint2D).X

    F# does support implicit interface casting for function parameters. It used to require generics but has been update in newer versions of the language.

    let Distance (a:IPoint2D) (b:IPoint2D) =
        (a.X - b.X) * (a.X - b.X) + (a.Y - b.Y) * (a.Y - b.Y)
        |> float |> Math.Sqrt
    
    let firstPoint = new Point2D(2, 3)
    let secondPoint = new Point2D(4, 5)
    
    let distance = Distance firstPoint secondPoint
    printf "%f" distance
    

    Not everyone agrees but as a design rule objects shouldn't be upcasted to access their interfaces directly. Doing so increases code coupling. If code is written that calls methods on an objects interfaces, that objects class can't be easily changed without updating all of the call sights.

    What if the interface was IPoint3D with x,y,z and you wanted to changed it to IPoint2D, all casts that reference z would have to be updated instead of just interface consumers like Distance.

    I believe this design choice was done for consistency with the rest of the language and to foster looser coupling while using OOP. There is a user voice feature request from 2014 that has gone unanswered requesting implicit upcasting.