问题描述
{
"data": [1,2,3]
}
data
字段可以是两个不同记录的编码,如下所示:
newtype ResultsTypeA = ResultsTypeA [ResultTypeA]
newtype ResultsTypeB = ResultsTypeB [ResultTypeB]
当我从 Haskell 查询这个 API 时,我事先知道我是在处理 ResultsTypeA
还是 ResultsTypeB
,因为我在查询中明确要求它。
我遇到困难的部分是 Aeson ToJSON
和 FromJSON
实例。由于结果类型 A
和 B
最终都是 Int
的列表,因此我无法在 FromJSON
中使用模式匹配器,因为我只能匹配 [Int]
在这两种情况下。
这就是我想到做以下事情的原因:
newType ApiResponse a =
ApiResponse {
data :: a
}
newtype ResultsTypeA = ResultsTypeA [ResultTypeA]
newtype ResultsTypeB = ResultsTypeB [ResultTypeB]
但是我无法理解如何编写上述的 ToJSON
和 FromJSON
实例,因为现在 ApiResponse
有一个类型参数,而 Aeson 文档中似乎没有任何地方是解释如何派生这些涉及类型参数的实例的地方。
另一种避免类型参数的替代方法如下:
newtype Results =
ResultsTypeA [ResultTypeA]
| ResultsTypeB [ResultTypeB]
newtype ApiResponse =
ApiResponse {
data :: Results
}
在这种情况下,ToJSON
很简单:
instance ToJSON ApiResponse where
toJSON = genericToJSON $ defaultOptions
但是 FromJSON
让我们回到无法在结果类型 A
和 B
之间做出决定的问题...
也有可能我完全做错了,还有第三种我看不到的选择。
- 如果
FromJSON
上有类型参数,ToJSON
/ApiResponse
实例会是什么样子? - 是否有更好的替代方案来解决这个问题?
解决方法
由于结果类型 A 和 B 最终都是 Int 列表,我不能在 FromJSON 中使用模式匹配器,因为在这两种情况下我只能匹配 [Int]。
如果你有一个参数化的类型,并且你正在手工编写一个 FromJSON
实例,你可以把参数本身必须有一个 FromJSON
实例作为前提。
然后,在编写解析器时,您可以将类型参数的解析器用作定义的一部分。像这样:
{-# LANGUAGE OverloadedStrings #-}
import Data.Aeson
data ApiResponse a =
ApiResponse {
_data :: a,other :: Bool
}
instance FromJSON a => FromJSON (ApiResponse a) where
parseJSON = withObject "" $ \o ->
ApiResponse <$> o .: "data" -- we are using the parameter's FromJSON
<*> o .: "other"
现在,让我们定义两个新类型,它们使用 FromJSON
借用各自的 Int
实例:
GeneralizedNewtypeDeriving
如果我们在 ghci 中加载文件,我们可以为 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE DerivingStrategies #-}
-- Make the instances for the newtypes exactly equal to that of Int
newtype ResultTypeA = ResultTypeA Int deriving newtype FromJSON
newtype ResultTypeB = ResultTypeB Int deriving newtype FromJSON
和 interrogate the available instances 提供类型参数:
ApiResponse
您还可以为 ghci> :instances ApiResponse [ResultTypeA]
instance FromJSON (ApiResponse [ResultTypeA])
自动推导 FromJSON
,如果您还推导 ApiResponse
:
Generic
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
import Data.Aeson
import GHC.Generics
data ApiResponse a =
ApiResponse {
_data :: a,other :: Bool
}
deriving stock Generic
deriving anyclass FromJSON
使 GHC 生成数据类型结构的表示,可用于派生其他类型类的实现——这里是 deriving stock Generic
。对于通过 FromJSON
机制进行的派生,他们需要使用 anyclass
方法。
生成的实例将采用 Generic
的形式,就像手写的一样。我们可以在 ghci 中再次检查:
FromJSON a => FromJSON (ApiResponse a)