zegkljan
zegkljan

Reputation: 8411

Store and retrieve an object of some typeclass - is it possible? If yes, how?

Suppose I have some typeclass Foo and some data type FooInst that is instance of Foo:

class Foo a where
  foo :: a -> String

data FooInst = FooInst String

instance Foo FooInst where
    foo (FooInst s) = s

Now, I would like to define a data type that stores an object whose type is in the Foo typeclass and be able to extract that object from inside that data type and use it.

The only way I found is to use GADTs and Rank2Types language extensions and define the data type like this:

data Container where
  Container :: { content :: Foo a => a } -> Container

However, the problem is, that I cannot use the content selector to get the content out of the Container:

cont :: Container
cont = Container{content = FooInst "foo"}

main :: IO ()
main = do
  let fi = content cont
  putStrLn $ foo fi

results to a compile error

Cannot use record selector ‘content’ as a function due to escaped type variables
Probable fix: use pattern-matching syntax instead

but when I modify the let ... line to

let Conainer fi = cont

I get a rather funny error

My brain just exploded
I can't handle pattern bindings for existential or GADT data constructors.
Instead, use a case-expression, or do-notation, to unpack the constructor.

And if I try to, again, modify the let ... line to use case-expression

let fi = case cont of
           Container x -> x

I get a a different error

Couldn't match expected type ‘t’ with actual type ‘a’
  because type variable ‘a’ would escape its scope
This (rigid, skolem) type variable is bound by
  a pattern with constructor
    Container :: forall a. (Foo a => a) -> Container,
  in a case alternative
  at test.hs:23:14-24

So, how can I store a typeclassed thing and retrieve it back?

Upvotes: 3

Views: 190

Answers (2)

behzad.nouri
behzad.nouri

Reputation: 77961

With:

data Container where
    Container :: {content :: Foo a => a} -> Container

the type class constraint is not even enforced. That is

void :: Container
void = Container {content = 42 :: Int}

type checks even though 42 :: Int is not an instance of Foo.

But if you change to:

data Container where
    Container :: Foo a => {content :: a} -> Container
  1. you no longer need Rank2Types language extension.
  2. the type class constraint is enforced; therefore above void example will no longer type check.
  3. further, you may invoke foo (or any other function with signature Foo a => a -> ...) on the content with pattern matching:

    case cont of Container {content = a} -> foo a
    

Upvotes: 6

Reid Barton
Reid Barton

Reputation: 15009

For example

main :: IO ()
main = do
  case cont of
    Container fi -> putStrLn $ foo fi

You need to encapsulate your use of the existentially-typed field fi in an expression whose type does not depend on the type of fi; here putStrLn $ foo fi which has type IO ().

This example is quite useless since the only thing you can do to the content field of a Container is call foo on it, so you might as well just call foo before constructing the container and give the field type String. But it's more interesting if Foo has operations with types like a -> a -> a, or Container has multiple fields with types that involve the same existentially quantified variable, etc.

Upvotes: 2

Related Questions