Adventures in F# - F# 101 Part 7 (Creating Types)
Where We Are
Before we begin today, let's catch up to where we are today:
- Part 1 - Basic functional programming
- Part 2 - Currying and Tuples
- Part 3 - Scope, Recursion and Anonymous Functions
- Part 4 - History of F#, Operators and Lists
- Part 5 - Pattern Matching
- Part 6 - Lazy Evaluation
Types of Types?
If you're familiar with OCaml, the type system in F# should look rather familiar. For a brief introduction to how OCaml works, check out Dr. Jon Harrop's OCaml for Scientists introduction here. We have several categories of types in base F#. The first category is the tuples or records, and the second type are unions.
Tuples
The most simple type in F# is a tuple. A tuple is a compound type composed of a fixed sequence of other types. Each value is separated by a comma and can be referred to by a single identifier. The values from the tuple can be retrieved by putting commas on the other side of the equals. Below is a quick sample of this concept:
#light
let coin = "heads", "tails"
let c1, _ = coin
let _, c2 = coin
print_string c1
print_string c2
What I did to look up the values was to use the _ to indicate I wasn't interested in the other values. Hence my c1 got me heads and c2 got me tails. As always, I'm always curious how it compiles down to C# and how it gets represented in IL, so let's take a look at the coin type that I created.
Nothing really fancy, as we created a tuple of type string and string which held our "heads" and "tails" value. Like I said before, tuples are the simplest type in F#. In fact, as you saw above I didn't even need to use the type keyword to specify I was creating a new type. However, I can choose to use the type keyword so that I can create a type alias which can be very useful for method signature constraint. Below is a simple sample of doing that.
#light
type Point3D = double * double * double
let pointPrinter(p : Point3D) =
let x, y, z = p in
"x:" + x.ToString() + " y:" + y.ToString() + " z:" + z.ToString()
let pointString = pointPrinter(2.3, 5.3, 9.8)
print_string pointString
As you can see from above, I created a 3D point which contains an x, y and z axis. Then I constrained the pointPrinter function to take that as a constraint so that I can format it properly. Then of course I do all the great things I normally would do by printing it out to the console. But, if you want to take a look at what this pointPrinter function does behind the scenes, wonder no longer.
Quite an interesting mouthful, I must admit, but that's what you get when you deal with immutable types such as strings.
Records
Record types are also a user defined data structure within F#. These are similar to tuples, but differ because each piece of data (field) inside a record must be named as well as the keyword type must be used. Think of this much as a .NET class with simple properties. What's interesting is that these fields that it creates for us aren't forced to be unique. Let's go ahead and show a simple example of a Person record that we may want to create.
#light
open System
type Person =
{
FirstName : string;
MiddleName : string;
LastName : string;
DateOfBirth : DateTime;
}
let person1 = { FirstName="Robert"; MiddleName="William"; LastName="Jones"; DateOfBirth = new DateTime(1960, 12, 12); }
let person2 =
{ new Person
with FirstName = "William"
and MiddleName = "Franklin"
and LastName = "Smith"
and DateOfBirth = new DateTime(1970, 1, 23)
}
print_string person1.FirstName
print_string person2.FirstName
What I have done is created a Person record with a FirstName, MiddleName, LastName and a DateOfBirth field. Pretty simple. Now, we have two ways of creating an instance of this type. When I created person1, I just simply just set each field separated by a semicolon. On the other hand, since I'm not explicitly naming a type, there could be conflicts due to multiple types containing the same fields. Should I want to be more explicit, I can specify it using the longhand way with using the new operator and setting each property with the with keyword and followed by an and on each subsequent line to fill in all the other values. Such a conflict would look like this:
#light
type point2D = { X:double; Y:double }
type coordinate2D = { X:double; Y:double }
let myPoint = { X = 2.5; Y = 3.2; }
What the above sample will default to is that myPoint will be of type coordinate2D. Hover over and let Intellisense do its magic. Interesting, huh? That's why we should be explicit about our data structures should something like this arise.
Discriminated Unions
The next category of F# type that will be discussed is the discriminated union. This type is a data structure that can bring together values but have different meanings or structures. Only one of these types can be used at once, however. Think of it more of a type which is almost "case" like. Each piece of that is called a discriminator. A quick example of this would be something like distance where it could be measured in miles or kilometers, but not both at the same time. The structure of this data is the same, but have different meanings to the program.
Let's look at some quick examples of this:
#light
type Distance =
| Kilometer of double
| Mile of double
let distance1 = Mile 26.2
let distance2 = Kilometer 10.0
let convertDistanceToMile x =
match x with
| Mile x -> x
| Kilometer x -> x * 1.609344
let convertDistanceToKilometer x =
match x with
| Mile x -> x * 0.621371192
| Kilometer x -> x
let convertedDistance1 = convertDistanceToKilometer distance1
let convertedDistance2 = convertDistanceToMile distance2
As you can see from above, I created a Distance discriminated union that defines both miles and kilometers. Both discriminators are of type double. So, I wrote a function to convert from one to the other. It's just a simple pattern matching statement. If you've read my previous posts, you should be caught up to date with this.
I could also parameterize this data differently, meaning that each discriminator can hold a different value.
#light
type Carrier = string
type Route = int
type Make = string
type Model = string
type Year = int
type ModeOfTransport =
| Airplane of Carrier * Route
| Bus of Carrier * Route
| Car of Make * Model
let mode1 = Car("Audi", "A4")
let mode2 = Airplane("United", 222)
As you can see, I type aliased a few things such as Carrier, Route, Make, Model and so on. After that, I defined my ModeOfTransport to be by bus, car or airplane. A pretty simple example.
I can also specify the type of arguments later, by specifying them as generics. One of them you may have already run across is the option type. To declare one, just use the <'a> or whichever letter you so choose. Let do a recursive example of a tree.
#light
type Tree<'t> =
| TreeNode of 't Tree * 't Tree
| TreeValue of 't
let tree =
TreeNode(
TreeNode(TreeValue 0, TreeValue 1),
TreeNode(TreeValue 2, TreeValue 3))
From the above statement, I created a tree of nodes quite easily using this union type. These types can get complicated quickly, however when you get into language oriented programming, so we're just dipping our toes in at the moment.
.NET Types?
Yes, you can also create .NET types as well just as easily, although with a few major details. For example, the mutable types which is not by default in F#. I'll cover that in the next post.
Conclusion
As you can see, there are a few basic types in F#. They are mostly for holding analytical data, traversing them and so on. These are interesting and serve as the basis of what you will do in F# when you need to store and analyze data, especially custom types. In the next installment, I hope to cover some .NET specific topics such as creating .NET types. Until next time...