BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage News .NET Futures: Type Classes and Extensions

.NET Futures: Type Classes and Extensions

Leia em Português

This item in japanese

Another feature being considered for future versions of .NET are type classes. Referred to as “shapes” in the Shapes and Extensions proposal, they would greatly increase the capabilities of .NET generics. Mads Torgersen describes type classes:

Interfaces abstract over the shape of objects and values that are instances of types. The idea behind type classes is essentially to abstract over the shapes of the types themselves instead. Furthermore, where a type needs to opt in through its declaration to implement an interface, somebody else can make it implement a type class in separate code.

Type classes solve a long-running problem with interfaces: they can’t deal with static functions or operator overloads. This has led to problems like needing to declare the same function over and over again in math libraries to account for all of the different numeric data types.

Mads continues:

In general, a "shape" declaration is very much like an interface declaration, except that it:

Can define almost any kind of member (including static members)

Can be implemented by an extension

Can be used like a type only in certain places

That last restriction is important: a shape is not a type. Instead, the primary purpose of a shape is to be used as a generic constraint, limiting type arguments to have the right shape, while allowing the body of the generic declaration to make use of that shape.

Closely married to the idea of shapes is an improved extension syntax. Rather than just extension methods, an extension construct can provide almost anything for a type class. Consider the following simplistic example:

public shape SNumber<T>
{
    static T operator +(T t1, T t2);
    static T operator -(T t1, T t2);
    static T Zero { get; }
}

The Int32 type already provides most of this, but is lacking the zero property. An extension could fix that:

public extension IntGroup of int : SNumber<int>
{
    public static int Zero => 0;
}

You could then use it as follows:

public static AddAll<T>(T[] ts) where T : SNumber<T> // shape used as constraint
{
    var result = T.Zero; // Making use of the shape's Zero property
    foreach (var t in ts) { result += t; } // Making use of the shape's + operator
    return result;
}

Implementation

Implementing this requires some trickery with interfaces and structs.

  • Shapes are translated into interfaces, with each member (even static ones) turning into an instance member on the interface
  • Extensions are translated into structs, with each member (even static ones) turning into an instance member on the struct
  • If the extension implements one or more shapes, then the underlying struct implements the underlying interfaces of those shapes

The struct described above is often referred to as the “witness struct”. Its existence proves that a class conforms to the rules of a shape. Or in other words, the class is in the type class.

Here is that same AddAll method after the compiler has translated it:

public static T AddAll<T, Impl>(T[] ts) where Impl : struct, SNumber<T>
{
    var impl = new Impl();
    var result = impl.Zero;
    foreach (var t in ts) { result = impl.op_Addition(result, t); }
    return result;
}

The aforementioned witness struct is then used to provide the necessary functionality to the AddAll method. The struct can call methods directly on the type or use the extension construct as necessary.

Implementing Shapes in Classes and Interfaces

Classes can explicitly implement a shape via the same syntax we use for extending base classes and implementing interfaces. The compiler will then provide the matching witness struct.

Interfaces can also be marked as fulfilling a shape’s requirements. Here is an example:

public extension Comparable<T> of IComparable<T> : SComparable<T> ;

Since there is a one-to-one match between IComparable<T> and our theoretical type class, there is no need to provide a body to the extension construct.

Generic Types

Generic types prove to have their own problems. Like generic methods, adding a shape or type class as a type constraint to a generic class necessitates an additional type parameter. Since the number of type parameters on a generic class is part of its name, this can lead to collisions with other generic types that share the same name.

Extensions on Shapes

Not only can extension constructs be used to implement shapes, but they can also extend them. So you can take an existing shape and add new methods, static functions, and operators to it. Just like extension methods, the syntax is the same as if they were defined directly on the underlying type.

Criticism

Overall, the feedback on this feature has been positive. However, there have been some changes requested. For example, shapes currently have to be explicitly implemented. Some developers would prefer that the shapes be implicitly implemented by the compiler if no additional extension methods are needed for a given class or interface. Mads cites some problems with this:

It may lead to many struct types getting generated for witnessing exactly the same shape being applied to the same type in the same way, so there's a risk of undue proliferation of generated types. Potentially this could be mitigated by the compiler being clever and generating only one per assembly, but we know from anonymous type that this kind of de-duping is hard and fraught with error.

If we allow generic types to have shape-constrained type parameters, then having multiple witness structs for the same thing would lead to the instantiated generic types to have different type identity and not be interchangeable.

There are also concerns about how closely shapes and extensions are tied together. It is thought that this may prove to be confusing down the road.

To this Mads responded:

Conflation: The "extensions" in my proposal indeed conflate multiple concerns:

[…]

I think there's a lot to be said for the same language mechanism serving all these purposes - they are, after all, quite closely related. But it would be instructive to see a proposal that cleanly separates them. Maybe it leads to a more elegant place.

Rate this Article

Adoption
Style

BT