我如何用 Aeson 解析枚举?

How do I parse an Enum with Aeson?

我有一个代表枚举的类型:

data FooBar = Foo | Bar deriving (Show, Enum)

我想使用 aeson 从 JSON 中的数值解析它,即 JSON 文件中的 0 应该 return Foo,1 应该 return Bar .

我最好的尝试如下:

instance FromJSON FooBar where
    parseJSON (Number n) = return (case maybeInt of
                                     Just i -> (toEnum i)
                                     Nothing -> ??)
      where maybeInt = (Scientific.toBoundedInteger n)
    parseJSON _ = mzero

但这并不能真正处理错误情况。执行此操作的最佳方法是什么?

您可以利用您的解析器是 Monad(或者更准确地说,是 MonadFail)这一事实,因此您可以使用 fail 方法使其失败。有了它,您的代码将如下所示:

instance FromJSON FooBar where
    parseJSON (Number n) =
      case maybeInt of
        Just i -> return (toEnum i)
        Nothing -> fail "Out of bounds"
      where
        maybeInt = Scientific.toBoundedInteger n
    parseJSON _ = mzero

怎么样:

import Control.Monad
import Control.Monad.Fail as F

data FooBar = Foo | Bar deriving (Show, Enum, Bounded) -- note Bounded instance

instance FromJSON FooBar where
  -- parseJSON is the one for Int, which internally uses the Scientific function
  -- you used yourself
  -- Control.Monad.Fail.MonadFail Parser, so safeToEnum may have type
  -- safeToEnum :: Int -> Parser FooBar
  -- (Control.Monad.<=<) is (.) but in the Kleisli category
  -- (<=<) :: (b -> m c) -> (a -> m b) -> (a -> m c)
  -- (.)   :: (b ->   c) -> (a ->   b) -> (a ->   c)
  -- (f <=< g) x = do { x' <- g x;   f x' }
  -- (f  .  g) x = let  x' =  g x in f x'
  -- it's always nice when something can just be a composition of smaller things
  parseJSON = safeToEnum <=< parseJSON

-- using fail over mzero gives nicer error messages
-- (mzero = fail "mzero" for Parser, which is *very* informative)
-- this is a reusable function that can also produce Maybes, Eithers, etc.
-- depending on context, and works for lots of enumerations, since
-- many are Bounded (e.g. Bool, Char, etc.), and user-defined ones almost
-- always are.
safeToEnum :: (Enum a, Bounded a, MonadFail m) => Int -> m a
safeToEnum i = if i < min
                  then F.fail $ show i ++ " is less than the minimum, " ++ show min
                  else if i > max
                          then F.fail $ show i ++ " is greater than the maximum, " ++ show max
                          else return result
  where min = fromEnum (minBound `asTypeOf` result)
        max = fromEnum (maxBound `asTypeOf` result)
        result = toEnum i -- hooray laziness
        -- defining result and using it in min and max is safe, because
        -- asTypeOf ignores the second argument; it's just there to clarify
        -- to the type system which instance of Bounded we're talking about
        -- it's not strictly necessary in GHC Haskell, because ScopedTypeVariables
        -- would allow min = fromEnum (maxBound :: a)
        -- but using asTypeOf keeps the language extension count down

产生如下结果:

> data D = D { dInt :: Int, dFooBar :: FooBar } deriving (Show, Generic, FromJSON)

> safeToEnum 3 :: Maybe FooBar
Nothing
-- discards error messages
> safeToEnum 3 :: Result FooBar
Error "3 is greater than the maximum, 1"
-- keeps error messages

> eitherDecode "{ \"dInt\": 5, \"dFooBar\": 1 }" :: Either String D
Right (D {dInt = 5, dFooBar = Bar})
> eitherDecode "{ \"dInt\": 0, \"dFooBar\": -10 }" :: Either String D
Left "Error in $.dFooBar: -10 is less than the minimum, 0"
-- safeToEnum doesn't cause runtime errors like toEnum
> toEnum 10 :: FooBar
*** Exception: toEnum{FooBar}: tag (10) is outside of enumeration's range (0,1)
CallStack (from HasCallStack):
  error, called at T.hs:8:41 in main:T

> eitherDecode "{ \"dInt\": 0, \"dFooBar\": 0.5 }" :: Either String D
Left "Error in $.dFooBar: Int is either floating or will cause over or underflow: 0.5"
-- Scientific.toBoundedInteger is there for you, too
> eitherDecode "{ \"dInt\": 0, \"dFooBar\": 1e42 }" :: Either String D
Left "Error in $.dFooBar: Int is either floating or will cause over or underflow: 1.0e42"
-- and aeson tags the errors with their location