I'm very new to the magic of lenses, so I'm having some trouble with this.
With reference to: https://www.fpcomplete.com/user/tel/lens-aeson-traversals-prisms
a JSON object can be traversed in the following way:
val ^? nth 0 . key "someObject" . key "version" . nth 2
for a JSON object that resembles:
"[{\"someObject\": {\"version\": [1, 0, 3]}}]"
The Maybe monad is used throughout, so if any 'accessor' fails, I get a Nothing
.
I would like to propagate the failure too, so that I know what accessor failed.
The only way I can think of doing it would be pass an array of accessors, apply them sequentially, and return an error at any point that failed. Something like this:
import Data.Aeson
import Data.Text
import Data.Vector ((!?))
import qualified Data.HashMap.Strict as HM
data MyAccessor = Nth Int | Key Text
withFailure :: Value -> [MyAccessor] -> Either String Value
withFailure val [] = Right val
withFailure val (x:xs) = case x of
Nth i -> case val of
(Array val') -> case (val' !? i) of
Just e -> withFailure e xs
_ -> Left $ "Could not get index " ++ (show i)
_ -> Left $ "Expected JSON array for index " ++ (show i)
Key k -> case val of
(Object val') -> case (HM.lookup k val') of
Just e -> withFailure e xs
_ -> Left $ "Could not get key " ++ (unpack k)
_ -> Left $ "Expected JSON object for key " ++ (unpack k)
So with this:
-- val = [[1,0,3], {"name" : "value"}]
> withFailure val [Nth 1, Key "name", Key "hello"]
Left "Expected JSON object for key hello"
> withFailure val [Nth 1, Key "name"]
Right (String "value")
Is there a more elegant way of doing this? Making an Either-ish monad for lens to use, that results in like what withFailure
is?
Since aeson is so widely used, there are a fair amount of libraries in the ecosystem that provide extra functionality on top of what is provided in aeson itself. You don’t needany of these libraries to work with JSON, but you might find them useful.
The autoderived parsers that aeson gives you won’t cut it for these sorts of situations. Fortunately, it ispossible to use aeson for any complicated JSON parsing solution you need, including the ones listed above, but it can be surprisingly nonobvious how to do so.
Inside the catchError you can handle the error and then use throwError to throw it to the service. We then register the Interceptor in the Providers array of the root module using the injection token HTTP_INTERCEPTORS.
So here’s a cheatsheet for some common operations using aeson. Note that the later examples will make heavy use of monadic code; for the more complicated use cases of aeson, there’s really no way around it.
Another possibility is to use monadic folds from Control.Lens.Action
.
Monadic folds let you pepper your fold with effectful actions, so that these actions are interelaved with the process of "exploring" the data structure.
Notice that this is different from something like mapMOf
. Monadic folds let you do things like creating parts of the structure being explored by the fold "on the fly", for example by loading them from disk, or asking the user for input.
Normal folds are directly usable as monadic folds. You just have to run them with specialized operators like (^!!)
and (^!?)
.
To introduce an effect into a monadic fold, use the act
function.
We can create a monadic fold working in the Writer
monad, and insert actions in the fold that "log" progress. Something like this:
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE FlexibleContexts #-}
import Control.Monad
import Control.Monad.Writer
import Control.Lens
import Data.Monoid
import Data.Aeson
import Data.Aeson.Lens
msg :: String -> IndexPreservingAction (Writer (Last String)) a a
msg str = act $ \a -> tell (Last . Just $ str) >> return a
main :: IO ()
main = do
let str = "[{\"someObject\": {\"version\": [1, 0, 3]}}]"
val = maybe (error "decode err") id . decode $ str :: Value
monfol = nth 0 . msg "#1"
. key "someObject" . msg "#2"
. key "version" . msg "#3"
. nth 2
(mresult,message) = runWriter $ val ^!? monfol
putStrLn $ case mresult of
Just result -> show result
Nothing -> maybe "no messages" id . getLast $ message
If you change the "version" key in the JSON to make the fold fail, the error message will be "#2".
It would be nice to use some kind of error monad like Either
instead of Writer
, to be able to pinpoint exactly the place of failure, instead of the last "checkpoint". But I'm not sure if this is possible, because the fold already represents failure by returning Nothing
.
Module Control.Lens.Reified has the ReifiedMonadicFold
newtype that offers some useful instances for monadic folds. ReifiedMonadicFold
s behave a little like the Kleisli arrows of a monad which is an instance of MonadPlus
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With