2017-06-13 415 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