F Sharp Programming/Classes

From Wikibooks, open books for an open world
< F Sharp Programming
Jump to: navigation, search
Previous: Operator Overloading Index Next: Inheritance
F# : Classes and Objects


In the real world, an object is a "real" thing. A cat, person, computer, and a roll of duct tape are all "real" things in the tangible sense. When we think about these things, we can broadly describe them in terms of a number of attributes:

  • Properties: a person has a name, a cat has four legs, computers have a price tag, duct tape is sticky.
  • Behaviors: a person reads the newspaper, cats sleep all day, computers crunch numbers, duct tape attaches things to other things.
  • Types/group membership: an employee is a type of person, a cat is a pet, a Dell and Mac are types of computers, duct tape is part of the broader family of adhesives.

In the programming world, an "object" is, in the simplest of terms, a model of something in the real world. Object-oriented programming (OOP) exists because it allows programmers to model real-world entities and simulate their interactions in code. Just like their real-world counterparts, objects in computer programming have properties and behaviors, and can be classified according to their type.

While we can certainly create objects that represents cats, people, and adhesives, objects can also represent less concrete things, such as a bank account or a business rule.

Although the scope of OOP has expanded to include some advanced concepts such as design patterns and the large-scale architecture of applications, this page will keep things simple and limit the discussion of OOP to data modeling.

Defining an Object[edit]

Before you create an object, you have to identify the properties of your object and describe what it does. You define properties and methods of an object in a class. There are actually two different syntaxes for defining a class: an implicit syntax and an explicit syntax.

Implicit Class Construction[edit]

Implicit class syntax is defined as follows:

type TypeName optional-type-arguments arguments [ as ident ] =
    [ inherit type { as base } ]
    [ let-binding | let-rec bindings ] *
    [ do-statement ] *
    [ abstract-binding | member-binding | interface-implementation ] *
Elements in brackets are optional, elements followed by a * may appear zero or more times.

This syntax above is not as daunting as it looks. Here's a simple class written in implicit style:

type Account(number : int, holder : string) = class
    let mutable amount = 0m
 
    member x.Number = number
    member x.Holder = holder
    member x.Amount = amount
 
    member x.Deposit(value) = amount <- amount + value
    member x.Withdraw(value) = amount <- amount - value
end

The code above defines a class called Account, which has three properties and two methods. Let's take a closer look at the following:

type Account(number : int, holder : string) = class

The underlined code is called the class constructor. A constructor is a special kind of function used to initialize the fields in an object. In this case, our constructor defines two values, number and holder, which can be accessed anywhere in our class. You create an instance of Account by using the new keyword and passing the appropriate parameters into the constructor as follows:

let bob = new Account(123456, "Bob’s Saving")

Additionally, let's look at the way a member is defined:

member x.Deposit(value) = amount <- amount + value

The x above is an alias for the object currently in scope. Most OO languages provide an implicit this or self variable to access the object in scope, but F# requires programmers to create their own alias.

After we can create an instance of our Account, we can access its properties using .propertyName notation. Here's an example in FSI:

> let printAccount (x : Account) =
    printfn "x.Number: %i, x.Holder: %s, x.Amount: %M" x.Number x.Holder x.Amount;;
 
val printAccount : Account -> unit
 
> let bob = new Account(123456, "Bob’s Savings");;
 
val bob : Account
 
> printAccount bob;;
x.Number: 123456, x.Holder: Bob’s Savings, x.Amount: 0
val it : unit = ()
 
> bob.Deposit(100M);;
val it : unit = ()
 
> printAccount bob;;
x.Number: 123456, x.Holder: Bob’s Savings, x.Amount: 100
val it : unit = ()
 
> bob.Withdraw(29.95M);;
val it : unit = ()
 
> printAccount bob;;
x.Number: 123456, x.Holder: Bob’s Savings, x.Amount: 70.05

Example[edit]

Let's use this class in a real program:

open System
 
type Account(number : int, holder : string) = class
    let mutable amount = 0m
 
    member x.Number = number
    member x.Holder = holder
    member x.Amount = amount
 
    member x.Deposit(value) = amount <- amount + value
    member x.Withdraw(value) = amount <- amount - value
end
 
let homer = new Account(12345, "Homer")
let marge = new Account(67890, "Marge")
 
let transfer amount (source : Account) (target : Account) =
    source.Withdraw amount
    target.Deposit amount
 
let printAccount (x : Account) =
    printfn "x.Number: %i, x.Holder: %s, x.Amount: %M" x.Number x.Holder x.Amount
 
let main() =
    let printAccounts() =
        [homer; marge] |> Seq.iter printAccount
 
    printfn "\nInializing account"
    homer.Deposit 50M
    marge.Deposit 100M
    printAccounts()
 
    printfn "\nTransferring $30 from Marge to Homer"
    transfer 30M marge homer
    printAccounts()
 
    printfn "\nTransferring $75 from Homer to Marge"
    transfer 75M homer marge
    printAccounts()
 
main()

The program has the following types:

type Account =
  class
    new : number:int * holder:string -> Account
    member Deposit : value:decimal -> unit
    member Withdraw : value:decimal -> unit
    member Amount : decimal
    member Holder : string
    member Number : int
  end
val homer : Account
val marge : Account
val transfer : decimal -> Account -> Account -> unit
val printAccount : Account -> unit

The program outputs the following:

Initializing account
x.Number: 12345, x.Holder: Homer, x.Amount: 50
x.Number: 67890, x.Holder: Marge, x.Amount: 100

Transferring $30 from Marge to Homer
x.Number: 12345, x.Holder: Homer, x.Amount: 80
x.Number: 67890, x.Holder: Marge, x.Amount: 70

Transferring $75 from Homer to Marge
x.Number: 12345, x.Holder: Homer, x.Amount: 5
x.Number: 67890, x.Holder: Marge, x.Amount: 145

Example using the do keyword[edit]

The do keyword is used for post-constructor initialization. For example, let's say we wanted to create an object which represents a stock. We only need to pass in the stock symbol, and initialize the rest of the properties in our constructor:

open System
open System.Net
 
type Stock(symbol : string) = class
    let url =
        "http://download.finance.yahoo.com/d/quotes.csv?s=" + symbol + "&f=sl1d1t1c1ohgv&e=.csv"
 
    let mutable _symbol = String.Empty
    let mutable _current = 0.0
    let mutable _open = 0.0
    let mutable _high = 0.0
    let mutable _low = 0.0
    let mutable _volume = 0
 
    do
	(* We initialize our object in the do block *)
 
        let webClient = new WebClient()
 
       	(* Data comes back as a comma-seperated list, so we split it
           on each comma *)
        let data = webClient.DownloadString(url).Split([|','|])
 
        _symbol <- data.[0]
        _current <- float data.[1]
        _open <- float data.[5]
        _high <- float data.[6]
        _low <- float data.[7]
        _volume <- int data.[8]
 
    member x.Symbol = _symbol
    member x.Current = _current
    member x.Open = _open
    member x.High = _high
    member x.Low = _low
    member x.Volume = _volume
end
 
let main() =
    let stocks = 
        ["msft"; "noc"; "yhoo"; "gm"]
        |> Seq.map (fun x -> new Stock(x))
 
    stocks |> Seq.iter (fun x -> printfn "Symbol: %s (%F)" x.Symbol x.Current)
 
main()

This program has the following types:

type Stock =
  class
    new : symbol:string -> Stock
    member Current : float
    member High : float
    member Low : float
    member Open : float
    member Symbol : string
    member Volume : int
  end

This program outputs the following (your outputs will vary):

Symbol: "MSFT" (19.130000)
Symbol: "NOC" (43.240000)
Symbol: "YHOO" (12.340000)
Symbol: "GM" (3.660000)
Note: It's possible to have any number of do statements in a class definition, although there's no particular reason why you'd need more than one.

Explicit Class Definition[edit]

Classes written in explicit style follow this format:

type TypeName =
    [ inherit type ]
    [ val-definitions ]
    [ new ( optional-type-arguments arguments ) [ as ident ] =
      { field-initialization }
      [ then constructor-statements ]
    ] *
    [ abstract-binding | member-binding | interface-implementation ] *

Here's a class defined using the explicit syntax:

type Line = class
    val X1 : float
    val Y1 : float
    val X2 : float
    val Y2 : float
 
    new (x1, y1, x2, y2) =
        { X1 = x1; Y1 = y1;
            X2 = x2; Y2 = y2}
 
    member x.Length =
        let sqr x = x * x
        sqrt(sqr(x.X1 - x.X2) + sqr(x.Y1 - x.Y2) )
end

Each val defines a field in our object. Unlike other object-oriented languages, F# does not implicitly initialize fields in a class to any value. Instead, F# requires programmers to define a constructor and explicitly initialize each field in their object with a value.

We can perform some post-constructor processing using a then block as follows:

type Line = class
    val X1 : float
    val Y1 : float
    val X2 : float
    val Y2 : float
 
    new (x1, y1, x2, y2) as this =
        { X1 = x1; Y1 = y1;
            X2 = x2; Y2 = y2;}
        then
            printfn "Line constructor: {(%F, %F), (%F, %F)}, Line.Length: %F"
                this.X1 this.Y1 this.X2 this.Y2 this.Length
 
    member x.Length =
        let sqr x = x * x
        sqrt(sqr(x.X1 - x.X2) + sqr(x.Y1 - x.Y2) )
end

Notice that we have to add an alias after our constructor, new (x1, y1, x2, y2) as this), to access the fields of our object being constructed. Each time we create a Line object, the constructor will print to the console. We can demonstrate this using fsi:

> let line = new Line(1.0, 1.0, 4.0, 2.5);;
 
val line : Line
 
Line constructor: {(1.000000, 1.000000), (4.000000, 2.500000)}, Line.Length: 3.354102


Example Using Two Constructors[edit]

Since the constructor is defined explicitly, we have the option to provide more than one constructor.

open System
open System.Net
 
type Car = class
    val used : bool
    val owner : string
    val mutable mileage : int
 
    (* first constructor *)
    new (owner) =
        { used = false;
            owner = owner;
            mileage = 0 }
 
    (* another constructor *)
    new (owner, mileage) =
        { used = true;
            owner = owner;
            mileage = mileage }
end
 
let main() =
    let printCar (c : Car) =
        printfn "c.used: %b, c.owner: %s, c.mileage: %i" c.used c.owner c.mileage
 
    let stevesNewCar = new Car("Steve")
    let bobsUsedCar = new Car("Bob", 83000)
    let printCars() =
        [stevesNewCar; bobsUsedCar] |> Seq.iter printCar
 
    printfn "\nCars created"
    printCars()
 
    printfn "\nSteve drives all over the state"   
    stevesNewCar.mileage <- stevesNewCar.mileage + 780
    printCars()
 
    printfn "\nBob commits odometer fraud"
    bobsUsedCar.mileage <- 0
    printCars()
 
main()

This program has the following types:

type Car =
  class
    val used: bool
    val owner: string
    val mutable mileage: int
    new : owner:string * mileage:int -> Car
    new : owner:string -> Car
  end

Notice that our val fields are included in the public interface of our class definition.

This program outputs the following:

Cars created
c.used: false, c.owner: Steve, c.mileage: 0
c.used: true, c.owner: Bob, c.mileage: 83000

Steve drives all over the state
c.used: false, c.owner: Steve, c.mileage: 780
c.used: true, c.owner: Bob, c.mileage: 83000

Bob commits odometer fraud
c.used: false, c.owner: Steve, c.mileage: 780
c.used: true, c.owner: Bob, c.mileage: 0

Differences Between Implicit and Explicit Syntaxes[edit]

As you've probably guessed, the major difference between the two syntaxes is related to the constructor: the explicit syntax forces a programmer to provide explicit constructor(s), whereas the implicit syntax fuses the primary constructor with the class body. However, there are a few other subtle differences:

  • The explicit syntax does not allow programmers to declare let and do bindings.
  • Even though you can use val fields in the implicit syntax, they must have the attribute [<DefaultValue>] and be mutable. It is more convenient to use let bindings in this case. You can add public member accessors when they need to be public.
  • In the implicit syntax, the primary constructor parameters are visible throughout the whole class body. By using this feature, you do not need to write code that copies constructor parameters to instance members.
  • While both syntaxes support multiple constructors, when you declare additional constructors with the implicit syntax, they must call the primary constructor. In the explicit syntax all constructors are declared with new() and there is no primary constructor that needs to be referenced from others.
Class with primary (implicit) constructor Class with only explicit constructors
// The class body acts as a constructor
type Car1(make : string, model : string) = class
    // x.Make and x.Model are property getters
    // (explained later in this chapter)
    // Notice how they can access the
    // constructor parameters directly
    member x.Make = make
    member x.Model = model
 
    // This is an extra constructor.
    // It calls the primary constructor
    new () = Car1("default make", "default model")
end
type Car2 = class
    // In this case, we need to declare
    // all fields and their types explicitly 
    val private make : string
    val private model : string
 
    // Notice how field access differs
    // from parameter access
    member x.Make = x.make
    member x.Model = x.model
 
    // Two constructors
    new (make : string, model : string) = {
        make = make
        model = model
    }
    new () = {
        make = "default make"
        model = "default model"
    }
end

In general, its up to the programmer to use the implicit or explicit syntax to define classes. However, the implicit syntax is used more often in practice as it tends to result in shorter, more readable class definitions.

Class Inference[edit]

F#'s #light syntax allows programmers to omit the class and end keywords in class definitions, a feature commonly referred to as class inference or type kind inference. For example, there is no difference between the following class definitions:

Class Inference Class Explicit
type Product(make : string, model : string) =
    member x.Make = make
    member x.Model = model
type Car(make : string, model : string) = class    
    member x.Make = make
    member x.Model = model
end

Both classes compile down to the same bytecode, but the code using class inference allows us to omit a few unnecessary keywords.

Class inference and class explicit styles are considered acceptable. At the very least, when writing F# libraries, don't define half of your classes using class inference and the other half using class explicit style -- pick one style and use it consistently for all of your classes throughout your project.

Class Members[edit]

Instance and Static Members[edit]

There are two types of members you can add to an object:

  • Instance members, which can only be called from an object instance created using the new keyword.
  • Static members, which are not associated with any object instance.

The following class has a static method and an instance method:

type SomeClass(prop : int) = class
    member x.Prop = prop
    static member SomeStaticMethod = "This is a static method"
end

We invoke a static method using the form className.methodName. We invoke instance methods by creating an instance of our class and calling the methods using classInstance.methodName. Here is a demonstration in fsi:

> SomeClass.SomeStaticMethod;; (* invoking static method *)
val it : string = "This is a static method"
 
> SomeClass.Prop;; (* doesn't make sense, we haven't created an object instance yet *)
 
  SomeClass.Prop;; (* doesn't make sense, we haven't created an object instance yet *)
  ^^^^^^^^^^^^^^^
 
stdin(78,1): error FS0191: property 'Prop' is not static.
 
> let instance = new SomeClass(5);;
 
val instance : SomeClass
 
> instance.Prop;; (* now we have an instance, we can call our instance method *)
val it : int = 5
 
> instance.SomeStaticMethod;; (* can't invoke static method from instance *)
 
  instance.SomeStaticMethod;; (* can't invoke static method from instance *)
  ^^^^^^^^^^^^^^^^^^^^^^^^^^
 
stdin(81,1): error FS0191: property 'SomeStaticMethod' is static.

We can, of course, invoke instance methods from objects passed into static methods, for example, let's say we add a Copy method to our object defined above:

type SomeClass(prop : int) = class
    member x.Prop = prop
    static member SomeStaticMethod = "This is a static method"
    static member Copy (source : SomeClass) = new SomeClass(source.Prop)
end

We can experiment with this method in fsi:

> let instance = new SomeClass(10);;
 
val instance : SomeClass
 
> let shallowCopy = instance;; (* copies pointer to another symbol *)
 
val shallowCopy : SomeClass
 
> let deepCopy = SomeClass.Copy instance;; (* copies values into a new object *)
 
val deepCopy : SomeClass
 
> open System;;
 
> Object.ReferenceEquals(instance, shallowCopy);;
val it : bool = true
 
> Object.ReferenceEquals(instance, deepCopy);;
val it : bool = false

Object.ReferenceEquals is a static method on the System.Object class which determines whether two objects instances are the same object. As shown above, our Copy method takes an instance of SomeClass and accesses its Prop property.

When should you use static methods rather than instance methods?

When the designers of the .NET framework were designing the System.String class, they had to decide where the Length method should go. They had the option of making the property an instance method (s.Length) or making it static (String.GetLength(s)). The .NET designers chose to make Length an instance method because it is an intrinsic property of strings.

On the other hand, the String class also has several static methods, including String.Concat which takes a list of string and concatenates them all together. Concatenating strings is instance-agnostic, its does not depend on the instance members of any particular strings.

The following general principles apply to all OO languages:

  • Instance members should be used to access properties intrinsic to an object, such as stringInstance.Length.
  • Instance methods should be used when they depend on state of a particular object instance, such as stringInstance.Contains.
  • Instance methods should be used when its expected that programmers will want to override the method in a derived class.
  • Static methods should not depend on a particular instance of an object, such as Int32.TryParse.
  • Static methods should return the same value as long as the inputs remain the same.
  • Constants, which are values that don't change for any class instance, should be declared as a static members, such as System.Boolean.TrueString.

Getters and Setters[edit]

Getters and setters are special functions which allow programmers to read and write to members using a convenient syntax. Getters and setters are written using this format:

    member alias.PropertyName
        with get() = some-value
        and set(value) = some-assignment

Here's a simple example using fsi:

> type IntWrapper() = class
    let mutable num = 0
 
    member x.Num
        with get() = num
        and set(value) = num <- value
end;;
 
type IntWrapper =
  class
    new : unit -> IntWrapper
    member Num : int
    member Num : int with set
  end
 
> let wrapper = new IntWrapper();;
 
val wrapper : IntWrapper
 
> wrapper.Num;;
val it : int = 0
 
> wrapper.Num <- 20;;
val it : unit = ()
 
> wrapper.Num;;
val it : int = 20

Getters and setters are used to expose private members to outside world. For example, our Num property allows users to read/write to the internal num variable. Since getters and setters are glorified functions, we can use them to sanitize input before writing the values to our internal variables. For example, we can modify our IntWrapper class to constrain our to values between 0 and 10 by modifying our class as follows:

type IntWrapper() = class
    let mutable num = 0
 
    member x.Num
        with get() = num
        and set(value) =
            if value > 10 || value < 0 then
                raise (new Exception("Values must be between 0 and 10"))
            else
                num <- value
end

We can use this class in fsi:

> let wrapper = new IntWrapper();;
 
val wrapper : IntWrapper
 
> wrapper.Num <- 5;;
val it : unit = ()
 
> wrapper.Num;;
val it : int = 5
 
> wrapper.Num <- 20;;
System.Exception: Values must be between 0 and 10
   at FSI_0072.IntWrapper.set_Num(Int32 value)
   at <StartupCode$FSI_0076>.$FSI_0076._main()
stopped due to error

Adding Members to Records and Unions[edit]

Its just as easy to add members to records and union types as well.

Record example:

> type Line =
    { X1 : float; Y1 : float;
        X2 : float; Y2 : float }
    with    
        member x.Length =
            let sqr x = x * x
            sqrt(sqr(x.X1 - x.X2) + sqr(x.Y1 - x.Y2))
 
        member x.ShiftH amount =
            { x with X1 = x.X1 + amount; X2 = x.X2 + amount }
 
        member x.ShiftV amount =
            { x with Y1 = x.Y1 + amount; Y2 = x.Y2 + amount };;
 
type Line =
  {X1: float;
   Y1: float;
   X2: float;
   Y2: float;}
  with
    member ShiftH : amount:float -> Line
    member ShiftV : amount:float -> Line
    member Length : float
  end
 
> let line = { X1 = 1.0; Y1 = 2.0; X2 = 5.0; Y2 = 4.5 };;
 
val line : Line
 
> line.Length;;
val it : float = 4.716990566
 
> line.ShiftH 10.0;;
val it : Line = {X1 = 11.0;
                 Y1 = 2.0;
                 X2 = 15.0;
                 Y2 = 4.5;}
 
> line.ShiftV -5.0;;
val it : Line = {X1 = 1.0;
                 Y1 = -3.0;
                 X2 = 5.0;
                 Y2 = -0.5;}

Union example

> type shape =
    | Circle of float
    | Rectangle of float * float
    | Triangle of float * float
    with
        member x.Area =
            match x with
            | Circle(r) -> Math.PI * r * r
            | Rectangle(b, h) -> b * h
            | Triangle(b, h) -> b * h / 2.0
 
        member x.Scale value =
            match x with
            | Circle(r) -> Circle(r + value)
            | Rectangle(b, h) -> Rectangle(b + value, h + value)
            | Triangle(b, h) -> Triangle(b + value, h + value);;
 
type shape =
  | Circle of float
  | Rectangle of float * float
  | Triangle of float * float
  with
    member Scale : value:float -> shape
    member Area : float
  end
 
> let mycircle = Circle(5.0);;
 
val mycircle : shape
 
> mycircle.Area;;
val it : float = 78.53981634
 
> mycircle.Scale(7.0);;
val it : shape = Circle 12.0

Generic classes[edit]

We can also create classes which take generic types:

type 'a GenericWrapper(initialVal : 'a) = class
    let mutable internalVal = initialVal
 
    member x.Value
        with get() = internalVal
        and set(value) = internalVal <- value
end

We can use this class in FSI as follows:

> let intWrapper = new GenericWrapper<_>(5);;
 
val intWrapper : int GenericWrapper
 
> intWrapper.Value;;
val it : int = 5
 
> intWrapper.Value <- 20;;
val it : unit = ()
 
> intWrapper.Value;;
val it : int = 20
 
> intWrapper.Value <- 2.0;; (* throws an exception *)
 
  intWrapper.Value <- 2.0;; (* throws an exception *)
  --------------------^^^^
 
stdin(156,21): error FS0001: This expression has type
	float
but is here used with type
	int.
 
> let boolWrapper = new GenericWrapper<_>(true);;
 
val boolWrapper : bool GenericWrapper
 
> boolWrapper.Value;;
val it : bool = true

Generic classes help programmers generalize classes to operate on multiple different types. They are used in fundamentally the same way as all other generic types already seen in F#, such as Lists, Sets, Maps, and union types.

Pattern Matching Objects[edit]

While it's not possible to match objects based on their structure in quite the same way that we do for lists and union types, F# allows programmers to match on types using the syntax:

match arg with
| :? type1 -> expr
| :? type2 -> expr

Here's an example which uses type testing:

type Cat() = class
    member x.Meow() = printfn "Meow"
end
 
type Person(name : string) = class
    member x.Name = name
    member x.SayHello() = printfn "Hi, I'm %s" x.Name
end
 
type Monkey() = class
    member x.SwingFromTrees() = printfn "swinging from trees"
end
 
let handlesAnything (o : obj) =
    match o with
    | null -> printfn "<null>"
    | :? Cat as cat -> cat.Meow()
    | :? Person as person -> person.SayHello()
    | _ -> printfn "I don't recognize type '%s'" (o.GetType().Name)
 
let main() =
    let cat = new Cat()
    let bob = new Person("Bob")
    let bill = new Person("Bill")
    let phrase = "Hello world!"
    let monkey = new Monkey()
 
    handlesAnything cat
    handlesAnything bob
    handlesAnything bill
    handlesAnything phrase
    handlesAnything monkey
    handlesAnything null
 
main()

This program outputs:

Meow
Hi, I'm Bob
Hi, I'm Bill
I don't recognize type 'String'
I don't recognize type 'Monkey'
<null>
Previous: Operator Overloading Index Next: Inheritance