[英]Haskell: How do I model a nested JSON with fixed outer keys and an enumerated inner key?
Consider an external API that takes as input either usd
or eur
, and accordingly returns a json, something like this:考虑一个将
usd
或eur
作为输入的外部 API,并相应地返回一个 json,如下所示:
api currency = case currency of
"usd" -> "{\"bitcoin\": {\"usd\": 20403}, \"ethereum\": {\"usd\": 1138.75}}"
"eur" -> "{\"bitcoin\": {\"eur\": 20245}, \"ethereum\": {\"eur\": 1129.34}}"
If I just needed api "usd"
, I would use Aeson's (?) generic decoding feature:如果我只需要
api "usd"
,我会使用 Aeson 的 (?) 通用解码功能:
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}
import Data.Aeson
import GHC.Generics
data Usd = Usd
{ usd :: Double
} deriving (Show, Generic)
instance FromJSON Usd
data Coin = Coin
{ bitcoin :: Usd
, ethereum :: Usd
} deriving (Show,Generic)
instance FromJSON Coin
processUsd = decode (api "usd") :: Maybe CoinUsd
But if both api "usd"
and api "eur"
are to be used, what is the best way to abstract currency
out?但是如果要同时使用
api "usd"
和api "eur"
,那么抽象currency
的最佳方法是什么?
(In case you ask what I really want to do with it, well, the answer is nothing! This example is admittedly contrived. I want to understand ways to use data
and class
in modeling a json format whose keys are constrained in some ways. I would also like to maximally use Aeson's automatic decoding feature, avoiding custom fromJSON
code to the extent possible.) (如果你问我真的想用它做什么,那么答案是什么!这个例子是人为的。我想了解在建模 json 格式时使用
data
和class
的方法,它的键在某些方面受到约束。我也想最大限度地利用Aeson的自动解码功能,尽量避免自定义fromJSON
代码。)
One option is to use nested Data.Map
:一种选择是使用嵌套
Data.Map
:
processAny :: String -> Maybe (M.Map String (M.Map String Double))
processAny currency = decode (api currency)
But this is too general.但这太笼统了。 I still want the outer keys (
"bitcoin"
etc) hardcoded/fixed.我仍然想要硬编码/固定外部键(
"bitcoin"
等)。 What are the options at this degree of pickiness?在这种挑剔程度下有哪些选择? My immediate thought is to have a generalized
Currency
type and use it as a parameter for Coin
.我的直接想法是拥有一个通用的
Currency
类型并将其用作Coin
的参数。 But I can't figure how to work it out?!但我不知道如何解决它?! Below are some vague statements that I hope convey my intent:
以下是一些模糊的陈述,我希望能传达我的意图:
data (Currency a) => Coin a
{ bitcoin :: a
, ethereum :: a
} deriving (Show,Generic)
instance FromJSON (Coin a) where
-- parseJSON x = codeIfNeeded
class (FromJSON a) => Currency a where
-- somehow abstract out {currencyName :: Double} ?!
I am not even sure if it makes any sense at all, but if it does, how do I formalize it?我什至不确定它是否有任何意义,但如果有,我该如何正式化它? Also, what is the best way to model it otherwise (while, as mentioned before, not resorting to the extremes of
Data.Map
and fully hand written parseJSON
)?此外,其他建模的最佳方法是什么(同时,如前所述,不诉诸极端的
Data.Map
和完全手写的parseJSON
)?
Let's begin by modeling elements like {"usd": 20403}
in isolation.让我们首先对像
{"usd": 20403}
这样的元素进行单独建模。 We can define a type like我们可以定义一个类型
{-# LANGUAGE DerivingStrategies #-}
newtype CurrencyAmount currency = CurrencyAmount {getCurrencyAmount :: Double}
deriving stock (Show)
parameterized with "phantom types" like:用“幻像类型”参数化,如:
data Euro -- no constructors required, used only as type-level info
data USD
This approach lets us (and forces us) to reuse the same "implementation" and operations for different currencies.这种方法让我们(并迫使我们)为不同的货币重用相同的“实现”和操作。
One operation we want to do is to parse "tagged" currency amounts.我们要做的一项操作是解析“标记”的货币金额。 But the key in the JSON varies for each currency, that is, it depends on the phantom type.
但 JSON 中的 key 因每种货币而异,即取决于 phantom 类型。 How to tackle that?
如何解决这个问题?
Typeclasses in Haskell let us obtain values from types. Haskell 中的类型类让我们从类型中获取值。 So let's write a typeclass that gives us the JSON
Key
to use for each currency:因此,让我们编写一个类型类,为我们提供用于每种货币的 JSON
Key
:
import Data.Aeson
import Data.Aeson.Key
import Data.Proxy
class Currency currency where
currencyKey :: Proxy currency -> Key -- Proxy optional with AllowAmbiguousTypes
With instances有实例
{-# LANGUAGE OverloadedStrings #-}
instance Currency Euro where
currencyKey _ = "eur"
instance Currency USD where
currencyKey _ = "usd"
Now we can write an explicit FromJSON
instance for CurrencyAmount
:现在我们可以为
CurrencyAmount
编写一个显式的FromJSON
实例:
instance Currency currency => FromJSON (CurrencyAmount currency) where
parseJSON = withObject "amount" $ \o ->
CurrencyAmount <$> o .: currencyKey (Proxy @currency)
And we can define Coin
like this:我们可以这样定义
Coin
:
{-# LANGUAGE DeriveAnyClass #-}
data Coin currency = Coin
{ bitcoin :: CurrencyAmount currency,
ethereum :: CurrencyAmount currency
}
deriving stock (Show, Generic)
deriving anyclass (FromJSON)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.