F# Instance member constraint – opens a side door on your type so ducks can walk in

I was trying to wrap my head around “Member constraint”,  internal wirings in F#.   Lets start with, what is constraint? Here is what MSDN says about constraints – ” You have to add a constraint when your code has to use a feature that is available on the constraint type but not on types in general. For example, if you use the type constraint to specify a class type, you can use any one of the methods of that class in the generic function or type.”

Lets back up a second to C# and see what is constraints on Type parameters. When you define a generic class, you can apply restrictions to the kinds of types that client code can use for type arguments when it instantiates your class. If client code attempts to instantiate your class with a type that is not allowed by a constraint, the result is a compile-time error. These restrictions are called constraints. Constraints are specified using the where contextual keyword.


public class Employee {
        private string name;
        private int id;
}

public class GenericList<T> where T : Employee {  ... }

The constraint enables the generic class to use the Employee.Name property since all items of type T are guaranteed to be either an Employee object or an object that inherits from Employee.

In F# we will write similar type constraint as follows:


type GenericList<;'T when 'T: Employee> =
class end

In addition to the usual constraints well-known in other CLR languages, F# also has member constraints, which require in lining.

The following is an example from Marc Sigrist “F# Generic inlining with Member constraint” post.


type Converter(cultureName:string) =
member val Culture = CultureInfo.GetCultureInfo cultureName
     /// Converts value to a string, based on the specified culture.
member inline self.ToString value =         (^T: (member ToString: IFormatProvider -> string) value, self.Culture)

[] let main argv =
let germanConverter = Converter "de"
let nrString = germanConverter.ToString 1234.643  // "1234,643"
let dtString = germanConverter.ToString <|   DateTime(2003, 11, 25, 17, 38, 47) // "25.11.2003 17:38:47";

After looking at this code, if you read the MSDN description again it may make more sense –  you have to add a constraint when your code has to use a feature that is available on the constraint type but not on types in general.  Here we are adding a feature “ToString” on Converter type, which in turn will take IFormatProvider type and return a string.  Because of “inline” and it being a generic in nature, this method can be used on any type as far as it satisfy the constraint. This is what “Duck typing” is, if any type has ToString implemented which can take IFormatProvider and return string, then compiler will let any of those type call ToString of Converter. In this example,  DateTime is passed to “ToString” of converter and compiler does not complain as ToString of DateTime obeys the constraints defined in “Converter”.

Member constraints open another door on type, and type member can define their own constraints.  Lets see it with another example, I am writing a game and I have a type called “Highway”, and it has constraint  that only “Trucks” can drive on it. So sure about this constraint that Highway takes truck in its constructor so only trucks can drive thru it. Attempt to initialize Highway with any other vehicle type will result in a error. Code will not even compile. However, you later realize that as a special case police cars should be able to drive on highway too. In place of changing the highway constructor and its other behavior, you can just open a special door for police cars to drive on highway, and for that you define an inline Drive method with the constraints.


type Truck() =
      member this.Drive () = printf(&quot;Truck driving&quot;)

type PoliceCar() =
      member this.Drive () = printf(&quot;Police Car driving&quot;)

type Highway(t: Truck) =
      member inline this.Drive value = (^T : (member Drive : unit -> unit) value )
      member this.Drive () = t.Drive()

[<EntryPoint>]
let main argv =
  let t = new Truck()
  let h = new Highway(t)
  h.Drive()
let pc = new PoliceCar()
//let h = new Highway(pc) This line will not compile, as PoliceCar is not compatible with Truck
h.Drive pc     //But this line will work perfectly and PoliceCar will drive on highway

0 // return an integer exit code

This is a stupid example,  but remember this is not a discussion about OOP, but i just want to illustrate the member constaint point. Hopefully this clears how the member constraint works at the logical level. Now, lets get down at the internal wiring of these function and see how it works. Looking at the code below, I was not sure what is exactly happening under the hood.


member inline self.ToString value = (^T: (member ToString: IFormatProvider -> string) value, self.Culture)

Above line says, the parameter “value” can be any generic type as long as it implements ToString which takes IFormatProvider and returns string and takes culture as a parameter. Which translate to

Value.ToString (Culture)

But the signature says ToString takes IFormatProvider  and returns a string.  You may wonder what is happening here. Sometime compiler and language construct do things under the hood to make your job easy, but hide few details which may make understanding few things difficult. What exactly happening here is, Culture is cast to IFormatProvider. If you will see the definition of CultureInfo class you will find, it is derived from IFormatProvider. So, effectively the code boils down to


Value.ToString((IFormatProvider) Culture)

So Marc’s F# code

let germanConverter = Converter "de"
let nrString = germanConverter.ToString 1234.643  // 1234,643
let dtString = germanConverter.ToString <|   DateTime(2003, 11, 25, 17, 38, 47) // "25.11.2003 17:38:47"

will translate to as follows in C#:


Program.Converter germanConverter = new Program.Converter("de");
string nrString = 1234.643.ToString((IFormatProvider)germanConverter.Culture);
DateTime dateTime = new DateTime(2003, 11, 25, 17, 38, 47);
string dtString = dateTime.ToString((IFormatProvider)germanConverter.Culture);

Hopefully, deconstructing F# code back to C# will help in understanding the constraint with static member. So why we need this duck typing? From C#, you can already see an example of duck typing in use. To allow an object to be enumerated by foreach operator, the object needs to implement few methods, GetEnumerator, which returns an object, which implement MoveNext and Current.

Read more on Duck typing here.

Advertisements

2 thoughts on “F# Instance member constraint – opens a side door on your type so ducks can walk in

  1. Pingback: F# Instance member constraint – opens a side door on your type so ducks can walk in | Functional programming | Scoop.it

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s