Search code examples
haskellserializationtypeclassmultiparameter

Flexible serialization with MultiParamTypeClasses


I'm playing around with an idea for an extensible serialization library. I have the following typeclass:

class Monoid m => BuilderS m a where
  cstr :: String -> a -> m

The idea is that people can define instances for pairs of different serializers/types, like this:

import qualified Data.Serialize.Builder as B

instance Serialize a => BuilderS B.Builder a where
  cstr _ = B.fromByteString . encode

And a usage example:

sfPut :: (BuilderS a b, Monoid a) => WriterT a Identity ()
sfPut = do
  tell $ cstr "version" (5 :: Int)
  -- ...
  return ()

However, it turns out that the type of a needs to be specialized:

Could not deduce (BuilderS a Int) arising from a use of `cstr'
    from the context (BuilderS a b, Monoid a)
      bound by the type signature for
                 sfPut :: (BuilderS a b, Monoid a) => WriterT a Identity ()

i.e, the following type signature works without problems:

sfPut :: WriterT B.Builder Identity ()

Is there an obvious way I'm missing to solve this?


Solution

  • Well, if I use the typeclass you've given, then use GHCi to check types:

    > :t tell $ cstr "version" (5 :: Int)
    tell $ cstr "version" (5 :: Int) :: (MonadWriter s m, BuilderS s Int) => m ()
    

    So it looks like you need to specify that it's BuilderS a Int, not BuilderS a b. If I have

    sfPut :: BuilderS a Int => WriterT a Identity ()
    sfPut = tell $ cstr "version" (5 :: Int)
    

    It works fine. Note that you need FlexibleContexts for this as well, and the type signature on 5 is not optional.


    To further elaborate, the type signature you gave sfPut was

    BuilderS m a => WriterT m Identity ()
    

    But you had the term tell $ cstr "version (5 :: Int), which has the type

    BuilderS m Int => WriterT m Identity ()
    

    The type system couldn't unify a and Int, so it gave you an error. Additionally, if you leave off the type signature from 5 :: Int, you'd instead have

    tell $ cstr "version" 5 :: (Num a, BuilderS m a) => WriterT m Identity ()
    

    But since a doesn't appear in WriterT m Identity (), the type system doesn't know which instance of Num to use for 5 and would give you another error. Specifically, you'd get

    > let sfPut = tell $ cstr "version" 5
    Could not deduce (BuilderS s a0)
      arising from the ambiguity check for ‘sfPut’
    from the context (BuilderS s a, MonadWriter s m, Num a)
      bound by the inferred type for ‘sfPut’:
                 (BuilderS s a, MonadWriter s m, Num a) => m ()
      at <interactive>:20:5-35
    The type variable ‘a0’ is ambiguous
    When checking that ‘sfPut’
      has the inferred type ‘forall (m :: * -> *) s a.
                             (BuilderS s a, MonadWriter s m, Num a) =>
                             m ()’
    Probable cause: the inferred type is ambiguous
    

    However, if you use a monomorphic literal instead (or a value whose type is not polymorphic), then you can easily do

    > let sfPut = tell $ cstr "version" "I'm not polymorphic"
    > :t sfPut
    sfPut :: (BuilderS s [Char], MonadWriter s m) => m ()