Fundamentals of Generics

Introduction to Generic Functions

Consider the following function:

let display value = printfn "%A" value;

This function takes a value as argument and displays it. Here are examples of calling that function:

let display value = printfn "%A" value;

let a = 25
display a
let b = 48.07
display b
let c = true
display c
let d = "Movie Production"
display d
let e = (2, 6)
display e

This would produce:

25
48.07
true
"Movie Production"
(2, 6)
Press any key to close this window . . .

The function doesn't specify the type of value with which it is dealing. This means that any value can be passed to the function. In fact, this function becomes aware of the type of its parameter only when the function is called.

A generic function is one that involves one or more parameters without specifying the type of value the parameter(s) is using but still carrying its normal operation(s). In other words, at the time the function is created, it specifies its operation or role but doesn't restrict it to one particular type. Only when the function is called is the desired type specified.

Passing Generic Arguments

We already know that you can specify the data type of a parameter by following its name with a colon and the desired type, all of that in parentheses. Here is an example:

let display (value : int) = printfn "%A" value;

You also know that you can omit the type of the parameter. As a matter of fact, the above function can be written as we saw already:

let display value = printfn "%A" value;

As a result, practically all functions in F# are generic by default. Still, to indicate that the type of a parameter is not specified, instead of adding a colon and a data type to it, use the colon but replace the data type with an apostrophe and a letter or a word. Here is an example:

let display (value : 'something) = printfn "%A" value;

let a = 25
display a
let b = 48.07
display b
let c = true
display c
let d = "Movie Production"
display d
let e = (2, 6)
display e

By tradition, a letter instead of a word is used for the generic type. It can be any letter, but by tradition, most people use the letter a or the letter T:

let display (value : 'T) = printfn "%A" value;

If the function is receiving more than one parameter, if the parameters are of the same type, you can write each in its own parentheses, its name followed by a colon and the generic letter or word preceded by '. Here are examples:

let addition (a : 'T) (b : 'T) =
    printfn "Numbers: %A %A" a b

// Passing two integers
let x = 248
let y = 75
addition x y

This would produce:

Numbers: 248 75
Press any key to close this window . . .

Of course, if the parameters are of the same type, when the function is called, you must pass the same kind of value to the parameters, otherwise you would receive an error. For example the following code produces an error:

let addition (a : 'T) (b : 'T) =
    printfn "Numbers: %A %A" a b

// Passing a decimal and an integer
let x = 248.04
let y = 75
addition x y

If the function is taking more than one parameter and the parameters are of different types, you can write each in its own parentheses, its name followed by a colon and its own generic letter or word preceded by '. Here are examples:

let addition (a : 'U) (b : 'V) =
    printfn "%A %A" a b

// Passing two integers
let x = 248
let y = 75
addition x y

// Passing a string and an integer
let c = "Catherine Watts, "
let d = 26
addition c d

// Passing a string and a Boolean value
let status = "Full-Time Employee"
let truth = true
addition status truth

// Passing a decimal and a natural number
let n = 282.74
let m = 60
addition n m

This would produce:

248 75
"Catherine Watts" 26
"Full-Time Employee" true
282.74 60
Press any key to close this window . . .

Creating a Generic Function

Although most functions in F# are primarily considered generic, when creating a function, you can formally specify that it is generic. To do this, just after the name of the function, type <>. Inside those symbols, type an apostrophe and a letter or word. Here is an example:

let display<'T> value = printfn "Number: %A" value;

Calling a Generic Function

You have two options to call a generic function. You can call it just as we have done so far, simply using the name of the function. Here is an example:

let display<'T> value = printfn "Number: %A" value;

let a = 25;
display a;

This would produce:

Number: 25
Press any key to close this window . . .

Here is another run of the program:

let display<'T> value = printfn "Video Status: %A" value;

let d = "Movie Production";
display d;

This would produce:

Video Status: "Movie Production"
Press any key to close this window . . .

If the function is called many times in the program, then you must include the paremeter in parentheses followed by : ' and the same letter or word applied to the generic function. Here is an example:

let display<'T> (value : 'T) = printfn "%A" value;

let a = 25;
display a;
let b = 48.07;
display b;
let c = true;
display c;
let d = "Movie Production";
display d;
let e = (2, 6);
display e

If the function takes more than one parameter and you want to apply the same type to all parameters, include each parameter in its parentheses with the common type. Here are examples:

let display<'T>  (a : 'T) (b : 'T) = printfn "%A" (a, b);

display 2 5;
display 4.12 8.06;
display true false;
display "Movie Production" "Release Date";
display (2, 6) (0, 5);

This would produce:

(2, 5)
(4.12, 8.06)
(true, false)
("Movie Production", "Release Date")
((2, 6), (0, 5))
Press any key to close this window . . .

The Data Type of a Generic Parameter

Another technique to call a generic function is to specify the data type of a generic argument. To do this, on the right side of the function, type <>. Inside the operator, enter the data type. Here is an example:

let display<'T> value = printfn "Number: %A" value;

let a = 25;
display<int> a;

Here is another run of the program:

let display<'T> value = printfn "Video Status: %A" value;

let d = "Movie Production";
display<string> d;

In the same way, whenever you call the function, on the right side of the function, you can specify the data type of its argument. Here is an example:

let display<'T> (value : 'T) = printfn "%A" value;

let a = 25;
// Passing an integer
display<int> a;

let b = 48.07;
// Passing a decimal number
display<float> b;

let c = true;
// Passing a Boolean value
display<bool> c;

let d = "Movie Production";
// Passing a string
display<string> d;

// Passing a tuple
let e = (2, 6);
display e

You can also ask the compiler to figure out what the actual data type of the parameter is. To do this, use the underscore in place of the parameter. Here are examples:

let display (value : 'something) = printfn "%A" value;

let a = 25;
display<_> a;
let b = 48.07;
display<_> b;
let c = true;
display<_> c;
let d = "Movie Production";
display<_> d;
let e = (2, 6);
display<_> e

The Data Types of Generic Parameters

If the function is taking more than one generic argument, when calling the function, if you want to specify the data type of each generic argument, on the right side of the name of the function, type <>. Inside the operator, enter each data type and separate them with commas. You can omit the generic letters or words on the right side of the name of the function when defining it. Here is an example:

let addition (a : 'U) (b : 'V) =
    printfn "%A %A" a b;

let x = 248;
let y = 75;
// Passing two integers
addition<int, int> x y;

let c = "Catherine Watts";
let d = 26;
// Passing a string and an integer
addition<string, int> c d

let status = "Full-Time Employee"
let truth = true
// Passing a string and a Boolean value
addition<string, bool> status truth

let n = 282.74;
let m = 60;
// Passing a decimal and a natural number
addition<float, int> n m;

Or you can specify the generic letters or words on the right side of the name of the function when defining it. Here is an example:

let addition<'U, 'V> (a : 'U) (b : 'V) =
    printfn "%A %A" a b;

let x = 248;
let y = 75;
// Passing two integers
addition<int, int> x y;

let c = "Catherine Watts";
let d = 26;
// Passing a string and an integer
addition<string, int> c d

let status = "Full-Time Employee"
let truth = true
// Passing a string and a Boolean value
addition<string, bool> status truth

let n = 282.74;
let m = 60;
// Passing a decimal and a natural number
addition<float, int> n m;

You can also ask the compiler to figure out type one, a few, or all of the parameters is (are). To do this, when calling the function, use the underscore in the placeholder of the parameter. Here are examples:

let addition<'U, 'V> (a : 'U) (b : 'V) =
    printfn "%A %A" a b;

let x = 248;
let y = 75;
// Passing an unknown type and an integer
addition<_, int> x y;

let c = "Catherine Watts";
let d = 26;
// Passing a string and an unknown type
addition<string, _> c d

let status = "Full-Time Employee"
let truth = true
// Passing two specified types
addition<_, _> status truth

let n = 282.74;
let m = 60;
// Passing a decimal and a natural number
addition<float, int> n m;

Using Generic and Non-Generic Types in a Function

You can create a function that uses (a) specific data type(s) for one or more parameters and one or more generic parameters. Here is an example:

let display<'T>  a (b : 'T) = printfn "%s: %A" a b

display "Number" 5
display "Distance" 8.06
display "Timie Sheet Submitted" true
display "Video Status" "Movie Production"

In fact, you can indicate (a) specific type for the parameter(s) whose type(s) is(are) known. Here is an example:

let display<'T>  (a : string) (b : 'T) = printfn "%s: %A" a b

display "Number" 5
display "Distance" 8.06
display "Timie Sheet Submitted" true
display "Video Status" "Movie Production"

This would produce:

Number: 5
Distance: 8.06
Timie Sheet Submitted: true
Video Status: "Movie Production"
Press any key to close this window . . .

Using Various Generic Types in a Function

You can create a generic function that takes different types of parameters but whose actual types are not known at the time the function is created. To specify the generic types, after the name of the function and inside <>, type a letter or word for each type. The letter or word must be preceded by '. The types are separated by commas. Here is an example:

let addition<'U, 'V> a b =
    . . .

You can access each parameter in the body of the function. If you are calling the printf() or the printfn() function, you can use %A. Here is an example:

let addition<'U, 'V> a b =
    printfn "Numbers: %A %A" a b;

let x = 248;
let y = 75;
addition x y;

This would produce:

Numbers: 248 75
Press any key to close this window . . .

If the function is called many times in the program, you must include each parameter in its own parentheses followed by : and its generic type. Here is an example:

let addition<'U, 'V> (a : 'U) (b : 'V) =
    printfn "Numbers: %A %A" a b;

// Passing two integers
let x = 248;
let y = 75;
addition x y;

// Passing a string and an integer
let c = "Catherine Watts, ";
let d = 26;
addition c d

// Passing a string and a Boolean value
let status = "Full-Time Employee"
let truth = true
addition status truth

// Passing a decimal and a natural number
let n = 282.74;
let m = 60;
addition n m;

This would produce:

Numbers: 248 75
Numbers: "Catherine Watts, " 26
Numbers: "Full-Time Employee" true
Numbers: 282.74 60
Press any key to close this window . . .

Generic Classes

Introduction

Consider the following class:

type BillCollector(item) =
    member this.GottenBack = item

The constructor of this class takes one parameter and assigns it to a member variable in the body of the class. Neither the constructor nor the member variable specifies the type of value the parameter will carry or use. In fact, here is a way to run the program:

type BillCollector(item) =
    member this.GottenBack = item

let coll = BillCollector(2250.75)
printfn "Money Collected: %.2F" coll.GottenBack

This would produce:

Money Collected: 2250.75

Here is another run of the program:

type BillCollector(item) =
    member this.GottenBack = item

let coll1 = BillCollector("Honda Accord 2002")
printfn "Vehicle Collected: %s" coll1.GottenBack

This would produce:

Vehicle Collected: Honda Accord 2002

When creating a class, you may want to have one or more members whose type(s) is(are) not known at the time the class is created. This is typical for a class that would be used to create a list. That is, you may want to create a list-based class (also called a collection class) that can be used to create lists of any kinds.

A generic class is one whose main member variable(s) is(are) not known in advance.

Creating a Generic Class

The above class indicates, as we saw with functions, that any class in F# is generic by default. Still, you can formally create a generic class when necessary.

To create a generic class, after the name of the class, type <>. Inside the operator, type a letter or word preceded by '. This would represent the generic type, indicating that the data type that will be used is not determined at this time. In the parentheses of any constructor that uses a parameter that will use the generic type, follow the name of the parameter with ' and the generic letter or word. In the body of the class, use the parameter as you see fit. Here is an example:

type BillCollector<'T>(item : 'T) =
    member self.GottenBack = item

When creating an object, you can call the constructor of the class by simply using its name. Here is an example:

let coll = BillCollector(2250.75)
printfn "Money Collected: %.2F" coll.GottenBack

As an alternative, when declaring a variable from the class, to indicate that the constructor is using a generic parameter, on the right side of the name of the class, type <> and include the appropriate data type used by the parameter. Here is an example:

type BillCollector<'T>(item : 'T) =
    member this.GottenBack = item
    
let coll = BillCollector<float>(2250.75)
printfn "Money Collected: %.2F" coll.GottenBack

Here is another version of creating an object from the class:

type BillCollector<'T>(item : 'T) =
    class
	member this.GottenBack = item
    end
    
let coll = BillCollector<string>("Honda Accord 2002");
printfn "Vehicle Collected: %s" coll.GottenBack;

A generic class can have various members that use the same type. In this case, in the parentheses of the constructor, enter each paremeter followed by : ' and the generic letter or word. Eventually, in the body of the class, use each parameter as you see fit.

type BillCollector<'T>(info : 'T, add : 'T) =
    class
        member this.GottenBack = info
        member this.Additional = add
    end
 
let coll = BillCollector<string>("Honda Accord 2002", "Pass Due");
printfn "Vehicle Delinquency: %s" coll.GottenBack;
printfn "Payment Status:      %s" coll.Additional;

This would produce:

Vehicle Delinquency: Honda Accord 2002
Payment Status:      Pass Due
Press any key to close this window . . .

Here is another run of the program:

type BillCollector<'T>(info : 'T, add : 'T) =
    class
        member this.GottenBack = info
        member this.Additional = add
    end
 
let coll = BillCollector<bool>(false, true);
printfn "Has been keeping payments: %b" coll.GottenBack;
printfn "Time to collect:           %b" coll.Additional;

This would produce:

Has been keeping payment: false
Time to collect:          true
Press any key to close this window . . .

A Generic Class With Various Types

A generic class can use different generic types. To specify those types, In the <> operator, type a lette or word preceded by ' for each paramater. In the parentheses of the constructor, apply the appropriate letter or word to the desired parameter. In the body of the class, use each parameter as you judge necessary. Here is an example:

type BillCollector<'U, 'V>(item : 'U, add : 'V) =
    class
        member this.GottenBack = item
        member this.Additional = add
    end

When creating an object from the class, on the right side of the constructor, add the <> operator. Inside that operator, enter the actual data types that the arguments use, separated by commas. In the parentheses of the constructor, pass the arguments, each conform to the corresponding type of the <> operator. Here is an example:

type BillCollector<'U, 'V>(item : 'U, add : 'V) =
    class
        member this.GottenBack = item
        member this.Additional = add
    end
    
let coll = BillCollector<string, float>("Honda Accord 2002", 6250.85);
printfn "Vehicle Collected: %s" coll.GottenBack;
printfn "Item Value:        %.02F" coll.Additional;

This would produce:

Vehicle Collected: Honda Accord 2002
Item Value:        6250.85

Here is another run of the program:

let coll = BillCollector<int, bool>(6, true);
printfn "Delinquency Period: %d months" coll.GottenBack;
printfn "Patience Run Out: %b" coll.Additional;

This would produce:

Delinquency Period: 6 months
Patience Run Out: true
Press any key to close this window . . .

In the same way, when creating a class, you can use as many generic types as you want in your class. You can also use a mixture of generic and non-generic parameters. In this case, create the generic types in the <> operator. In the parentheses of the class, include as many parameters as you want. Each generic parameter must be accompanied by its generic type. In the body of the class, use the parameters as you judge necessary. Here is an example:

type BillCollector<'U, 'V>(cat : 'U, item : string, add : 'V) =
    class
        member this.Category   = cat
        member this.GottenBack = item
        member this.Additional = add
    end

When creating the object, after starting to declare the variable, on the right side of the constructor, add <> in which you must specify (only) the actual type(s) of the generic parameter(s). Here are examples:

type BillCollector<'U, 'V>(cat : 'U, item : string, add : 'V) =
    class
        member this.Category   = cat
        member this.GottenBack = item
        member this.Additional = add
    end
    
let coll = BillCollector<int, bool>(1, "Honda Accord 2002", true);
printfn "Item Category:      %d" coll.Category;
printfn "Vehicle Collected:  %s" coll.GottenBack;
printfn "Collection Started: %b" coll.Additional;

This would produce:

Item Category:      1
Vehicle Collected:  Honda Accord 2002
Collection Started: true
Press any key to close this window . . .

Here is another run of the program:

type BillCollector<'U, 'V>(cat : 'U, item : string, add : 'V) =
    class
        member this.Category   = cat
        member this.GottenBack = item
        member this.Additional = add
    end
    
let coll = BillCollector<string, float>("Unresponsive", "Boat - Yamaha 212SS", 22480.00);
printfn "Item Category:   %s" coll.Category;
printfn "Item to Collect: %s" coll.GottenBack;
printfn "Remaining Value: %.02F" coll.Additional;

This would produce:

Item Category:   Unresponsive
Item to Collect: Boat - Yamaha 212SS
Remaining Value: 22480.00
Press any key to close this window . . .

Generic Methods

A method is referred to as generic if it takes a generic parameter. Of course, the generic type must have been defined in the name of the class. On the right side of the name of the method, apply the same generic type. Here is an example:

type BillCollector<'T>(item : 'T) =
    class
        member this.GottenBack = item
        member this.Show<'T> (i : 'T) =
            printfn "%A" i;
    end

When calling the method, pass the same type of argument you pass to the constructor when creating an object. Here is an example:

type BillCollector<'T>(item : 'T) =
    class
        member this.GottenBack = item
        member this.Show<'T> (i : 'T) =
            printfn "%A" i;
    end
    
let coll = BillCollector<string>("Honda Accord 2002");
printfn "Vehicle Collected: %s" coll.GottenBack;
coll.Show "Reason: Payment Delinquency";

This would produce:

Vehicle Collected: Honda Accord 2002
"Reason: Payment Delinquency"
Press any key to close this window . . .

Constraints on Generics

Introduction

In all places where we have used generic types so far, almost any value could be used. In some cases when creating a generic function or a generic class, you may want to put a restriction on the types of values that can be used on the generic parameter. Putting a restriction on a generic parameter is referred to as constraining the generic type. Generic constraining is done using a keyword named when. You have various options.

Constraining a Generic Function or a Generic Class to a Structure

To indicate that a parameter type of a function or class must be based on an object created from a structure, in the <> operator of the function name or of the class name, after the generic letter or word, type when followed by the same generic type, followed by : struct>. Here are examples:

let display<'T when 'T : struct> value = ...;

type Calculation<'T when 'T : struct>(x : 'T, y : 'T) =
    class

    end

Constraining a Generic Function or a Generic Class to a Class

To indicate that a parameter type of a function or class must be based on an object created from a class, in the <> operator, use not struct. Here are examples:

let display<'T when 'T : not struct> value = ...;

type Calculation<'T when 'T : not struct>(x : 'T, y : 'T) =
    class

    end

Constraining a Generic Function or a Generic Class to Nullity

To indicate that a parameter type can allow null values, in the <> operator, instead of struct, use null. Here are examples:

let display<'T when 'T : null> value = ...;

type Calculation<'T when 'T : null>(x : 'T, y : 'T) =
    class

    end

Constraining a Generic Function or a Generic Class to Comparison Operations

To indicate that Boolean comparisons can be performed on the values of the generic type, use comparison. Here are examples:

let display<'T when 'T : comparison> value = ...;

type Calculation<'T when 'T : comparison>(x : 'T, y : 'T) =
    class

    end

Constraining a Generic Function or a Generic Class to Equality Operations

To indicate that the comparison for equality can be performed on the values of the generic type, use equality. Here are examples:

let display<'T when 'T : equality> value = ...;

type Calculation<'T when 'T : equality>(x : 'T, y : 'T) =
    class

    end

Constraining Generic Parameters to a Certain Class

You can specify that only values or objects of a certain class can be used as parameters for a certain function or class. Of course, you must have a class. You can use an existing class or you can first create one. Once you know the class you want to use as the basis for a generic type, to put the constraint, in the <> operator of the class name, after the generic letter or word, type when followed by the same generic type, the :> operator, and the name of the class. Here are examples:

type Numeric(a, b) =
    class
        member this.Add = a + b
        member this.Multiply = a * b
        member this.Subtract = a - b
        member this.Divide =
            if b <> 0 then
                a / b
            else
                0
    end

type Calculation<'T when 'T :> Numeric> (u : 'T) =
    class
        member this.Show = u;
    end

When creating an object, specify the data type as the class you had specified as the generic constraint. Here is an example:

type Numeric(a, b) =
    class
        member this.Add = a + b
        member this.Multiply = a * b
        member this.Subtract = a - b
        member this.Divide =
            if b <> 0 then
                a / b
            else
                0
    end

type Calculation<'T when 'T :> Numeric> (u : 'T) =
    class
        member this.Show = u;
    end

let oper = Numeric(15, 49);
let result = Calculation<Numeric>(oper);

Previous Copyright © 2014-2024, FunctionX Monday 23 October 2023 Next