Search code examples
haskelltypeclass

Choose a typeclass based on return type


I want to be able to have a function, which implementation will choose a typeclass based on the manual type specification of it's return type.

Here's a contrived example: a typeclass and two instances:

class ToString a where
  toString :: a -> String

instance ToString Double where
  toString = const "double"

instance ToString Int where
  toString = const "int"

I'm able to choose the instance by calling the toString with Int type:

function :: String
function = toString (undefined :: Int)

So far so good. What I'd like to do is to be able to write the function, so it will work for any a if there exists a typeclass for it:

function' :: (ToString a) => String
function' = toString (undefined :: a)

Here, the function' doesn't compile, because the a type parameter is not mentioned anywhere in the signature and it's not possible to specify the typeclass upon calling.

So it looks like the only option is to pass the type information about the type of a into the return type:

data Wrapper a = Wrapper {
  field :: String }

function'' :: (ToString a) => Wrapper a
function'' = Wrapper $ toString (undefined :: a)

showToString :: String
showToString = field (function'' :: Wrapper Int)

I define a Wrapper type just for carrying the type information. In the showToString, my hope is that since I specify the exact type of Wrapper, then the typechecker can infer that the a in function'' is and Int and pick the Int instance of the ToString typeclass.

But the reality doesn't correspond with my hopes, this is the message from compiler

Could not deduce (ToString a0) arising from a use of `toString'

Is there a way, how to convince the compiler, that he can pick the right typeclass in the function'', because I specify the it by having the type declaration of :: Wrapper Int?


Solution

  • First, let me suggest that instead of an own Wrapper type, you use Data.Tagged.Tagged, whose purpose is exactly this kind of stuff.

    Apart from that, you need to turn on the -XScopedTypeVariables extension, otherwise the a type variable only exists in the type signature itself, but not in signatures of local bindings.

    {-# LANGUAGE ScopedTypeVariables #-}
    
    import Data.Tagged
    
    function''' :: forall a. ToString a => Tagged a String
    function''' = Tagged $ toString (undefined :: a)
    

    The explicit forall is necessary for a to actually become a scoped variable, otherwise the extension doesn't kick in.

    However....

    Actually, the best thing would probably to have the class method produce a tagged value in the first place:

    class NamedType a where
      typeName :: Tagged a String
    
    instance NamedType Double where
      typeName = Tagged "double"
    instance NamedType Int where
      typeName = Tagged "int"
    ...
    

    Or don't write your own class at all but use typeable:

    import Data.Typeable
    
    typeName' :: Typeable a => Tagged a String
    typeName' = fmap show $ unproxy typeRep
    

    Of course, that will give you the actual uppercase type names, and it might work for types you don't actually want it to.