在工作中,我使用泛型为API和数据库类型轻松派生Data.Aeson.ToJSON和Database.Selda.SqlRow的实例。这非常有用,但是我经常希望可以编写类型。
作为一个简单的示例,也许用户想要创建一个新帐户。它们提供了基本信息,其中不包含数据库ID。但是我需要用ID将其发送回给他们。
data AccountInfo = AccountInfo
{ firstName :: Text
, lastName :: Text
} deriving (Show, Eq, Generic)
data Account = Account
{ accountId :: Id Account
, firstName :: Text
, lastName :: Text
} deriving (Show, Eq, Generic)
我不能简单地将AccountInfo嵌套在Account中,因为我希望Aeson实例是平坦的(所有字段都在顶层),而Selda要求它是平坦的才能将其存储在数据库中。
-- This won't work because the Aeson and Selda outputs aren't flat
data Account = Account
{ accountId :: Id Account
, info :: AccountInfo
} deriving (Show, Eq, Generic)
我想创建一个产品类型,该产品类型可以将两种类型组合在一起,但可以使字段变平。
data Aggregate a b = Aggregate a b
data AccountId = AccountId
{ accountId :: Id Account
} deriving (Show, Eq, Generic)
type Account = Aggregate AccountId AccountInfo
我知道如何为这种类型手动编写ToJSON和FromJSON实例,但是我不知道如何编写SQLRow实例。
作为一种学习泛型的练习,我想为Aggregate
编写一个Generic实例,其中Aggregate AccountId AccountInfo
具有与上面的Account
的第一个定义完全相同的表示形式(三个字段都变平了)
我该怎么做?我已经读过一天有关泛型的文章,而且我还是很困。
答案 0 :(得分:1)
对于泛型, user 定义了一种数据类型,该数据类型将基于在其通用派生结构上定义的操作来继承某些操作(例如toJSON
)。它不会在旧的基础上创建新的类型。如果您正在寻找泛型来基于旧类型创建新类型,您会感到沮丧。
更有针对性的是,“我想为Aggregate编写通用实例”这样的语句没有任何意义。我们创建类(class ToJSON
)的通用表示实例,而不是数据结构。通用JSON实例已经编写好了...
幸运的是,好的解决方案可能很容易。相反,您只需要一种处理JSON对象集合的方法。下面,我演示如何组合具有JSON.Object
表示形式的两种数据类型。由于JSON.Object
只是一个哈希表,可以通过union
进行组合,因此我只是将haskell值转换为对象并执行并集。有改进的空间,但这就是想法。
import qualified Data.Aeson as JSON
import qualified Data.Aeson.Types as JSON
import GHC.Generics
import qualified Data.HashMap.Lazy as HashMap
data A = A { fieldA :: String } deriving (Show,Eq,Generic)
data B = B { fieldB :: String } deriving (Show,Eq,Generic)
instance ToJSON A
instance ToJSON B
instance FromJSON A
instance FromJSON B
toObj :: ToJSON a => a -> Maybe JSON.Object
toObj = JSON.parseMaybe parseJSON . toJSON
toJSONAB :: A -> B -> Maybe JSON.Value
toJSONAB a b = do
aObj <- toObj a
bObj <- toObj b
return . JSON.Value $ HashMap.union aObj bObj
这时,当您需要输出JSON时,将调用toJSONAB
而不是toJSON
。包括从输出中检索A或B。就是
(toJSONAB a b >>= JSON.parseMaybe parseJSON) :: Maybe <Desired Type>
将解析A或B,具体取决于提供(推断)的类型签名。
以上是您想要的核心。您可以为所需的任何数据组合设置类似的功能。缺少的是创建新的类型,如下所示:
data AB = AB A B
这可以确保代码安全。毕竟,您对特定类型感兴趣,而不是对类型的JSON表示的临时集合感兴趣。要通过泛型做到这一点,我建议使用一个新的类,如下所示(未试用和未完成),
class ToFlatJSON a where
toFlatJSON :: a -> JSON.Value
default toFlatJSON :: (Generic a, GToFlatJSON (Rep a)) => a -> JSON.Value
toFlatJSON = gToFlatJSON . from
class GToFlatJSON a where
gToFlatJSON :: a p -> JSON.Value
并提供GToFlatJSON
的实例GHC.Generics
。
instance (ToJSON a) => GToFlatJSON (K1 i a) where
gToFlatJSON (K1 a) = toJSON a
instance (GToFlatJSON a) => GToFlatJSON (a :*: a') where
gToFlatJSON (a :*: a') = cmb (gToFlatJSON a) (toFlatJSON a')
where
cmb = someFunctionLike_toJSONAB
instance (GToFlatJSON a) => GToFlatJSON (M1 t i a) where
gToFlatJSON (M1 _ _ a) = gToFlatJSON a
where
cmb = someFunctionLike_toJSONAB
然后,您将可以像使用ToFlatJSON
一样定义空白的ToJSON
实例
instance ToFlatJSON a
,并使用toFlatJSON
代替toJSON
。您可以用术语toJSON
来定义toFlatJSON
。那么,对于每种数据类型,您将需要:
instance ToFlatJSON AB
instance ToFlatJSON AB => ToJSON AB where
toJSON = toFlatJSON
因此,总而言之,您可以通过使用JSON表示本身(即它们的对象表示的并集)来轻松地创建组合类型。您可以直接使用它们的fromJSON
恢复原始类型。无法重载To/FromJSON
通用实例,但是您可以创建一个新的类似类及其通用实例。我个人建议此应用程序不要使用泛型。我认为为您的数据类型自定义To / FromJSON将是最直接的方法。