Associate a function with a type in Haskell

Viewed 372

Suppose you have a serializer/deserializer type class

class SerDes a where
    ser :: a -> ByteString
    des :: ByteString -> a

and it turns out that it's crucial to have a special helper function for each type a, e.g.

compress :: ByteString -> ByteString     -- actually varies with the original type

I see compress as a function that I would like to associate with each a that is a SerDes. (The word "associate" is probably a bad choice, and the reason why internet searches yield nothing.)

The example is not as contrived as it looks, for example when decompress is an optional feature of the serializer/deserializer. (Yes, the helper could be avoided by augmenting ser with a switch that controls the compression, ser:: a -> Bool -> ByteString, or better use a Config record. But let's stick with the example.)

One way to do this is a 'dummy' class, a singleton:

data For a = For

Then this will work:

class SerDes a where
    ser      :: a -> ByteString
    des      :: ByteString -> a
    compress :: For a -> ByteString -> ByteString

and a compress for a would be instantiated as

compress (For :: For MyType) input = ...

Another way, somewhat unusual, would be to stick all the functions in a record.

data SerDes a = SerDes { ser      :: a -> ByteString
                       , des      :: ByteString -> a
                       , compress :: ByteString -> ByteString 
                       }

Are there any other ways to "associate" the compress function with the type a?

1 Answers

Your For a type is known as Proxy a in the libraries.

import Data.Proxy

class SerDes a where
    ser      :: a -> ByteString
    des      :: ByteString -> a
    compress :: Proxy a -> ByteString -> ByteString

Sometimes this is generalized to a generic proxy type variable.

class SerDes a where
    ser      :: a -> ByteString
    des      :: ByteString -> a
    compress :: proxy a -> ByteString -> ByteString

There is another option, similar to proxies. Instead of forcibly adding a to the arguments, one can add a to the result type using Tagged:

import Data.Tagged

class SerDes a where
    ser      :: a -> ByteString
    des      :: ByteString -> a
    compress :: ByteString -> Tagged a ByteString

This needs to be used as unTagged (compress someByteString :: Tagged T ByteString) to tell the compiler we want the compress function for T.


Personally, I'm not a fan of proxies and tags. They were needed in the past when GHC did not allow another simpler solution, but right now they should no longer be used.

The modern approach is to turn on the harmless extensions AllowAmbiguousTypes and TypeApplications and simply write your wanted class

class SerDes a where
    ser      :: a -> ByteString
    des      :: ByteString -> a
    compress :: ByteString -> ByteString

In this approach, instead of calling compress (Proxy :: Proxy T) someByteString we will need to use the shorter compress @T someByteString where we explicitly "pass the type a we want" (T in this case), so to select the wanted compress.

Full example:

{-# LANGUAGE AllowAmbiguousTypes, TypeApplications, OverloadedStrings #-}

import Data.ByteString as BS

class SerDes a where
    ser      :: a -> ByteString
    des      :: ByteString -> a
    compress :: ByteString -> ByteString

-- bogus implementation to show everything type checks
instance SerDes Int where
   ser _ = "int"
   des _ = 42
   compress bs = BS.tail bs

-- bogus implementation to show everything type checks
instance SerDes Bool where
   ser _ = "bool"
   des _ = True
   compress bs = bs <> bs

main :: IO ()
main = BS.putStrLn (compress @Int "hello" <> compress @Bool "world")
-- output: elloworldworld
Related