Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Associate a function with a type in Haskell

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?

like image 588
mcmayer Avatar asked Aug 31 '20 08:08

mcmayer


People also ask

How do you assign a type in Haskell?

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.

What does () mean in Haskell?

() 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 () .

Can Haskell lists have different types?

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.


Video Answer


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
like image 152
chi Avatar answered Oct 23 '22 06:10

chi