泛型-创建结合了所有字段的记录产品

时间:2019-01-11 16:15:58

标签: haskell generics record typeclass

在工作中,我使用泛型为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的第一个定义完全相同的表示形式(三个字段都变平了)

我该怎么做?我已经读过一天有关泛型的文章,而且我还是很困。

1 个答案:

答案 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将是最直接的方法。