Reputation: 8411
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
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
Rank2Types
language extension.void
example will no longer type check. 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
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