2017-06-13 209 views
4

有什么办法可以将阅读器环境传递给Aeson的JSON(de)序列化函数吗?这是一个真实的例子,为什么这可能是必需的?如何根据Reader环境中的设置条件解析JSON?

-- 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) 
+0

'实例FromJSON(读者十进制)'或更好:'NEWTYPE DecimalWithPrec = d( Reader Precision Decimal);实例FromJSON DecimalWithPrec'。这仍然不会允许您根据环境进行解析选择,但这不是您的示例所需的。 – user2407038

+0

@ user2407038听起来像这应该是一个答案:) –

+0

@ user2407038我试着去你的方法,并得到以下 - https://gist.github.com/saurabhnanda/6b2eaa437be9a2fff14540e0dcbbc334 - 但我怎么写'ToJSON'实例? –

回答

4

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

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

如果此数据类型存储在数据库和用户登录后查询它,那么这是最简单的解决方案,并没有从您所需要类型级别的技巧。

如果你事先不知道精度,例如用户通过控制台输入精度(我不知道为什么,但是让我们假设这个),那么这对你来说不起作用。大家都知道,«类型类数据类型只是语法糖»,您可以通过以下方式更换JsonDictToJSON/FromJSON约束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实现,则可以使用库。精确度是一个自然数,可让您使用此库。使用它你基本上可以像以前的方法一样在运行时创建实例,但是你仍然有你的类型类。请参阅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) 
+0

使用反射的方法是否可以与'Data.Aeson'中的'decode'一起使用? –

+0

你应该提到最初的Functonal Pearl:Implicit Configurations http://okmij.org/ftp/Haskell/tr-15-04.pdf – phadej

+0

@MichalCharemza我认为不是......因为'decode'不是一个类型类方法。 – Shersh