如何根据Reader环境中的设置有条件地解析JSON?

时间:2017-06-13 09:05:24

标签: haskell aeson

有没有办法将读者环境传递给Aeson的JSON(de)序列化功能?这是一个真实的例子,说明为什么需要这样做?

-- JSON instances for decimal -- ORPHAN instances

defaultPrecision :: Word8
defaultPrecision = fromInteger 2

instance ToJSON Data.Decimal.Decimal  where
  toJSON d = toJSON $ show d

instance FromJSON Data.Decimal.Decimal  where
  -- TODO: New problem! How do we get the precision dynamically, based on
  -- the currency settings of the logged-in user
  parseJSON (Number n) = return $ Data.Decimal.realFracToDecimal defaultPrecision n
  parseJSON x = fail $ "Expectig a number in the JSON to parse to a Decimal. Received " ++ (show x)

1 个答案:

答案 0 :(得分:5)

如果实例依赖于某些运行时值,那么您真正想要的是能够在运行时创建实例。您可以在gist中为FromJSON实施Reader。但正如你正确注意到的那样,你不能对ToJSON做同样的事情,因为你不知道这个精度。最简单的解决方案是将存储精度作为数据类型中的单独字段。像这样:

data DecimalWithPrecision = MkDWP
    { value     :: Decimal
    , precision :: Word8
    }

如果您将此数据类型存储在数据库中并在用户登录后进行查询,那么这是最简单的解决方案,并且不需要您的类型级别技巧。

如果你事先不知道精度,例如用户通过控制台输入精度(我不知道为什么,但我们假设这个),那么这对你不起作用。众所周知,“类型类只是数据类型的语法糖”,您可以通过以下方式将ToJSON/FromJSON约束替换为JsonDict Money_

newtype Money_ = Money_ (Reader Word8 Decimal)

data JsonDict a = JsonDict
    { jdToJSON    :: a -> Value
    , jdParseJSON :: Value -> Parser a
    }

mkJsonDict :: Word8  -- precision
           -> JsonDict Money_

您可以使用上下文中的Word8动态创建此类词典(或类似词典),并将其传递给需要它的函数。有关详情,请参阅this blog post Gabriel Gonzalez

如果您确实希望在实例中实现toJSON,则可以使用reflection库。精度是一个自然数,使您能够使用此库。使用它基本上可以像以前的方法一样在运行时创建实例,但是您仍然拥有类型类。请参阅this blog post,其中应用类似技术使Arbitrary实例依赖于运行时值。在你的情况下,这将是这样的:

{-# LANGUAGE ScopedTypeVariables  #-}
{-# LANGUAGE UndecidableInstances #-}

import           Control.Monad.Reader (Reader, ask)

import           Data.Aeson           (FromJSON (..), Result (..), ToJSON (..),
                                       Value, fromJSON, withNumber)
import           Data.Aeson.Types     (Parser)
import           Data.Decimal         (Decimal, realFracToDecimal)
import           Data.Proxy           (Proxy (..))
import           Data.Reflection      (Reifies (reflect), reify)
import           Data.Word8           (Word8)

newtype PreciseDecimal s = PD Decimal

instance Reifies s Int => FromJSON (PreciseDecimal s) where
    parseJSON = withNumber "a number" $ \n -> do
      let precision = fromIntegral $ reflect (Proxy :: Proxy s)
      pure $ PD $ realFracToDecimal precision n

instance Reifies s Int => ToJSON (PreciseDecimal s) where
    toJSON (PD decimal) =
        let precision = reflect (Proxy :: Proxy s)
            ratDec    = realToFrac decimal :: Double
        in toJSON ratDec -- use precision if needed

makeMoney :: Decimal -> Reader Word8 (Value, Decimal)
makeMoney value = do
    precision <- fromIntegral <$> ask
    let jsoned = reify precision $ \(Proxy :: Proxy s) ->
                     toJSON (PD value :: PreciseDecimal s)
    let parsed = reify precision $ \(Proxy :: Proxy s) ->
                     let Success (PD res :: PreciseDecimal s)
                           = fromJSON jsoned in res
    pure (jsoned, parsed)

然后你可以像这样运行它来测试:

ghci> runReader (makeMoney 3.12345) 2
(Number 3.12345,3.12)