F# Generic Inlining With Member Constraints

by Marc 13. July 2012 16:05

In this post, we are going to create a Converter class who converts values to strings. The conversion rules are speciļ¬ed by passing the name of a culture to the class’ constructor. The culture name can contain just the language (as ISO 639-1 alpha-2 code, e. g. “en”) or the language and region (as ISO 3166 code, e. g. “US”) combined with hyphen, e. g. “en-US”. An empty culture name “” specifies the invariant culture, which falls back to “en”.

The class has only one conversion method ToString, who is generic. It takes a single parameter, which is the value to be converted. The parameter’s type is inferred to have a member constraint, who restricts the type (at compile time) to have an instance member with the signature
ToString: IFormatProvider -> string. The parameter’s member is invoked with a member constraint invocation expression.

open System
open System.Globalization
 
type Converter(cultureName:string) =
    /// The culture used by this converter.
    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)
 
// Test
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"

Edit (Oct 15, 2012)

 The converter's culture is now exposed via a public property, based on F# 3.0 auto property syntax. (The Culture's value is evaluated only once, during class construction time.) Previously, the culture was a private field, and the example could not compile in a regular source file, due to access rule violation. Strangely, it did compile in a scripting file; this is an example where the scripting compiler (wrongly) does not behave in the exact same way as the regular compiler.

This is a blog post in the “Brief tip” category. It is meant as a starting point for further investigation, not as a complete copy/paste solution to a specific concern you may have.

Covariance and Contravariance in C# 4.0

by Marc 20. March 2011 02:07

C# 4.0 allows to declare variance compatibility for delegates and interfaces. This means, for instance, that one can assign an IEnumerable<Cat> to an IEnumerable<Animal>.  The term variance compatibility, in this context, defines the kind of assignment compatibility between two closed generic types, which exists when the parameters of those types are derived from each other (or are themselves variant to each other). In other words: Given two types T1<P1> and T2<P2>,  variance defines how T1 is assignment compatible with T2 in cases where P1 is in an inheritance relationship with P2 (or is itself variant to P2). A more detailed definition can be found in the C# 4.0 language specification, section 13.1.3 Variant type parameter lists. There are three kinds of variance:

Kind of VarianceDelegate ExampleInterface Example
Invariance
delegate T Clone<T>(T t);
interface IList<T>{
    T this[int index] { getset; }
    // Etc.
}
Covariance
delegate T Func<out T>();
interface IEnumerator<out T>{
T Current { get; }
};
Contravariance
delegate void Action<in T>(T t);
interface IComparer<in T>{
int Compare(T x, T y);
}

Invariance

In the above table, Clone<T> and IList<T> are invariant in T. As a consequence, it is not possible to assign Clone<Dog> to Clone<Animal>, or vice versa. This is because T is used both as an incoming parameter and an outgoing parameter at the same time. Imagine what would happen if one could actually assign an IList<Dog> to an IList<Animal>. If we then retrieved animals[0], we would get a dog as animal. So far, so good. However, what would happen if we set animals[0] = new Cat()? What should the underlying IList<Dog> do when it gets the cat assigned? Should it perhaps throw an InvalidCastException? There is no way this can be handled without violation of the strong typing principle. Therefore, the core languages of the .Net Framework: C#, F#, and VB.NET (with option strict on) do not allow covariance or contravariance in this situation.

IList<Animal> animals = new Collection<Animal>();
IList<Cat> cats = new Collection<Cat>();
// The following line would produce a compiler error.
// animals = cats;
// Thanks to the above compiler error, we can always be sure
// that the following line will not produce a runtime exception.
animals.Add(new Dog());

Unfortunately, ever since C# 1.0, strong typing has not been completely enforced with regards to arrays (many other languages, e.g. Java, have the same problem). The following programming error is not prevented by the compiler:

Cat[] cats = { new Cat(), new Cat() };
Animal[] animals = cats; // Compiler allows covariance, eventhough in parameters exist.
animals[0] = new Dog(); // --> ArrayTypeMismatchException at runtime!

Here is an interesting side note: It is sometimes argued that breaking type safety with arrays was a planned, pragmatic decision at the time of C# 1.0, because generics did not exist yet. However, according to Don Syme, who was responsible for generics in the CLR, back then it was not even certain whether generics would ever be introduced at all. Microsoft was reluctant, and the responsible research team was underfunded. The feature could only be introduced in C# 2.0 under extreme pressure at the very last minute. Luckily, things are different now. Runtime-integrated generics, and the many technologies built open them (such as Lambda expressions, Linq, and F#), have become a great success story, and they clearly distinguish Microsoft .Net and Mono from other popular development frameworks.

Covariance

In the above table, Func<out T> and IEnumerator<out T> are covariant in T. As a consequence, it is possible to assign Func<Cat> to Func<Animal>. Strong typing can be enforced by the compiler, because T is only used as outgoing parameter, never as incoming parameter. To enable covariance in T, T must be explicitly annotated with the generic modifier out.

Func<Animal> createAnimal = () => new Animal();
Func<Cat> createCat = () => new Cat();
createAnimal = createCat;
var a = createAnimal(); // Gets a cat as animal.

Contravariance

In the above table, Action<in T> and IComparer<in T> are contravariant. Therefore, it is possible to assign Action<Animal> to Action<Cat> or IComparer<Animal> to IComparer<Cat>. Strong typing can be enforced by the compiler, because T is only used as incoming parameter, never as outgoing parameter. To enable contravariance in T, it must be explicitly annotated with the generic modifier in.

Action<Animal> animalAction = animal => animal.Sleep();
Actionn<Cat> catAction = cat => cat.Meow();
catAction(new Cat()); // Meow...
catAction = animalAction;
catAction(new Cat()); // Zzz...

Other Considerations

Classes and structs cannot be declared as covariant or contravariant. Even if such a syntax were allowed, the feature would be useless in most cases. For instance, it would be logically impossible to assign a Collection<Cat> to a Collection<Animal>, because Collection<T> uses T both as an incoming parameter and an outgoing parameter. Every kind of collection, even if it is immutable, somehow needs a way to be built up (using T as incoming parameter) and a way to be read from (using T as outgoing parameter). Still, in some cases, where T is used only as incoming parameter or only as outgoing parameter, the feature might be practical (imagine some kind of Builder<out T> or Deserializer<out T>). However, the complexity of the C# compiler is stunning already today. For instance, given something as simple as an interface I1<out T>, it has to enforce type safety for extreme situations like I1<I1<I1<I2<I1<Pet>, I3<I2<Dog, I1<Cat>>>>>>>. But even if it can be proved that it is theoretically possible to create a compiler for C# who is sophisticated enough to check type safety for covariant or contravariant classes, it still does not necessarily make sense to spend the immense amount of time and resources it takes. Rather than doing this, I would prefer Microsoft to implement other features for the language, who take less effort, but are more urgent, such as read-only local "variables", an internal protected access modifier (as opposed to protected internal), and (my favorite wish) non-nullable reference types (if we have int? i, why not also have string! s? :)

I have covered the basics about variance in C#, the rest can mostly be concluded by logical deduction and combination (e.g., dealing with ref and out method parameters, etc.). However, if you truly want to dive into the matter, I recommend Eric Lippert's (of the C# compiler team) excellent twenty-one-part blog series on the subject.

Month List