0

我有一个 API,它以以下形式返回 JSON 结果:

{
  "data": [1, 2, 3]
}

data字段可以是两个不同记录的编码,如下所示:

newtype ResultsTypeA = ResultsTypeA [ResultTypeA]
newtype ResultsTypeB = ResultsTypeB [ResultTypeB]

当我从 Haskell 查询这个 API 时,我提前知道我是在处理 aResultsTypeA还是 a ResultsTypeB,因为我在查询中明确要求它。

我苦苦挣扎的部分是 AesonToJSONFromJSON实例。由于这两种结果类型A最终B都是 的列表Int,因此我不能在 中使用模式匹配器FromJSON,因为在这两种情况下我只能匹配 a [Int]

这就是为什么我想到做以下事情:

newType ApiResponse a =
    ApiResponse {
        data :: a
    }

newtype ResultsTypeA = ResultsTypeA [ResultTypeA]
newtype ResultsTypeB = ResultsTypeB [ResultTypeB]

但是,我无法理解如何为上述内容编写ToJSONFromJSON实例,因为现在ApiResponse有一个类型参数,并且在 Aeson 文档中似乎没有一个地方可以解释如何使用涉及的类型参数派生这些实例.

另一种避免类型参数的替代方法如下:

newtype Results =
    ResultsTypeA [ResultTypeA]
  | ResultsTypeB [ResultTypeB]

newtype ApiResponse =
    ApiResponse {
        data :: Results
    }

在这种情况下,这ToJSON很简单:

instance ToJSON ApiResponse where
    toJSON = genericToJSON $ defaultOptions

但是这FromJSON让我们回到了无法在结果类型AB...之间做出决定的问题。

我也有可能完全做错了,还有第三个选项我看不到。

  • 带有类型参数的FromJSON/ToJSON实例会是什么样子ApiResponse
  • 有没有更好的替代方案与上面公开的任何东西完全不同来解决这个问题?
4

1 回答 1

2

由于结果类型 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

{-# 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

如果我们在 ghci 中加载文件,我们可以提供类型参数ApiResponse询问可用实例

ghci> :instances ApiResponse [ResultTypeA]
instance FromJSON (ApiResponse [ResultTypeA])

如果您还派生,您也FromJSON可以自动派生:ApiResponseGeneric

{-# 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

deriving stock Generic使 GHC 生成数据类型结构的表示,该结构可用于派生其他类型类的实现——这里,FromJSON. 对于那些通过Generic机器进行的推导,他们需要使用anyclass方法。

生成的实例将采用 形式FromJSON a => FromJSON (ApiResponse a),就像手写的一样。我们可以在 ghci 中再次检查:

ghci> :set -XPartialTypeSignatures
ghci> :set -Wno-partial-type-signatures
ghci> :instances ApiResponse _
instance FromJSON w => FromJSON (ApiResponse w)
instance Generic (ApiResponse w) 
于 2021-05-15T13:32:31.440 回答