Erlang concurrency in the Haskell type system (04 Sep 2006)

Say that we wanted to implement message-passing concurrency in Haskell. Haskell is a nice language which is ruled by its type system like a Mafia don. (It's easy to design yourself into a corner from which there's nowhere to go if you're not careful.)

From first glance, things look promising. GHC has lightweight threads and message channels already. But, unlike Erlang, the message channels are typed. So what's the type of a message? Well, we could implement a ping/pong example with:

data Message = Ping | Pong

That works fine, but it means that for every different type of message in the system we have to have an entry in that union type.

So let's try using the class system to define an interface which an actor can implement:

data Message b = Xon Int | Xoff Int | Reparent Int b

class DataSource a where
  xon :: a -> Int -> IO ()
  xoff :: a -> Int -> IO ()
  reparent :: DataSink b => a -> Int -> b -> IO ()

The message sending operations are all implemented as IO () functions. Note that we have also used the type system to enforce that any actor passed to reparent has to implement the DataSink interface. Sadly, Haskell can't cope with cycles in the module graph, although cycles in the reference graph are just fine. So if DataSource named DataSink they would both have to be in the same module.

newtype MySinkThread = MySinkThread (Chan DataSink.Message)

instance DataSink.DataSink MySinkThread where
  write (MySinkThread chan) fd dat = writeChan chan $ DataSink.Write fd dat
  closed (MySinkThread chan) fd = writeChan chan $ DataSink.Closed fd

There's the instance of a DataSink class, which has two messages: write and closed. The methods just add a message to the channel object. If the actor had several interfaces to implement we would need to create a union type of the Message types of each of the interfaces. This means that having default values for the class methods isn't going to work because we don't know how to wrap the values which get pushed on the channel. I expect we can use templates to generate all the instance decls.

We can use a monad very like ReaderT to thread the channel through the body of the actor:

newtype ActorT t m a = T (t -> m a)

runActor :: t -> ActorT t m a -> m a
runActor thread (T func) = func thread

instance Monad m => Monad (ActorT t m) where
  return a = T (\_ -> return a)
  T func >>= k = T (\r -> do a <- func r
                             let T func2 = k a
                             func2 r)

instance MonadTrans (ActorT t) where
  lift c = T (\_ -> c)

this :: ActorT t IO t
this = T (\r -> return r)

And then there's the question of how to write receive. Currently I don't have a better solution than so pass a series of values to a unary lambda with a case statement and to catch the exception if the match fails:

receive $ \x -> case x of
                  DataSink.Write fd d -> ....

The receive also needs a way to store the values which didn't match so that they can be tried with future receives. Probably the ActorT can carry a list of such values. Also, any pattern match failure in the receive body will cause the next message to be tried - I don't know any way round that.

In short: could work fairly well with some work