Type Classes Are The Secret Sauce

I came across a recent post on adding Ruby and C# operators to F# that sparked a few thoughts in my head.  The post was good, but yet there were operators that already existed for some of the operations mentioned such as the defaultArg and ( @ ).  But what really got me was thinking about type classes again due to the fact that extension operators aren’t currently supported in the language whereas extension events, properties, methods and statics are.  I covered this in the past in regards to implementing an approximate equality check for my Functional Programming Unit Testing series, but I want to dive further into that subject a little more in this post.

Just What Are Type Classes?

As you may note that I do cover a bit of Haskell on this blog, and I think for very good reason.  It’s a great language for incubating a lot of interesting ideas.  Some of these ideas have been distilled into other languages such as concepts as monads.  Type classes is another great feature of the language that I have come to know and love. 

When we talk about implementing functions, we have two main types of polymorphism, parametric and overloading.  Talking about parametric polymorphism is the typical programming with generics in which a function allows arguments of arbitrary related types of an unlimited nature.  One such example of this is the map function, or the Select method for those in the LINQ land which allows us to specify both the source and destination types which might be just about anything.

Prelude> :t map
map :: (a -> b) -> [ a ] -> [ b ]

On the other hand, we have ad-hoc polymorphism or overloading, which depending on its context, can assume more than one type.  If the allowable types are limited and must be known ahead of time, this makes is ad-hoc.  This polymorphism type is made possible in Haskell through the use of type classes.  For example, if we want to check for equality between two given algebraic types, this type must either derive the Eq type class or we create and instance of our own.  Let’s make this a little more clear by actually looking at one.  Instead of taking a look at the Eq or Equality check, let’s look at the Ord which contains the comparison operators.

Prelude> :i Ord
class (Eq a) => Ord a where
  compare :: a -> a -> Ordering
  (<) :: a -> a -> Bool
  (>=) :: a -> a -> Bool
  (>) :: a -> a -> Bool
  (<=) :: a -> a -> Bool
  max :: a -> a -> a
  min :: a -> a -> a
      -- Defined in GHC.Classes

This type class lays out our basic comparison operators such as greater than, less than, min, max, etc.  But, it also has a restriction which states that any implementation of this type class must also have an associated implementation of the Eq type class which checks for equality.  You can think of this as generic restrictions.  The command we ran above, the info command also not only gave us information about the type class, but also some implementations such as the following:

instance (Ord a) => Ord (Maybe a) -- Defined in Data.Maybe
instance (Ord a, Ord b) => Ord (Either a b)
  -- Defined in Data.Either
instance Ord Integer -- Defined in efined in GHC.Base
instance Ord Char -- Defined in GHC.Base
instance (Ord a) => Ord [a] -- Defined in GHC.Base
instance Ord Int -- Defined in GHC.Base

In order to provide that functionality such as ordering checking, we must create an instance of the Ord class and implement some members.  We needn’t implement all, just the ones we need for our given scenario.  To give you a sense of how this works, let’s take a look at the implementation of Ord for the Bool type. 

instance Ord Bool where
    compare False True  = LT
    compare True  False = GT
    compare _     _     = EQ

Now we have the compare function available to use with the Bool type.  Pretty nice and easy.  Because of this, we can determine whether one is greater than the other such as the following:

Prelude> compare True False
GT

Besides the Eq and the Ord I’ve already mentioned, there are others such as:

  • Show – Converting values to strings
  • Read – Converting strings to other values (opposite of Show)
  • Num – Numeric operations such as addition, subtraction, multiplication, etc
  • Enum – Enumeration operations
  • Fractional - Division

And the list goes on.  But what I 'like about this, is the opt-in nature of providing a given set of operations and implementing only what you need.  Also, it allows everything to be open so that I can create new instances of a type class for a given type as I see fit, even outside of the module in which it was created.  If you want to go deeper on the subject, Brent Yorgey has a great article called the Typeclassopedia in the Monad Reader Issue 13.

A Poor Man’s Type Class in F#

Ah, but alas we do not have type classes in F#.  It would be great if we did, but I’m sure it might take some changes to the CLR to make this happen such as allowing for higher-kinded polymorphism, which I’ve covered in the past.  As it is, creating generic functions such as the basic math ones can prove to be rather difficult even with the CLR generics.  What it essentially does is checks to see if that given operator exists for your given class among other things.  But, what if you don’t own that class, how could you possibly extend it this way, for example things like adding two Colors?  That’s the power of type classes.

Although not supported natively, we can add that behavior to our classes.  Take for example checking for approximate equality for floating point arithmetic.  As we know, the precise nature of checking for equality of floating points isn’t the same as for integers, and instead must be checked if the difference is less than epsilon.  Let’s define an interface for what that might look like:

open System
open System.Collections.Generic

// Define the type class and it's global associations table
type IApproxEq<'a> = 
    abstract member approxEqual    : 'a -> 'a -> bool
    abstract member approxNotEqual : 'a -> 'a -> bool

What we have here is the beginning of a type class as to define two functions, one for equality and one for inequality.  Next, we need to have some sort of repository for our instances that we declare so that we can dynamically look them up and perform the given operation.  That might look like the following:

[<AbstractClass>]
[<Sealed>]
type ApproxEqualAssociations private() = 
  static let associations = new Dictionary<Type, obj>()
  static member Add<'a>(approxEqual : IApproxEq<'a>) = associations.Add(typeof<'a>, approxEqual)
  static member Get<'a>() = 
    match associations.TryGetValue(typeof<'a>) with 
    | true, assoc -> assoc :?> IApproxEq<'a>
    | false, _    -> failwithf "Type %O does not have an implementation of IApproxEq" <| typeof<'a>

This solution gives us a global singleton in a way of dealing with these operators.  As we shouldn’t be adding to them frequently, and instead reading, it shouldn’t be too much of an issue, but it is an issue nonetheless.  Now that we have this repository of them, let’s declare ones for both single precision floating point and double precision floating points. 

type SingleApproxEqual() =
    let epsilon = 0.001f
    interface IApproxEq<float32> with
        member this.approxEqual x x' = abs(x - x') < epsilon
        member this.approxNotEqual x x' = abs(x - x') >= epsilon  
type DoubleApproxEqual() =
    let epsilon = 0.001
    interface IApproxEq<float> with
        member this.approxEqual x x' = abs(x - x') < epsilon
        member this.approxNotEqual x x' = abs(x - x') >= epsilon      
ApproxEqualAssociations.Add(new SingleApproxEqual())       
ApproxEqualAssociations.Add(new DoubleApproxEqual())

Once we’ve created these two implementations and added them to our associations table, it’s time to put some syntactic sugar on top to make them a real infix operator. 

let (=~) x x' = ApproxEqualAssociations.Get().approxEqual x x'
let (/~) x x' = ApproxEqualAssociations.Get().approxNotEqual x x'

Now we have an extensible operator that can be added to at any given time.  Say if we wanted to say that some colors are considered approximately equal?  It wouldn’t be hard to implement something like that and add it to the associations table.  We can now test our behavior such as the following:

> 4.000001f =~ 4.0f;; // Testing with float32
val it : bool = true
> 5.1 /~ 5.01;; // Testing with float
val it : bool = true

The true power of this approach is that it opens up the function for future implementations that may want to enlist in our behavior.  Take this in contrast to the typical way we create operators in F# which lock us in to one type, although it can be circumvented, but it’s not well known and not well documented.

> let (+*) x y = (x * y) + (x + y);;
val ( +* ) : int -> int -> int

As you can see, we’re locked into the integer implementation which isn’t extensible.  I noted above there are ways around this and if you look through the F# implementation, you will find some pretty interesting code for how they implemented the standard operators.

What Are the Limitations?

So like everything, there are limitations.  For example, there is no such thing as compile time safety, which doesn’t actually bother many people, especially those in the dynamically typed world.  Another issue is of course the maintenance of this global association table per type class can also be a little much as it might grow over time.  It’s not the ideal solution, but it’s a solution.  Do I think that F# should incorporate this?  Absolutely!  I really do like this language feature of type classes in Haskell and there are other lessons other languages can take from it.

2 Comments

  • I remember hearing or reading an interview with Don Syme, I can't remember where but he stated that full-blown type classes will most likely never be in F# but he has been looking to a mechanism to achieve type inferred ad-hoc polymorphism (overloading) in F# without the other abilities of type classes.

    I'm guessing it will be something like this with syntactic sugar.

  • @snk_kid,

    You are correct, Don has been asked this question before in this interview at QCon last year: http://www.infoq.com/interviews/F-Sharp-Don-Syme

    This exact approach above is one of the possible solutions mentioned by Don.

    Matt

Comments have been disabled for this content.