Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / F#

F#23 : Generics

5.00/5 (1 vote)
2 May 2014CPOL4 min read 15.7K  
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.

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:

image

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:

image

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:

image

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:

// A generic record, with the type parameter in angle brackets. 
type GenericRecord = 
    {
        Field1: 'a;
        Field2: 'a;
    }

// A generic discriminated union. 
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

image

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

image

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:

//Dummy class to be used in some of the 
type Foo() =
    class end

//Dummy class to be used in some of the 
type Dimension =
    struct 
        val Width: float
        val Height: float
        new(w: float, h: float) = { Width = w; Height = h}
    end


type MyNotSoUsefulException() =
    inherit Exception()
            
// Base Type Constraint
type Class1 System.Exception> =
        val ex : 'T
        new(e : 'T) = { ex = e}


// Reference type constraint
type Class2 =
        val refField : 'T
        new(r : 'T) = { refField = r}

// Interface Type Constraint
type Class3 System.IComparable> = 
    class end

// Null constraint
type Class4 =
    class end

// Member constraint with static member
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
// elements, or be System.IntPtr or System.UIntPtr. Also, 'T must not have
// the NoComparison attribute.
type Class10 =
    class end

// 'T must support equality. This is true for any type that does not
// have the NoEquality attribute.
type Class11 =
    class end

type Class12<'T when 'T : delegate> =
    class end

type Class13 =
    class end
    
// If there are multiple constraints, use the and keyword to separate them.
type Class14 =
    class end

// Member constraints with two type parameters
// Most often used with static type parameters in inline functions
let inline add(value1 : ^T when ^T : (static member (+) : ^T * ^T -> ^T), value2: ^T) =
    value1 + value2

// ^T and ^U must support operator +
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()
            
// Base Type Constraint
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

image

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

//Dummy class to be used for demos
type Foo() =
    class end

//Dummy class to be used for demos
type Dimension =
    struct 
        val Width: float
        val Height: float
        new(w: float, h: float) = { Width = w; Height = h}
    end

// Reference type constraint
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

image

The other generic constraints all work in the same manner, you can have fun and try the others on your own

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)