0
votes

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?

2
I guess you might need existential types or some such thing. I just want you to note that in function'' the type expression undefined::a could be written undefined::b without change in meaning, as type variables are not scoped (unless you turned the corresponding GHC extension on).Ingo

2 Answers

3
votes

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.

2
votes

leftaroundabout's answer is probably the one you want. But for completeness, here's one more thing you can do:

unwrap :: Wrapper a -> a
unwrap = error "can't actually unwrap"

function'' :: (ToString a) => Wrapper a
function'' = x
  where
    x = Wrapper (toString (unwrap x))

The idea is that I want an a to pass to toString but only Wrapper a shows up in my type. So I just define a function which takes Wrapper a and produces a – such a function can't have a real implementation, but we're not using it for its return value anyway – and apply it to the Wrapper a thing.

There's a bit of additional awkwardness because Wrapper a shows up in the result instead of an argument, but this (slightly silly) recursion takes care of that.