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
?
Haskell has three basic ways to declare a new type: The data declaration, which defines new data types. The type declaration for type synonyms, that is, alternative names for existing types. The newtype declaration, which defines new data types equivalent to existing ones.
() is very often used as the result of something that has no interesting result. For example, an IO action that is supposed to perform some I/O and terminate without producing a result will typically have type IO () .
Haskell also incorporates polymorphic types---types that are universally quantified in some way over all types. Polymorphic type expressions essentially describe families of types. For example, (forall a)[a] is the family of types consisting of, for every type a, the type of lists of a.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With