So we are about 1/2 way through the OO segment of our F# journey, we just have this article and one other to do. This one will focus on generics support in F#, we shall be covering the following topics
- Implicit Generics
- Explicit Generics
- Using Generics In Various Types
- Generic Constraints
I should just point out that I will not have time (or the will power) to go through all the generic constraints that F# supports, but I will try my best to show the basics.
So without further ado, shall we get on with the post at hand
Implicit Generics
In F# the type inference system is so good that some of the time you will not even need to tell the type system that an argument is generic. Essentially where ever the F# type system can treat a function as generic it will do so. Consider the following 2 examples that I have typed into the FSI window
let someFunction(x) = x
let makeList a b = [a; b]
If we examine these 2 functions in the FSI window, this is what we would get for the 2 functions above:
val makeList : a:'a -> b:'a -> 'a list
val someFunction : x:'a -> 'a
It can be seen that although I did not actually ask for he arguments to be generic they were inferred to be generic ones. You can of course implicitly say that you wish a certain parameter to be generic, which is done using the single quotation syntax which is typically “’a
” to show that a type is expected to be a generic one.
Here are those to functions rewritten to expect generic parameters:
let someFunction(x : 'a) = x
let makeList (a : 'a) (b : 'a) = [a; b]
do printfn "%A" (someFunction 12)
do printfn "%A" (someFunction "cat")
do printfn "%A" (makeList 1 2)
do printfn "%A" (makeList "cat" "dog")
It can be seen that I am now able to use the 2 demo functions to accept Ints, or Strings. Here is what it looks like when I run the above code:
Explicit Generics
It is also possible to create explicit generic functions/types. This is done using the angle bracket syntax “<T>
”, which would undoubtedly be familiar to any C# developers reading this. Here is are the 2 previous examples rewritten to use explicit generics. I have also included a 3rd function which expects 2 generic parameters which are simply printed to the console.
let someFunction (x : 'a) = x
let makeList (a:'a) (b:'a) = [a; b]
let printGenerics (x:'a) (y:'b) =
printfn "x = %A, y = %A" x y
It can now be seen if we try and use this these functions with a particular generic type specified in the generic type “<T>
” but supply some other type, we will get a compilation error:
If we however play nice, and pass in the correct types expected, we do indeed get the correct results:
do printfn "%A" (someFunction 12)
do printfn "%A" (someFunction "cat")
do printfn "%A" (makeList 1 2)
do printfn "%A" (makeList "cat" "dog")
printGenerics 1 2
printGenerics 1 "cat"
Which gives the following results:
Using Generics In Various Types
I mentioned earlier that you can also use generics in other types such as records, and unions. Here are some very trivial examples of how you could declare record and unions that are generic:
type GenericRecord =
{
Field1: 'a;
Field2: 'a;
}
type GenericUnion =
| Choice1 of 'a
| Choice2 of 'a * 'a
And here are some examples usages of these:
let rec1 = { Field1 = "ABC" ; Field2 = "XYZ" }
let rec2 = { Field1 = 12 ; Field2 = 45 }
let genericUnion1 = Choice1(12)
let genericUnion2 = Choice1(4343.0)
let genericUnion3 = Choice1("122.33")
let genericUnion4 = Choice2(12, 15)
let genericUnion5 = Choice2(2.0, 4.0)
let genericUnion6 = Choice2("1","2")
Which when run gives the following output
Generic Constraints
We now move on to look at generic constraints in F#. Generic constraints are used when you work with generic types to limit what may be allowed as generic type. Constraints may sometimes be required, as without them the compiler would have no way of verifying the features that are available on the generic type until runtime.
Most commonly this would be to say that the generic type inherits from a certain base class or implements a certain interface, but in F# there is a very good generic constraint system that allows us to constraint quite a few things.
I have shamelessly stolen this table and the matching generic constraints from MSDN : http://msdn.microsoft.com/en-us/library/dd233203.aspx
As you can see there is quite a lot you can do with generic constraints in F#. Let’s now see what this looks like when used in some actual code:
type Foo() =
class end
type Dimension =
struct
val Width: float
val Height: float
new(w: float, h: float) = { Width = w; Height = h}
end
type MyNotSoUsefulException() =
inherit Exception()
type Class1 System.Exception> =
val ex : 'T
new(e : 'T) = { ex = e}
type Class2 =
val refField : 'T
new(r : 'T) = { refField = r}
type Class3 System.IComparable> =
class end
type Class4 =
class end
type Class5 'T) > =
class end
// Member constraint with instance member
type Class6 int)> =
class end
// Member constraint with property
type Class7 =
class end
// Constructor constraint
type Class8 'T)>() =
member val Field = new 'T()
// Enumeration constraint with underlying value specified
type Class9<'T when 'T : enum> =
class end
// 'T must implement IComparable, or be an array type with comparable
type Class10 =
class end
type Class11 =
class end
type Class12<'T when 'T : delegate> =
class end
type Class13 =
class end
type Class14 =
class end
let inline add(value1 : ^T when ^T : (static member (+) : ^T * ^T -> ^T), value2: ^T) =
value1 + value2
let inline heterogenousAdd(value1 : ^T when (^T or ^U) : (static member (+) : ^T * ^U -> ^T), value2 : ^U) =
value1 + value2
Let's have a look at one or 2 of them.
Class 1
Class 1 from the list above should only be able to accept Exception derived classes. Here is the full code for it
type MyNotSoUsefulException() =
inherit Exception()
type Class1 System.Exception> =
val ex : 'T
new(e : 'T) = { ex = e}
Which as we can see will not compile if we try and use a non Exception derived type, but will compile if we do
Class 2
Class 2 from the list above should only be able to reference types, no structs should be allowed. Here is the full code for it
type Foo() =
class end
type Dimension =
struct
val Width: float
val Height: float
new(w: float, h: float) = { Width = w; Height = h}
end
type Class2 =
val refField : 'T
new(r : 'T) = { refField = r}
Which as we can see will not compile if we try and use a non reference type, but will compile if we do
The other generic constraints all work in the same manner, you can have fun and try the others on your own