我如何用 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
我有一个代表枚举的类型:
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