Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Automatic derivation of ToJSON for (Map NewtypeOfText v)

Tags:

haskell

aeson

I have a Map where the key is a newtype of Text. I would like to automatically (as much as possible) derive ToJSON and FromJSON for this Map. aeson already has instances for ToJSON and FromJSON for Map Text v.

My verbose code that works:

{-# LANGUAGE DeriveGeneric    #-}

module Test where

import           ClassyPrelude

import           Data.Aeson                  
import           GHC.Generics                (Generic)

import qualified Data.Map                    as M

newtype MyText = MyText {unMyText::Text} deriving (Eq, Ord)

data Bar = Bar deriving (Generic)
instance ToJSON Bar
instance FromJSON Bar

data Foo = Foo (Map MyText Bar)

instance ToJSON Foo where
  toJSON (Foo x) = toJSON mp
    where mp = M.fromList . map (\(x,y) -> (unMyText x,y)) . M.toList $ x

instance FromJSON Foo where
  parseJSON v = convert <$> parseJSON v
    where convert :: Map Text Bar -> Foo
          convert =  Foo . mapFromList . map (\(x,y) -> (MyText x,y)) . mapToList

Can I do something more like the following?

data Foo = Foo (Map MyText Bar) deriving (Generic)

instance ToJSON Foo 
instance FromJSON Foo

Edit

I tried (but still no luck):

newtype MyText = MyText {unMyText::Text} deriving (Eq, Ord, ToJSON, FromJSON)
instance ToJSON Foo where
  toJSON (Foo x) = toJSON x

and

newtype MyText = MyText {unMyText::Text} deriving (Eq, Ord, ToJSON, FromJSON)
instance ToJSON Foo
like image 260
Daniel K Avatar asked Oct 20 '14 07:10

Daniel K


1 Answers

The fact that you can't automatically derive this instance is 100% correct behavior. The reason that what you expect doesn't work is that there is no way to know that the instance FromJSON (Map Text v) can be used on values of type Map MyText v. This is because the creation and manipulation of a Map is predicated on the Ord instance of its key, and there is no way (for the compiler) to know that for all x y (x == y) == (MyText x == MyText y), which is required to safely coerce Map Text v to Map MyText v. More technically, the role declaration of Map is:

type role Map nominal representational

Essentially this says that Map k v is only coercible to other maps whose first type parameter is identical. The wiki says:

we have instance Coercible a b => Coercible (T a) (T b) if and only if the first parameter has a representational role.

The class Coercible is used to do type safe coercions in recent versions of GHC (7.8?) For more information about type roles and their role in type safety, see here. The

If you plan to derive the instance for Ord MyText, then it is indeed safe to coerce Map Text v to Map MyText v, since the Ord instance is the same. This requires the use of unsafeCoerce. You still have to write the instance yourself, though:

instance ToJSON v => ToJSON (Map MyText v) where
  toJSON = toJSON . (unsafeCoerce :: Map MyText v -> Map Text v)

instance FromJSON v => FromJSON (Map MyText v) where 
  parseJSON = (unsafeCoerce :: Parser (Map Text v) -> Parser (Map MyText v)) . parseJSON 

If you plan to write your own Ord instance, the above is absolutely not safe. Your solution is correct, but not very efficient. Use the following:

  toJSON = toJSON . M.mapKeys (coerce :: MyText -> Text)
  parseJSON = fmap (M.mapKeys (coerce :: Text -> MyText)) . parseJSON

Depending on your Ord instance, you may be able to use mapKeysMonotonic instead, which would be more efficient. See the documentation of Data.Map for precisely when you can use mapKeysMonotonic.

Then, the obvious things will work:

data Bar = Bar deriving (Eq, Ord, Generic)
instance ToJSON Bar
instance FromJSON Bar

data Foo = Foo (Map MyText Bar) deriving (Generic)
instance ToJSON Foo 
instance FromJSON Foo

-- Using GeneralizedNewtypeDeriving
newtype Foo2 = Foo2 (Map MyText Bar) deriving (FromJSON, ToJSON)

Full code:

{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving, FlexibleInstances #-}

module Test where

import Data.Aeson                  
import GHC.Generics (Generic)
import qualified Data.Map as M
import Data.Map (Map)
import Data.Text (Text)
import GHC.Prim (coerce)
import Unsafe.Coerce (unsafeCoerce)
import Data.Aeson.Types (Parser)

newtype MyText = MyText {unMyText::Text} deriving (Eq, Ord, Generic, ToJSON, FromJSON)

instance ToJSON v => ToJSON (Map MyText v) where
  -- toJSON = toJSON . M.mapKeys (coerce :: MyText -> Text)
  toJSON = toJSON . (unsafeCoerce :: Map MyText v -> Map Text v)

instance FromJSON v => FromJSON (Map MyText v) where 
  -- parseJSON x = fmap (M.mapKeys (coerce :: Text -> MyText)) (parseJSON x)
  parseJSON x = (unsafeCoerce :: Parser (Map Text v) -> Parser (Map MyText v)) (parseJSON x)

data Bar = Bar deriving (Eq, Ord, Generic)
instance ToJSON Bar
instance FromJSON Bar

data Foo = Foo (Map MyText Bar) deriving (Generic)
instance ToJSON Foo 
instance FromJSON Foo

newtype Foo2 = Foo2 (Map MyText Bar) deriving (FromJSON, ToJSON)
like image 123
user2407038 Avatar answered Oct 28 '22 17:10

user2407038