Object Oriented F# - Creating Classes

In the past couple of posts, I covered extension everything in F#.  This allows me to extend .NET types with such things as extension static and instance methods, properties, properties with indexers, events and so on.  But, let's go back to the beginning and cover object oriented programming with F# from the ground up.  I like to stress that F# is not only a first class functional language, albeit a more impure one than say Haskell, but it also treats imperative and object oriented code as first class citizens as well.  To be able to mix and match for the appropriate programming style makes this a very powerful tool, to be able to use functional aspects with first class citizenship, but as well with imperative and object oriented, well, then the sky is the limit.  With that, let's go over some of the things that make F# a player in this space.

Let's get caught up to where we are so far:


Defining Classes

As I will point out several times, F# is a pretty flexible language.  With this, you will find that there are several ways of defining classes, whether it be explicit constructors, constructors in the type definition among other things.  Constructed classes in F# must follow the sytanx, with those items in the brackets being option, and the ones with the asterisk may appear zero to many times.

type TypeName optional-arguments [ as ident ] =
  [ inherit type { as base } ]
  [ let-binding | let-rec bindings ] *
  [ do-statement ] *
  [ abstract-binding | member-binding | interface-implementation ] *
 

From the above syntax, I can specify the inheritance, local functions and values (let syntax), verify parameters with the do syntax and member properties, methods and interface implementations. 

Defining a simple class is very straightforward.  Consider if I want to implement a person class and override the ToString() to output some meaningful input.  The code for implementing this is compact without a lot of pomp and circumstance such as this.

type Person = { FirstName:string; LastName:string; Age:int }
  with override x.ToString() =
    sprintf "%s %s is %d years old" x.FirstName x.LastName x.Age
      
let p1 = { FirstName = "Matthew"; LastName = "Podwysocki"; Age = 31 }
printfn "%s" (p1.ToString())

What's nice about this approach is that I can define these simple classes and once they are constructed, they are immutable by default.  Very nice for message construction and other operations.  But, if you want more behaviors inside your defined classes, then it's best to use a more explicit syntax as below.


Constructing Classes

Using the construction style, I can create simple value objects such as a Geocoordinate to hold a latitude and longitude as well as have the capability of calculating the distance between two instances of Geocoordinates.

type Geocoordinate(lat:float, lon:float) =
  
  let PI = System.Math.PI
  
  // Validate arguments
  do if lon > 180.0 || lon < -180.0 then invalid_arg "lon"
  do if lat > 90.0 || lat < -90.0 then invalid_arg "lat"
  
  // Properties
  member x.Latitude with get() = lat
  member x.Longitude with get() = lon
  
  // Methods
  static member private DegreesToRadians(deg) =
    deg * PI / 180.0
  
  static member GetDistance(g1:Geocoordinate, g2:Geocoordinate)
    let x = (sin(Geocoordinate.DegreesToRadians g1.Latitude)
             sin(Geocoordinate.DegreesToRadians g2.Latitude)
             cos(Geocoordinate.DegreesToRadians g1.Latitude)
             cos(Geocoordinate.DegreesToRadians g2.Latitude)
             cos(abs((Geocoordinate.DegreesToRadians g2.Longitude)
                     (Geocoordinate.DegreesToRadians g1.Longitude))))
    let acos_x = atan(sqrt(1.0 - pown x 2) / x)
    let distance = 1.852 * 60.0 * ((x / PI) * 180.0)
    distance
 

In the above class, I specified the default constructor to take in a lat and lon.  From there, I can use the do syntax to validate the parameters.  I expose the lat and lon parameters also as Latitude and Longitude member properties.  And then I specified a static instance method that calculates the distance between two instances of Geocoordinate classes.

Alternatively, I could specify the constructors manually instead of during the initial type definition by using the new keyword to define my constructors as well such as the following.

type Geocoordinate =
  
  val lat:float
  val lon:float
 
  new(lat:float, lon:float) = { lat = lat; lon = lon; }
    
  // Properties
  member x.Latitude with get() = x.lat
  member x.Longitude with get() = x.lon
  
  // Rest Ommitted

The first example is the preferred way as you can be more flexible while using the do and let syntax, whereas this above solution does not allow for this.  Instead, I would only focus on this style of constructor for structs only.  But the flexibility here gives you some latitude in terms of your particular coding style.

Code Reuse Through Modules

Another particularly powerful feature is the ability for code reuse by importing modules.  For example, had some of those functions been implemented in an external module, then I can easily reuse them by opening them.  In the above example, the DegreesToRadians and GetDistance functions could have been defined externally such as this.

module GeospatialModule =
  let PI = System.Math.PI
  
  let degrees_to_radians deg =
    deg * PI / 180.0
  
  let calculate_distance lat1 lon1 lat2 lon2 =
    let x = (sin(degrees_to_radians lat1)
             sin(degrees_to_radians lat2)
             cos(degrees_to_radians lat1)
             cos(degrees_to_radians lat2)
             cos(abs((degrees_to_radians lon2)-
                     (degrees_to_radians lon1))))
    let acos_x = atan(sqrt(1.0 - pown x 2) / x)
    let distance = 1.852 * 60.0 * ((x / PI) * 180.0)
    distance
 

And then I could implement the class in a much more concise format while using these defined functions.

open GeospatialModule

type Geocoordinate(lat:float, lon:float)
  // Validate arguments
  do if lon > 180.0 || lon < -180.0 then invalid_arg "lon" 
  do if lat > 90.0 || lat < -90.0 then invalid_arg "lat"   
  
  // Properties
  member x.Latitude with get() = lat
  member x.Longitude with get() = lon
  
  // Methods
  static member GetDistance(g1:Geocoordinate, g2:Geocoordinate)
    calculate_distance g1.Latitude g1.Longitude
                       g2.Latitude g2.Longitude
 

The code as written is much more compact than before as I have refactored my code to use functions from modules and cut down on the clutter inside my code.

Optional and Named Arguments

In F#, you'll find that the OOP constructs that F# supports are very much oriented towards API designers.  As such, there is a need to permit designers to specify named arguments and allow for the fact that some arguments are optional.  Named arguments are simply when creating an object, specify the name before the assignment operator such as the example below.  Using this style makes reading object initialization much more readable as you are not relying on argument position, instead by given name. 

let g1 = new Geocoordinate(lat = 12.0, lon = 34.0)

An optional parameter can be specified during the declaration by prefixing the argument name with a question mark (?).  This becomes translated to an Option<'a> type which allows for either some value or no value at all.  Should the value not be specified, you can set the default argument value through the let syntax.  Let's walk through a simple example of a simple class called TextBoxInfo.

type TextBoxInfo(?text:string, ?color:Color, ?font:Font) =
  // Initialize
  let text = defaultArg text ""
  let color = match color with
    | None -> Color.Red
    | Some c ->
  let font = match font with
    | None -> new Font(FontFamily.GenericSerif, 10.0f)
    | Some f -> f
    
  // Properties
  member x.Text = text
  member x.Color = color
  member x.Font = font
 

I specified default values for each should they not have been specified.  This allows me to specify only the arguments I need to do my operation.  Some examples of how to create a TextBoxInfo are below.

let tb1 = new TextBoxInfo("Hello textbox")
let tb2 = new TextBoxInfo(text = "Hello textbox"
                          color = Color.Aqua)
let tb3 = new TextBoxInfo(color = Color.Purple,
                          font = new Font(FontFamily.GenericMonospace, 10.0f))

This syntax makes my code a bit more concise as I don't have to input default values, and instead only concentrate on the things I actually need.  Named and optional arguments are features that C# has needed desperately for some time, especially in regards to the COM Interop story.


Wrapping It Up

This is the first in a series on telling the story of object oriented programming in F#.  As programmers, we can decide which language paradigm fits best for the application and the mix of functional, imperative and object oriented programming is essential in today's environments.  This series is intended on how you can mix and match the paradigms, but more importantly, make you aware of the choices you have.  Next up on the plate, we'll cover interfaces, operators, overloading and so on.



kick it on DotNetKicks.com

2 Comments

Comments have been disabled for this content.