Functional Dependencies & Type Families

In the past few months I have learnt a lot! Probably the coolest stuff has been about Functional Dependencies and Type Families, so this is my attempt to explain it in order to gain a better understanding and hopefully help someone else out there as well.

So please be kind if you see any mistake, let me know and I’ll try to fix it :)

A motivating example

One of the fun applications I’ve worked on is exchange-rates, which uses the RIO Monad (basically ReaderT + IO).

When defining dependencies using such effect is very common to do it using the Has typeclass approach (or how I prefer to call it, the classy lenses Has pattern) instead of passing the whole context / environment.

Following this approach, I have defined a polymorphic Ctx record that represents the application context (or dependencies). It looks as follows:

data Ctx m = Ctx
  { ctxLogger :: Logger m
  , ctxCache :: Cache m
  , ctxForexClient :: ForexClient m
  }

If we continue with the Has approach we would get something like this for our Logger m:

class HasLogger ctx where
  loggerL :: Lens' ctx (Logger m)

instance HasLogger (Ctx m) where
  loggerL = lens ctxLogger (\x y -> x { ctxLogger = y })

But… Oops, it doesn’t compile!

Couldn't match type ‘m1’ with ‘m’
      ‘m1’ is a rigid type variable bound by
        the type signature for:
          loggerL :: forall (m1 :: * -> *). Lens' (Ctx m) (Logger m1)
        at src/Context.hs:27:3-9
      ‘m’ is a rigid type variable bound by
        the instance declaration
        at src/Context.hs:26:10-47

The reason is that the compiler has no way to know that the m in Logger m (declared in the type class) is the same as the m in Ctx m (declared in the instance), therefore the inferred type ends up being Lens' (Ctx m) (Logger m1).

Functional Dependencies to the rescue

We can fix it by introducing a language extension named FunctionalDependencies, introduced in the paper Type Classes with Functional Dependencies by Mark P. Jones in March 2000.

We need to change our type class definition as below:

{-# LANGUAGE FlexibleInstances      #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses  #-}

class HasLogger ctx m | ctx -> m where
  loggerL :: Lens' ctx (Logger m)

instance HasLogger (Ctx m) m where
  loggerL = lens ctxLogger (\x y -> x { ctxLogger = y })

Our type class has now two parameters, ctx and m, and in addition we define a functional dependency ctx -> m. This means that ctx uniquely determines the type of m, which constraints the possible instances and helps with type inference.

Notice the extensions we had to enable to make this compile.

Arithmetic example

Here’s another example taken from the Fun with Functional Dependencies paper by Thomas Hallgren. What’s simpler that adding two values together?

class Add a b c | a b -> c where
  add :: a -> b -> c

instance Add Zero b b
instance Add a b c => Add (Succ a) b (Succ c)

The functional dependency is pretty clear: given a and b we can add them together and produce c. So a and b uniquely determine c.

Notice how we don’t even need to define add, specifying the types is enough! If it’s still not clear, bear with me and let’s perform type substitution step by step (feel free to skip this part):

Given this instance, all we are saying is that:

instance Add Zero b b where
  • a is defined as Zero.
  • b is defined as b.
  • c is defined as b.

Since we now know the types of a, b and c, defining the add function becomes redundant:

instance Add Zero b b where
  add Zero b = b

Clear now? Awesome! Let’s try this out in the REPL:

λ :t add (u::Three) (u::Zero)
Succ (Succ (Succ Zero))

Where u = undefined, just a convenient type alias.

Functional Dependencies have proven to be very useful since it solves a real problem. But software evolves and so Type Families were created, the topic of the next section.

Type Families

Type Families were introduced in the paper Fun with type functions by Oleg Kiselyov, Simon Peyton Jones and Chung-chieh Shan in May 2010.

This GHC extension allows functions on types to be expressed as straightforwardly as functions on values. This means that our functions are executed at compile time, during type checking.

So here’s how we can define our HasLogger class using Type Families instead:

{-# LANGUAGE TypeFamilies #-}

import Data.Kind (Type)

class HasLogger ctx where
  type LoggerF ctx :: Type -> Type
  loggerL :: Lens' ctx (Logger (LoggerF ctx))

instance HasLogger (Ctx m) where
  type LoggerF (Ctx m) = m
  loggerL = lens ctxLogger (\x y -> x { ctxLogger = y })

Our class has once again a single parameter ctx and our functional dependency is now expressed as an associated type of the class (type family). It behaves like a function at the type level, so we can call LoggerF a type function.

Notice how we explicitly define the kind of our type function to be Type -> Type (formerly * -> *). If we don’t do it the default inferred kind will just be Type (formerly *).

Arithmetic example

In a similar way, we can define the Add class using Type Families:

class Add a b where
  type AddF a b :: Type
  add :: a -> b -> AddF a b

instance Add Zero b where
  type AddF Zero b = b

instance Add a b => Add (Succ a) b where
  type AddF (Succ a) b = Succ (AddF a b)

The c parameter defined before is now replaced by the AddF a b type function. Here we define the kind of the type function as a good practice but it’s not necessary.

And again, we can try this out in the REPL:

λ :t add (u::Three) (u::Succ Zero)
Succ (Succ (Succ (Succ Zero)))
Polymorphic Mutable Ref

Furthermore, with Type Families we could define a polymorphic mutable ref class where m defines Ref (example taken from the paper Fun with type functions linked above).

class Mutation m where
  type Ref m :: Type -> Type
  newRef   :: a -> m (Ref m a)
  readRef  :: Ref m a -> m a
  writeRef :: Ref m a -> a -> m ()

instance Mutation IO where
  type Ref IO = IORef
  newRef   = newIORef
  readRef  = readIORef
  writeRef = writeIORef

instance Mutation (ST s) where
  type Ref (ST s) = STRef s
  newRef   = newSTRef
  readRef  = readSTRef
  writeRef = writeSTRef

Once the compiler knows what m is it’s over. It’ll know what the type of the mutable ref is as well. And as a bonus, type inference will work flawlessly.

Final Thoughts

Most use cases of FunctionalDependencies can be expressed using TypeFamilies, however there are some subtle differences that come to light only in complex scenarios.

Most library authors and developers prefer to use TypeFamilies nowadays. Its main advantage over FunctionalDependencies is speed but it’s also possible to express many cases that require the extensions TypeSynonymInstances, FlexibleInstances, MultiParamTypeClasses and UndecidableInstances without them.

So when is it more convenient to use the former? You can find a more detailed comparison in the following articles:

  • https://wiki.haskell.org/Functional_dependencies_vs._type_families
  • https://gitlab.haskell.org/ghc/ghc/wikis/tf-vs-fd

Special thanks to Dmitrii Kovanikov for reviewing the first draft!