F Sharp Programming/Inheritance
F# : Inheritance |
Many object-oriented languages use inheritance extensively in the .NET BCL to construct class hierarchies.
Subclasses
[edit | edit source]A subclass is, in the simplest terms, a class derived from a class which has already been defined. A subclass inherits its members from a base class in addition to adding its own members. A subclass is defined using the inherit
keyword as shown below:
type Person(name) =
member x.Name = name
member x.Greet() = printfn "Hi, I'm %s" x.Name
type Student(name, studentID : int) =
inherit Person(name)
let mutable _GPA = 0.0
member x.StudentID = studentID
member x.GPA
with get() = _GPA
and set value = _GPA <- value
type Worker(name, employer : string) =
inherit Person(name)
let mutable _salary = 0.0
member x.Salary
with get() = _salary
and set value = _salary <- value
member x.Employer = employer
Our simple class hierarchy looks like this:
System.Object (* All classes descend from *) - Person - Student - Worker
The Student
and Worker
subclasses both inherit the Name
and Greet
methods from the Person
base class. This can be demonstrated in fsi:
> let somePerson, someStudent, someWorker =
new Person("Juliet"), new Student("Monique", 123456), new Worker("Carla", "Awesome Hair Salon");;
val someWorker : Worker
val someStudent : Student
val somePerson : Person
> somePerson.Name, someStudent.Name, someWorker.Name;;
val it : string * string * string = ("Juliet", "Monique", "Carla")
> someStudent.StudentID;;
val it : int = 123456
> someWorker.Employer;;
val it : string = "Awesome Hair Salon"
> someWorker.ToString();; (* ToString method inherited from System.Object *)
val it : string = "FSI_0002+Worker"
.NET's object model supports single-class inheritance, meaning that a subclass is limited to one base class. In other words, its not possible to create a class which derives from Student
and Employee
simultaneously.
Overriding Methods
[edit | edit source]Occasionally, you may want a derived class to change the default behavior of methods inherited from the base class. For example, the output of the .ToString()
method above isn't very useful. We can override that behavior with a different implementation using the override
:
type Person(name) =
member x.Name = name
member x.Greet() = printfn "Hi, I'm %s" x.Name
override x.ToString() = x.Name (* The ToString() method is inherited from System.Object *)
We've overridden the default implementation of the ToString()
method, causing it to print out a person's name.
Methods in F# are not overridable by default. If you expect users will want to override methods in a derived class, you have to declare your method as overridable using the abstract
and default
keywords as follows:
type Person(name) =
member x.Name = name
abstract Greet : unit -> unit
default x.Greet() = printfn "Hi, I'm %s" x.Name
type Quebecois(name) =
inherit Person(name)
override x.Greet() = printfn "Bonjour, je m'appelle %s, eh." x.Name
Our class Person
provides a Greet
method which may be overridden in derived classes. Here's an example of these two classes in fsi:
> let terrance, phillip = new Person("Terrance"), new Quebecois("Phillip");;
val terrance : Person
val phillip : Quebecois
> terrance.Greet();;
Hi, I'm Terrance
val it : unit = ()
> phillip.Greet();;
Bonjour, je m'appelle Phillip, eh.
Abstract Classes
[edit | edit source]An abstract class is one which provides an incomplete implementation of an object, and requires a programmer to create subclasses of the abstract class to fill in the rest of the implementation. For example, consider the following:
[<AbstractClass>]
type Shape(position : Point) =
member x.Position = position
override x.ToString() =
sprintf "position = {%i, %i}, area = %f" position.X position.Y (x.Area())
abstract member Draw : unit -> unit
abstract member Area : unit -> float
The first thing you'll notice is the AbstractClass
attribute, which tells the compiler that our class has some abstract members. Additionally, you notice two abstract members, Draw
and Area
don't have an implementation, only a type signature.
We can't create an instance of Shape
because the class hasn't been fully implemented. Instead, we have to derive from Shape
and override the Draw
and Area
methods with a concrete implementation:
type Circle(position : Point, radius : float) =
inherit Shape(position)
member x.Radius = radius
override x.Draw() = printfn "(Circle)"
override x.Area() = Math.PI * radius * radius
type Rectangle(position : Point, width : float, height : float) =
inherit Shape(position)
member x.Width = width
member x.Height = height
override x.Draw() = printfn "(Rectangle)"
override x.Area() = width * height
type Square(position : Point, width : float) =
inherit Shape(position)
member x.Width = width
member x.ToRectangle() = new Rectangle(position, width, width)
override x.Draw() = printfn "(Square)"
override x.Area() = width * width
type Triangle(position : Point, sideA : float, sideB : float, sideC : float) =
inherit Shape(position)
member x.SideA = sideA
member x.SideB = sideB
member x.SideC = sideC
override x.Draw() = printfn "(Triangle)"
override x.Area() =
(* Heron's formula *)
let a, b, c = sideA, sideB, sideC
let s = (a + b + c) / 2.0
Math.Sqrt(s * (s - a) * (s - b) * (s - c) )
Now we have several different implementations of the Shape
class. We can experiment with these in fsi:
> let position = { X = 0; Y = 0 };;
val position : Point
> let circle, rectangle, square, triangle =
new Circle(position, 5.0),
new Rectangle(position, 2.0, 7.0),
new Square(position, 10.0),
new Triangle(position, 3.0, 4.0, 5.0);;
val triangle : Triangle
val square : Square
val rectangle : Rectangle
val circle : Circle
> circle.ToString();;
val it : string = "Circle, position = {0, 0}, area = 78.539816"
> triangle.ToString();;
val it : string = "Triangle, position = {0, 0}, area = 6.000000"
> square.Width;;
val it : float = 10.0
> square.ToRectangle().ToString();;
val it : string = "Rectangle, position = {0, 0}, area = 100.000000"
> rectangle.Height, rectangle.Width;;
val it : float * float = (7.0, 2.0)
Working With Subclasses
[edit | edit source]Up-casting and Down-casting
[edit | edit source]A type cast is an operation which changes the type of an object from one type to another. This is not the same as a map function, because a type cast does not return an instance of a new object, it returns the same instance of an object with a different type.
For example, let's say B
is a subclass of A
. If we have an instance of B
, we are able to cast as an instance of A
. Since A
is upward in the class hierarchy, we call this an up-cast. We use the :>
operator to perform upcasts:
> let regularString = "Hello world";;
val regularString : string
> let upcastString = "Hello world" :> obj;;
val upcastString : obj
> regularString.ToString();;
val it : string = "Hello world"
> regularString.Length;;
val it : int = 11
> upcastString.ToString();; (* type obj has a .ToString method *)
val it : string = "Hello world"
> upcastString.Length;; (* however, obj does not have Length method *)
upcastString.Length;; (* however, obj does not have Length method *)
-------------^^^^^^^
stdin(24,14): error FS0039: The field, constructor or member 'Length' is not defined.
Up-casting is considered "safe", because a derived class is guaranteed to have all of the same members as an ancestor class. We can, if necessary, go in the opposite direction: we can down-cast from an ancestor class to a derived class using the :?>
operator:
> let intAsObj = 20 :> obj;;
val intAsObj : obj
> intAsObj, intAsObj.ToString();;
val it : obj * string = (20, "20")
> let intDownCast = intAsObj :?> int;;
val intDownCast : int
> intDownCast, intDownCast.ToString();;
val it : int * string = (20, "20")
> let stringDownCast = intAsObj :?> string;; (* boom! *)
val stringDownCast : string
System.InvalidCastException: Unable to cast object of type 'System.Int32' to type 'System.String'.
at <StartupCode$FSI_0067>.$FSI_0067._main()
stopped due to error
Since intAsObj
holds an int
boxed as an obj
, we can downcast to an int
as needed. However, we cannot downcast to a string
because it is an incompatible type. Down-casting is considered "unsafe" because the error isn't detectable by the type-checker, so an error with a down-cast always results in a runtime exception.
Up-casting example
[edit | edit source]open System
type Point = { X : int; Y : int }
[<AbstractClass>]
type Shape() =
override x.ToString() =
sprintf "%s, area = %f" (x.GetType().Name) (x.Area())
abstract member Draw : unit -> unit
abstract member Area : unit -> float
type Circle(radius : float) =
inherit Shape()
member x.Radius = radius
override x.Draw() = printfn "(Circle)"
override x.Area() = Math.PI * radius * radius
type Rectangle(width : float, height : float) =
inherit Shape()
member x.Width = width
member x.Height = height
override x.Draw() = printfn "(Rectangle)"
override x.Area() = width * height
type Square(width : float) =
inherit Shape()
member x.Width = width
member x.ToRectangle() = new Rectangle(width, width)
override x.Draw() = printfn "(Square)"
override x.Area() = width * width
type Triangle(sideA : float, sideB : float, sideC : float) =
inherit Shape()
member x.SideA = sideA
member x.SideB = sideB
member x.SideC = sideC
override x.Draw() = printfn "(Triangle)"
override x.Area() =
(* Heron's formula *)
let a, b, c = sideA, sideB, sideC
let s = (a + b + c) / 2.0
Math.Sqrt(s * (s - a) * (s - b) * (s - c) )
let shapes =
[(new Circle(5.0) :> Shape);
(new Circle(12.0) :> Shape);
(new Square(10.5) :> Shape);
(new Triangle(3.0, 4.0, 5.0) :> Shape);
(new Rectangle(5.0, 2.0) :> Shape)]
(* Notice we have to cast each object as a Shape *)
let main() =
shapes
|> Seq.iter (fun x -> printfn "x.ToString: %s" (x.ToString()) )
main()
This program has the following types:
type Point =
{X: int;
Y: int;}
type Shape =
class
abstract member Area : unit -> float
abstract member Draw : unit -> unit
new : unit -> Shape
override ToString : unit -> string
end
type Circle =
class
inherit Shape
new : radius:float -> Circle
override Area : unit -> float
override Draw : unit -> unit
member Radius : float
end
type Rectangle =
class
inherit Shape
new : width:float * height:float -> Rectangle
override Area : unit -> float
override Draw : unit -> unit
member Height : float
member Width : float
end
type Square =
class
inherit Shape
new : width:float -> Square
override Area : unit -> float
override Draw : unit -> unit
member ToRectangle : unit -> Rectangle
member Width : float
end
type Triangle =
class
inherit Shape
new : sideA:float * sideB:float * sideC:float -> Triangle
override Area : unit -> float
override Draw : unit -> unit
member SideA : float
member SideB : float
member SideC : float
end
val shapes : Shape list
This program outputs:
x.ToString: Circle, area = 78.539816 x.ToString: Circle, area = 452.389342 x.ToString: Square, area = 110.250000 x.ToString: Triangle, area = 6.000000 x.ToString: Rectangle, area = 10.000000