F Sharp Programming/Classes

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.

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.

Before an object is created, its properties and functions should be defined. 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.

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:

letbob=newAccount(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:

The do keyword is used for post-constructor initialization. For example, to create an object which represents a stock, it is necessary to pass in the stock symbol and initialize the rest of the properties in our constructor:

openSystemopenSystem.NettypeStock(symbol:string)=classleturl="http://download.finance.yahoo.com/d/quotes.csv?s="+symbol+"&f=sl1d1t1c1ohgv&e=.csv"letmutable_symbol=String.Emptyletmutable_current=0.0letmutable_open=0.0letmutable_high=0.0letmutable_low=0.0letmutable_volume=0do(* We initialize our object in the do block *)letwebClient=newWebClient()(* Data comes back as a comma-separated list, so we split it on each comma *)letdata=webClient.DownloadString(url).Split([|','|])_symbol<-data.[0]_current<-floatdata.[1]_open<-floatdata.[5]_high<-floatdata.[6]_low<-floatdata.[7]_volume<-intdata.[8]memberx.Symbol=_symbolmemberx.Current=_currentmemberx.Open=_openmemberx.High=_highmemberx.Low=_lowmemberx.Volume=_volumeendletmain()=letstocks=["msft";"noc";"yhoo";"gm"]|>Seq.map(funx->newStock(x))stocks|>Seq.iter(funx->printfn"Symbol: %s (%F)"x.Symbolx.Current)main()

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:

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:

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 val fields can be used in the implicit syntax, they must have the attribute [<DefaultValue>] and be mutable. It is more convenient to use let bindings in this case. Public member accessors can be added 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, it is not necessary 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 constructortypeCar1(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 directlymemberx.Make=makememberx.Model=model// This is an extra constructor.// It calls the primary constructornew()=Car1("default make","default model")end

typeCar2=class// In this case, we need to declare// all fields and their types explicitly valprivatemake:stringvalprivatemodel:string// Notice how field access differs// from parameter accessmemberx.Make=x.makememberx.Model=x.model// Two constructorsnew(make:string,model:string)={make=makemodel=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.

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:

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.

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:

typeSomeClass(prop:int)=classmemberx.Prop=propstaticmemberSomeStaticMethod="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 *)valit: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):errorFS0191:property'Prop'isnotstatic.>letinstance=newSomeClass(5);;valinstance:SomeClass>instance.Prop;;(* now we have an instance, we can call our instance method *)valit:int=5>instance.SomeStaticMethod;;(* can't invoke static method from instance *)instance.SomeStaticMethod;;(* can't invoke static method from instance *)^^^^^^^^^^^^^^^^^^^^^^^^^^stdin(81,1):errorFS0191:property'SomeStaticMethod'isstatic.

typeSomeClass(prop:int)=classmemberx.Prop=propstaticmemberSomeStaticMethod="This is a static method"staticmemberCopy(source:SomeClass)=newSomeClass(source.Prop)end

We can experiment with this method in fsi:

>letinstance=newSomeClass(10);;valinstance:SomeClass>letshallowCopy=instance;;(* copies pointer to another symbol *)valshallowCopy:SomeClass>letdeepCopy=SomeClass.Copyinstance;;(* copies values into a new object *)valdeepCopy:SomeClass>openSystem;;>Object.ReferenceEquals(instance,shallowCopy);;valit:bool=true>Object.ReferenceEquals(instance,deepCopy);;valit: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 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:

typeIntWrapper()=classletmutablenum=0memberx.Numwithget()=numandset(value)=ifvalue>10||value<0thenraise(newException("Values must be between 0 and 10"))elsenum<-valueend

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.