Tobias Dammers

programming

Object-Oriented Haskell

Part 3: Generalized Mutability

Nov 21, 2017

In Part 1, we have seen how an interface-based OOP model can be implemented in idiomatic Haskell, and we then extended this model to allow for mutable objects by using IORef in Part 2. In this part, we will extend the model further to decouple the mutable-variable implementation from mutability in the objects themselves.

Generalizing Mutable Variables

Let’s look at the bare minimum interface for mutable variables. We need the following building blocks:

We can implement these primitives for IORef like this:

newVar :: a -> IO (IORef a)
newVar = newIORef

readVar :: IORef a -> IO a
readVar = readIORef

writeVar :: IORef a -> a -> IO ()
writeVar = writeIORef

Let’s look at another mutable-variable type: TVar.

newVar :: a -> STM (TVar a)
newVar = newTVar

readVar :: TVar a -> STM a
readVar = readTVar

writeVar :: TVar a -> a -> STM ()
writeVar = writeTVar

Obviously the m type for TVar is STM.

Mutability Typeclasses

The above means that we will want the three functions there to be polymorphic over m and v. The idiomatic way of achieving that in Haskell is to use a typeclass; since two types are involved here, we are going to need the MultiParamTypeClasses extension here. So:

class MutableVars v m where
  newVar :: a -> m (v a)
  readVar :: v a -> m a
  writeVar :: v a -> a -> m ()

This works, but we will take a slightly different approach: instead of one typeclass, we will introduce two of them:

class ReadVar v m where
  readVar Write:: v a -> m a

class WriteVar v m where
  newVar :: a -> m (v a)
  writeVar :: v a -> a -> m ()

In practice, we will address two additional concerns: 1. The v type is not generally going to be mentioned in our method signatures, which means that we need to somehow tell the compiler how to infer v from a given m. 2. Whenever we are going to write to a variable, this will generally also involve reading from variables, either the same one or another.

Issue 2 is easy to address: we will simply use subclassing to make sure that WriteVar always implies ReadVar. So:

class ReadVar v m where
  readVar Write:: v a -> m a

class ReadVar v m => WriteVar v m where
  newVar :: a -> m (v a)
  writeVar :: v a -> a -> m ()

GHC has two extensions that can both solve the second problem: FunctionalDependencies (a.k.a. fundeps), and TypeFamilies. Using fundeps, this is what the typeclasses end up looking like:

class ReadVar v m | m -> v where
  readVar Write:: v a -> m a

class ReadVar v m => WriteVar v m | m -> v where
  newVar :: a -> m (v a)
  writeVar :: v a -> a -> m ()

For those unfamiliar with fundeps, the m -> v part means “the choice of v depends on the choice of m”, or, put differently, “the choice of m determines the choice of v”. Meaning that once we have written one instance for any combination of m and v, we are not allowed to write another instance that involves the same m.

With this functional dependency in place, we can specify just the m and have the compiler infer the correct v from it.

And now we’ll write instances:

instance ReadVar TVar STM where
  readVar = readTVar

instance WriteVar TVar STM where
  newVar = newTVar
  writeVar = writeTVar


instance ReadVar IORef IO where
  readVar = readIORef

instance WriteVar IORef IO where
  newVar = newIORef
  writeVar = writeIORef

Mutable Objects Generalized

Armed with the above, let’s generalize our mutable objects and interfaces, starting with the Renderable interface:

data Renderable v
  = Renderable
      { render :: forall m. ReadVar m v => Renderable v -> Graphics -> m ()
      }

The Label type also needs to be generalized:

data Label v
  = Label
      { labelPosition :: v Position
      , labelText :: v String
      }

newLabel :: (Applicative m, WriteVar v m)
         => Position
         -> String
         -> m (Label v)
newLabel position txt =
  Label <$> newVar position
        <*> newVar txt

instance (Label v) `Is` (Renderable v) where
  cast label =
    Renderable
      { render = \this g -> do
          position <- readVar (labelPosition label)
          txt <- readVar (labelText label)
          drawText g AlignLeft AlignBaseline position txt
      }

Conclusion

We have achieved two important goals here.

First, neither the Label type, nor the Render interface, nor the Render implementation for Label, mention IO or IORef anywhere, they are completely expressed in terms of the abstract ReadVar and WriteVar typeclasses. We have decoupled mutability semantics from mutability implementation. And this means that we can now use them in an STM context without further ado - at least if we also generalize our drawing primitives:

atomically $ do
  lbl <- newLabel "Hello!"
  lbl ==> render

Second, by splitting up the mutability typeclass into “read-only access” and “read-write access” typeclasses, we can declare some methods as “read-only”, and as long as our ReadVar instances are lawful, it will be impossible to implement them such that they mutate anything. At the same time, if we want to call such a read-only method from a read-write context, we can, because every read-write context is also a read-only context (because ReadVar v m => WriteVar v m), very much similar to how const works in C++.

By the way, the OOP framework laid out in this blog series is also available on Hackage, under the name boop, feel free to read along and see what it looks like when you put it all together.