Mastering Types and Generics
Mastering Types and Generics
Mastering Types and Generics
Generics
Generic constructs are always represented through the use of “type variables”, which in
F# syntax are written 'a, 'b, 'k, 'key, 'K etc. For example, the definition of the list type in
the F# library begins:
type 'a list = ...
Values can also be generic. A typical generic value is List.map, whose type is as follows:
val map : ('a -> 'b) -> 'a list -> 'b list
Each time you name a generic type or value the F# type system must infer instantiations
for the type variables involved. For example, in the previous section we used hen we
used “List.map fetch” over an input list. The type of the latter was:
In this case, in the signature of List.map the type variable 'a is instantiated to string and
'b to string * string.
Sometimes the binding point of these variables will be made explicit in error
messages, and explicit bindings can also be used in type annotations, e.g. you may see:
val map<'a,'b> : ('a -> 'b) -> 'a list -> 'b list
Type variables may also be constrained, so that they may only be instantiated with
particular kinds of types. The most important kind of constraint is a subtyping constraint,
and is discussed below in the section on subtyping.
That is, getFirst has been inferred to be generic in three type variables, where the result
type is related to the first element of the input tuple. Automatic generalization is applied
when a let or member definition doesn’t fully constrain the types of inputs or outputs.
You can tell automatic generalization has been applied by the presence of type variables
in an inferred type, and ultimately by the fact you can reuse a construct at multiple types.
Automatic generalization is particularly useful when taking functions as inputs. For
example, the following takes two functions as input and applies them to each side of a
tuple:
let mapPair f g (x,y) = (f x, g y)
The generalized, inferred type is:
val mapPair : ('a -> 'b) -> ('c -> 'd) -> ('a * 'c) -> ('b * 'd)
The code attempts to create an array of empty lists. Here the error message indicates that
automatic generalization would give empties the type 'a list []. However, this type
makes no sense: it would imply that we’ve created an array of lists somehow suitable for
use with any type 'a, when in reality our array should only be usable with one type, e.g.
int list [] or string list [], but not both. (If it were usable with both then we could store
an integer list into the array and fetch it out as a string list!) The “value restriction”
ensures that declarations don’t result in this kind of confusion: automatic generalization is
not applied to declarations unless they are functions or simple, immutable data constructs.
You can work around this restriction in two ways. First, if you intended to create a
function that generated arrays of different types on demand than you can often make the
declaration into a function by adding a dummy argument:
let makeEmpties () = Array.create 100 []
let intEmpties : int list [] = makeEmpties()
Page | 2
let stringEmpties : string list [] = makeEmpties()
However, if you really meant to create one value then simply use an explicit type
annotation, e.g.
let empties : int list [] = Array.create 100 []
Upcasts
We can explore how sub-typing works by using F# Interactive. Firstly, let’s look at how
some of the F# types we’ve already seen relate to the type obj (an abbreviation for the
.NET type System.Object):
Here we are simply investigating the subtyping relationship through the use of the built-in
coercion (or “up cast”) operator :>. This operator is used to convert a value to any of its
super-types. For some types this operation may involve a change of representation called
“boxing”. Boxing may also be performed with the F# library function box.
So far we have only investigated subtyping between ordinary types and the type obj.
Subtyping occurs between other kinds of types as well (the various kinds of type
definitions such as records, discriminated unions, classes and interfaces are discussed in
Chapter 6):
* All types are subtypes of System.Object
* Record and discriminated union types are subtypes of the interface types they
implement
* Interfaces types are subtypes of the other interfaces they extend
* Class types are subtypes of both the interfaces they implement and the classes they
extend
* Array types are subtypes of the .NET type System.Array
* Value types (e.g. types such as int32 that are abbreviations of .NET value types)
are subtypes of the .NET type System.ValueType. Likewise .NET enumeration
types are subtypes of System.Enum.
Page | 3
> let xobj = (1 :> obj);;
val xobj : obj = 1
> let sobj = ("abc" :> obj);;
val sobj : obj = "abc"
Downcasts
Values that may have subtypes also carry a “runtime type”, and runtime type tests may be
used to query the type of an object. This can be done in two main ways: through
downcasts and pattern type tests. As with most object-oriented languages, the “upcasts”
performed through subtyping is reversible through the use of “downcasts”, using the :?>
operator. We can see this through the following examples:
Downcasts are checked at runtime and is safe as all values of the obj type are implicitly
annotated with the “runtime type” of the value. The operator :?> will raise an exception if
the object is not of a suitable type:
Such a pattern may bind the matched value at its more specific type:
let reportObject (x: obj) =
match (box "abc") with
| :? string as s -> printf "the input is the string '%s'\n" s
| :? int as d -> printf "the input is the integer '%d'\n" d
| _ -> printf "the input is something else\n"
Page | 4
> reportObject (box 17)
the input is the integer '17'
This notation is shorthand for a generic function and a “constraint” on the type variable,
and we could equally have written:
hcf 18 12;;
val it : int = 6
hcf 33 24;;
Page | 5
val it : int = 3
However, this algorithm is not generic, since as written it works only over integers. In
particular, although the operator (-) is by default overloaded in F#, each particular use of
the operator must be associated with at most one type. Furthermore, the constant 0 is an
integer and is not overloaded. Overloading is described in more detail in the side panel on
page X. However, despite this, this algorithm can be easily generalized to work over any
type, though we must be given a zero, a subtraction function and an ordering that type.
Here’s how we do this:
let hcfGeneric (zero,(-),(<)) =
let rec hcf a b =
if a=zero then b
elif a<b then hcf a (b-a)
else hcf (a-b) b
hcf
Records of this kind are often called “dictionaries of operations”, and are similar to
vtables from object-oriented programming but are not associated with any particular
object. As we have seen dictionaries can be represented by both tuples and records.
Page | 7
Generic Algorithms through Interface Objects
For larger frameworks a carefully constructed classification of dictionaries represented by
interfaces are often used in place of records. We discuss interface declarations in more
detail in the Chapter 6, and the F# namespace Microsoft.FSharp.Math.Classifications
contains a number of numerical classification types. Here is an interface type definition
that plays the same role as the record in the example above:
type INumeric<’a> =
interface
abstract Zero : ’a;
abstract Subtract: ’a * ’a -> ’a;
abstract LessThan: ’a * ’a -> bool;
end
Interface values can be built and used in a similar way to record values:
let intOps =
{ new INumericOps<int>
with Zero = 0;
and Subtract(x,y) = x - y
and LessThan(x,y) = x < y }
Page | 8