Generically Constraining F# – Part II

In the previous post, we talked about the basic problems with C# generic restrictions.  We covered how it was possible in fact in the CLR to do these restrictions and sure enough the F# language supports them.  This time, we’ll go deeper into what kinds of generic restrictions are possible and how we might use them.  But, before we begin, let’s revisit our previous example.

Revisiting Our Previous Example

One commenter correctly pointed out that a more literal translation of the Enum.GetValues would in fact export to C#:

open System
let getValues<'a when 'a : struct and 'a :> Enum>() =
  Enum.GetValues(typeof<'a>) :?> 'a array

In this case, we are using a value type and then a type constraint to ensure that it is both a structure and derives from System.Enum.  Alternatively, we could also use the F# enum constraint as well, such as the following:

let getValues<'a,'b when 'a : struct and 'a : enum<'b> and 'a :> Enum>() =
  Enum.GetValues(typeof<'a>) :?> 'a array

And then we could execute as before:

> open System.IO;;
> getValues<FileAttributes, int>();;
val it : FileAttributes array =
  [|ReadOnly; Hidden; System; Directory; Archive; Device; Normal; Temporary;
    SparseFile; ReparsePoint; Compressed; Offline; NotContentIndexed;
    Encrypted|]

Both of these examples work when exported to C# as well.  Now that we’re certain we can get C# to respect our generic restriction, what else could we do?

Generic Restrictions

The F# language supports quite a few ways that we can constrain generics.  Each one listed below will be gone into detail:

  • Type constraint
  • Null constraint
  • Explicit member constraint
  • Constructor constraint
  • Reference type constraint
  • Enumeration type constraint
  • Delegate constraint

Type Constraint

A type constraint is where the provided type must be the same or derived from the specified type.  In the case of interfaces, the provided type must implement said interface.  As you saw earlier in the getValues example, we used a type constraint to ensure that our provided type must derive from System.Enum, as all enums do.  In this case, let’s define how to add a line as a child to a given element which implements the IAddChild interface in WPF:

let drawLine<'a when 'a :> IAddChild> stroke (x1, y1) (x2, y2) (e : 'a) =
  let line =
    new LineGeometry(StartPoint = Point(x1, y1), EndPoint = Point(x2, y2))
  let path =
    new Path(Stroke = stroke, StrokeThickness = 5., Data = line)
  e.AddChild(path)

By using the :> operator, we constrain our ‘a type that it must implement the IAddChild interface.  Alternatively, we could use the #<type> inline to our parameter to mean the same thing, where type means the what the type must inherit from or implement.:

let drawLine stroke (x1, y1) (x2, y2) (e : #IAddChild) =
  let line =
    new LineGeometry(StartPoint = Point(x1, y1), EndPoint = Point(x2, y2))
  let path =
    new Path(Stroke = stroke, StrokeThickness = 5., Data = line)
  e.AddChild(path)

Null Constraint

In this constraint, the provided type must support being set to null.  This will include all .NET reference types, but excludes value types as well as F# types of lists, tuples, functions, classes, records or union types.  By using the ‘a : null syntax, we can say that ‘a is a nullable reference.  Note that this doesn’t work for Nullable<T> types either.  Let’s look at a quick example of using the null constraint:

let printValue<'a when 'a : null> (arg:'a) =
  if arg = null then
    printfn "You gave me nothing"
  else
    printfn "Value: %A" arg

Then we could test the behavior by giving it several examples such as the following:

> printValue "hello";;
Value: "hello"
val it : unit = ()
> printValue null;;
You gave me nothing

Explicit Member Constraint

The explicit member constraint states that the type argument provided must have a member that has the specified signature.  This means that we could in fact do duck typing in such a way that our argument must support a certain member function.  I’ve covered this in the past, but let’s review.  One example would be to create a function which allows us to invoke implicit operators, which are by default not supported by F#. 

let inline implicit< ^a, ^b when ^a : (static member op_Implicit : ^b ->  ^a)> 
  arg =
    (^a : (static member op_Implicit : ^b -> ^a) arg)

This allows me to specify that my ^a type must support a static method called op_Implicit, simply known as the implicit operator.  Alternatively, we can allow type inference to do the work for us and leave off the top level declaration and declare the function calls inline and the results will be the same.

let inline implicit arg =
  (^a : (static member op_Implicit : ^b -> ^a) arg)

We can test the behavior of this by using the implicit operator built into LINQ to XML.  Using the library is not exactly as easy as it is in C# due to the implicit operator from string to XName.  In this instance, I’ll use the ( !! ) operator, or the convert dammit operator, to use on the XNamespace class and then append a string to create an XName.

> open System.Xml.Linq;;
> let ( !! ) : string -> XNamespace = implicit;;

val ( !! ) : (string -> XNamespace)

> let foo = !!"" + "foo";;

val foo : XName = foo

 

 

 

As you can see, we created the foo XName by creating an empty XNamespace and appending our “foo”.  Before we wrap up this section, let’s try one more example which supports multiple multiple methods.  In this case, we’ll extract both the name as an XName, and the value from a given LINQ to XML object.  The reason I do this is that the XElement and XAttribute do not share a common ancestor which supports a Name and Value property.

 

 

 

 

 

 

open System.Xml.Linq

let inline getNameValue< ^a when ^a : (member get_Name  : unit -> XName ) and
                                 ^a : (member get_Value : unit -> string)>
 xItem = 
  let name =  (^a : (member get_Name  : unit -> XName ) xItem)
  let value = (^a : (member get_Value : unit -> string) xItem)
  name, value

In this case, we constrain our generic that it must implement both a Name and Value property getter.  Then we invoke both the name and value functions and return them as a tuple.  Once again, as with above, we can leave off these constraints and let the type inference magic do the work for us.

open System.Xml.Linq

let inline getNameValue xItem = 
  let name =  (^a : (member get_Name  : unit -> XName ) xItem)
  let value = (^a : (member get_Value : unit -> string) xItem)
  name, value

You can begin to see the power in this where we are no longer constrained by inheritance, and instead which methods our type supports.  This is different of course than the dynamic type forthcoming in .NET 4.0 as this is statically checked at compile time instead of at runtime.  Let’s walk through some simple examples using it:

> getNameValue (new XElement(!!"" + "bar", "foo"));;
val it : XName * string = (bar {LocalName = "bar";
                                Namespace = ;
                                NamespaceName = "";}, "foo")
> getNameValue (new XAttribute(!!"" + "foo", "bar"));;
val it : XName * string = (foo {LocalName = "foo";
                                Namespace = ;
                                NamespaceName = "";}, "bar")
> getNameValue (new XDocument());;

  getNameValue (new XDocument());;
  --------------^^^^^^^^^^^^^^^

stdin(238,15): error FS0001: The type 'XDocument' does not support any operators
 named 'get_Name'

Now you can see it works nicely with XElement and XAttribute, but not with XDocument as it doesn’t support the Name property with a getter accessor, nor does it support a Value property either.  I think that’s enough for this post.

Conclusion

So far, we’ve covered just a few of the generic type constraints that you can do with F# including type, null constraint and member constraint.  In the next part, we’ll cover the rest of the generic constraints that F# supports. 

No Comments