Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Polymorphic Return Types Depending on Context

Tags:

types

haskell

I'm playing around with implementing a Redis client-library in Haskell and it is my goal to encode, as much as possible, the semantics of the Redis commands in Haskell's type system. Redis, for those who don't know, is a datastore, accessed over the network. I will use it to exemplify my problem, but Redis is not the focus of this question.

An Example Function

Consider the function

get :: (RedisValue a) => Key -> Redis a
get k = decodeValue <$> sendCommand ["GET", key]

It sends a command to the datastore and returns a value stored under the given Key (for this example, you can consider type Key = String). As for the return-type:

  • Redis is an instance of Monad and MonadIO. It encapsulates information about the network connection. sendCommand sends the request and returns the datastore's reply.

  • a is polymorphic, for example either Strings or ByteStrings can be returned, depending on the context.

The following code should clarify the text above.

data Redis a = ...

instance MonadIO Redis where ...
instance Monad Redis where ...

sendCommand :: [String] -> Redis String

class RedisValue a where
    decodeValue :: String -> a

-- example instances
instance RedisValue String where ...
instance RedisValue ByteString where ...

Different Context, Different Types

Redis supports a simple form of transactions. In a transaction most commands can be sent the same as outside of a transaction. However their execution is delayed until the user sends the commit command (which is called exec in Redis). Inside the transaction, the datastore only returns an acknowledgment that the command is stored for later execution. Upon commit (exec) all results of all stored commands are returned.

This means that the get-function from above looks a bit different in the context of a transaction:

get :: (RedisStatus a) => Key -> RedisTransaction a
get k = decodeStatus <$> sendCommand ["GET", key]

Note that:

  • The monadic type is now RedisTransaction to indicate the transaction context.

  • The a return type is now any instance of RedisStatus. There is an overlap between instances of RedisValue and RedisStatus. For example String is in both classes. A specialized Status data type might be only in the RedisStatus class.

The Actual Question

My question is, how can I write a function get that works in both contexts, with context-appropriate return type classes. What I need is

  • a way to give get a return type "either Redis or RedisTransaction",

  • The type a to be an instance of RedisValue in the Redis context and an instance of RedisStatus in the RedisTransaction context.

  • A function decode that automagically does the right thing, depending on the context. I assume this must come from a (multi-param) type class.

If you know how I can do this or have a pointer to some example code or even an article, you will have my thanks!

like image 441
informatikr Avatar asked Dec 08 '11 17:12

informatikr


2 Answers

First, I think that it would be better to have two different get commands. That said, here's an approach.

class RedisGet m a where
    get :: Key -> m a

instance (RedisValue a) => RedisGet Redis a where...

instance (RedisStatus a) => RedisGet RedisTransaction a where...

You need MPTCs, but no FunDeps or Type Families. Every use of get requires that enough information be available to determine both the m and a uniquely.

like image 159
sclv Avatar answered Sep 27 '22 18:09

sclv


I agree that multiparameter type classes are a good fit here. Here's an approach:

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

newtype Redis a = Redis (IO a) deriving Monad
newtype RedisTransaction a = RedisTransaction (IO a) deriving Monad

newtype Key    = Key {unKey :: String}
newtype Value  = Value {unValue :: String}
newtype Status = Status {unStatus :: String}

class Monad m => RedisMonad m a | m -> a where
  sendCommand :: [String] -> m a

instance RedisMonad Redis Value where
  sendCommand = undefined -- TODO: provide implementation                       

instance RedisMonad RedisTransaction Status where
  sendCommand = undefined -- TODO: provide implementation                       

class Decodable a b where
  decode :: a -> b

instance Decodable Status String where
  decode = unStatus

instance Decodable Value String where
  decode = unValue

get :: (RedisMonad m a, Decodable a b) => Key -> m b
get k = do
  response <- sendCommand ["GET", unKey k]
  return (decode response)

Note the use of the type isomorphisms for Value and Status: it makes things slightly stronger typed as the Strings you are having produced by your implementations of sendCommand obviously are not just arbitrary sequences of characters but instead adhere to some fixed formats for return values and statuses.

like image 45
Stefan Holdermans Avatar answered Sep 27 '22 18:09

Stefan Holdermans