Search code examples
haskellgadt

Heterogeneous Data.Map in Haskell


Is it possible to do heterogeneous Data.Map in Haskell with GADT instead of Dynamic? I tried to model heterogeneous collection as laid out in this answer:

{-# LANGUAGE GADTs #-}

class Contract a where
   toString :: a -> String

data Encapsulated where
   Encapsulate :: Contract a => a -> Encapsulated

getTypedObject :: Encapsulated -> a
getTypedObject (Encapsulate x) = x

The idea being that Encapsulated could be used to store different objects of TypeClass a, and then extracted at run-time for specific type.

I get error about type of x not matching Contract a. Perhaps I need to specify some kind of class constraints to tell GHC that type of x in Encapsulate x is same as a in Contract a?

T.hs:10:34:
    Couldn't match expected type ‘a’ with actual type ‘a1’
      ‘a1’ is a rigid type variable bound by
           a pattern with constructor
             Encapsulate :: forall a. Contract a => a -> Encapsulated,
           in an equation for ‘getTypedObject’
           at T.hs:10:17
      ‘a’ is a rigid type variable bound by
          the type signature for getTypedObject :: Encapsulated -> a
          at T.hs:9:19
    Relevant bindings include
      x :: a1 (bound at T.hs:10:29)
      getTypedObject :: Encapsulated -> a (bound at T.hs:10:1)
    In the expression: x
    In an equation for ‘getTypedObject’:
        getTypedObject (Encapsulate x) = x

I am trying this approach because I have JSON objects of different types, and depending on the type that is decoded at run-time over the wire, we want to retrieve appropriate type-specific builder from Map (loaded at run-time IO in main from configuration files, and passed to the function) and pass it decoded JSON data of the same type.

Dynamic library would work here. However, I am interested in finding out if there are other possible approaches such as GADTs or datafamilies.


Solution

  • your problem is that you push the a out again (which will not work) - what you can do is using the contract internally like this:

    useEncapsulateContract :: Encapsulated -> String
    useEncapsulateContract (Encapsulate x) = toString x
    

    basically the compiler is telling you everything you need to know: inside you have a forall a. Contract a (so basically a constraint a to be a Contract)

    On getTypedObject :: Encapsulated -> a you don't have this constraint - you are telling the compiler: "look this works for every a I'll demand"

    To get it there you would have to parametrize Encapsulated to Encapsulated a which you obviously don't want.

    The second version (the internal I gave) works because you have the constraint on the data-constructor and so you can use it there


    to extent this a little:

    this

    getTypedObject :: Contract a => Encapsulated -> a
    getTypedObject (Encapsulate x) = x
    

    wouldn't work either as now the you would have Contract a but still it could be two different types which just share this class.

    And to give hints to the compiler that both should be the same you would have to parametrize Encapsulate again ....

    right now by doing this:

    Encapsulate :: Contract a => a -> Encapsulated
    

    you erase that information