1

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.

Community
  • 1
  • 1
Sal
  • 4,312
  • 1
  • 17
  • 26
  • 2
    Perhaps worth noting that in this case you don't even need GADTs; it's enough to enable `ExistentialQuantification` to write something like `data Encapsulated = forall a. Show a => Encapsulate a` – Bartek Banachewicz Apr 01 '16 at 13:04
  • For many (most?) purposes, and almost surely this one, `Dynamic` is conceptual overkill and a `Typeable` constraint is sufficient. `data Box where Box :: Typeable a => a -> Box`. Then you can use functions from `Data.Typeable` to `Maybe` get a value out of a box. – dfeuer Apr 02 '16 at 03:39
  • In GHC 8.2, `Typeable` is expected to become substantially more powerful, more than subsuming the functionality currently offered by `Dynamic`. – dfeuer Apr 02 '16 at 03:40

2 Answers2

4

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

Random Dev
  • 51,810
  • 9
  • 92
  • 119
  • yep, I too thought the same way, and was curious if there is any way to preserve this information using GADTs while forcing the type of Map to be homogeneous in same way (so, a GADTs function has to inspect the type). `Dynamic` seems right way to go then. – Sal Apr 01 '16 at 14:42
4

@Carsten answer is obviously right, but my two cents that helped me understand that before.

When you write:

getTypedObject :: Encapsulated -> a

What you are "saying" is:

getTypedObject is a function that can take a value of the Encapsulated type and its result can be used whenever any type whatsoever is needed.

You can't obviously satisfy that, and the compiler won't allow you to try. You can only use the knowledge about the value inside of Encapsulated to bring out something meaningful based on the Contract. In other words, if the Contract wasn't there, there'd be no way for you to do anything meaningful with that value.

The concept here can be succintly described as type erasure and is also present in other languages, C++ being one I know of. Hence the value is in erasing every information about the type except the things you want to preserve via the contract they satisfy. The downside is that getting the original types back requires runtime inspection.


As a bonus, here's how a dynamic approach might work:

{-# LANGUAGE GADTs #-}

import Unsafe.Coerce

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

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

printString :: String -> IO ()
printString = print

x = Encapsulate "xyz"
y = getTypedObject x

main = printString y

But it's very easy to see how that could break, right? :)

Bartek Banachewicz
  • 38,596
  • 7
  • 91
  • 135
  • yep, very easy to break indeed. I was thinking of constraining the type `a` to that of `Contract a`, the type class. Still, we do need to inspect the type to make sure it is correct. So, `Dynamic` seems right approach to me for that. – Sal Apr 01 '16 at 14:45