[英]Haskell: How do I model a nested JSON with fixed outer keys and an enumerated inner key?
考虑一个将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}}"
如果我只需要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
但是如果要同时使用api "usd"
和api "eur"
,那么抽象currency
的最佳方法是什么?
(如果你问我真的想用它做什么,那么答案是什么!这个例子是人为的。我想了解在建模 json 格式时使用data
和class
的方法,它的键在某些方面受到约束。我也想最大限度地利用Aeson的自动解码功能,尽量避免自定义fromJSON
代码。)
一种选择是使用嵌套Data.Map
:
processAny :: String -> Maybe (M.Map String (M.Map String Double))
processAny currency = decode (api currency)
但这太笼统了。 我仍然想要硬编码/固定外部键( "bitcoin"
等)。 在这种挑剔程度下有哪些选择? 我的直接想法是拥有一个通用的Currency
类型并将其用作Coin
的参数。 但我不知道如何解决它?! 以下是一些模糊的陈述,我希望能传达我的意图:
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} ?!
我什至不确定它是否有任何意义,但如果有,我该如何正式化它? 此外,其他建模的最佳方法是什么(同时,如前所述,不诉诸极端的Data.Map
和完全手写的parseJSON
)?
让我们首先对像{"usd": 20403}
这样的元素进行单独建模。 我们可以定义一个类型
{-# LANGUAGE DerivingStrategies #-}
newtype CurrencyAmount currency = CurrencyAmount {getCurrencyAmount :: Double}
deriving stock (Show)
用“幻像类型”参数化,如:
data Euro -- no constructors required, used only as type-level info
data USD
这种方法让我们(并迫使我们)为不同的货币重用相同的“实现”和操作。
我们要做的一项操作是解析“标记”的货币金额。 但 JSON 中的 key 因每种货币而异,即取决于 phantom 类型。 如何解决这个问题?
Haskell 中的类型类让我们从类型中获取值。 因此,让我们编写一个类型类,为我们提供用于每种货币的 JSON Key
:
import Data.Aeson
import Data.Aeson.Key
import Data.Proxy
class Currency currency where
currencyKey :: Proxy currency -> Key -- Proxy optional with AllowAmbiguousTypes
有实例
{-# LANGUAGE OverloadedStrings #-}
instance Currency Euro where
currencyKey _ = "eur"
instance Currency USD where
currencyKey _ = "usd"
现在我们可以为CurrencyAmount
编写一个显式的FromJSON
实例:
instance Currency currency => FromJSON (CurrencyAmount currency) where
parseJSON = withObject "amount" $ \o ->
CurrencyAmount <$> o .: currencyKey (Proxy @currency)
我们可以这样定义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.