简体   繁体   English

Haskell:如何使用固定的外键和枚举的内键对嵌套 JSON 建模?

[英]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:考虑一个将usdeur作为输入的外部 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 格式时使用dataclass的方法,它的键在某些方面受到约束。我也想最大限度地利用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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM