Search code examples
jsonhaskellaesonlenses

Optimize lens based JSON handling


In my current "learning haskell" project I try to fetch weather data from a third party api. I want to extract the name and main.temp value from the following response body:

{
  ...
  "main": {
    "temp": 280.32,
    ...
  },
  ...
  "name": "London",
  ...
}

I wrote a getWeather service to perform IO and transform the response to construct GetCityWeather data:

....

data WeatherService = GetCityWeather String Double
    deriving (Show)

....

getWeather :: IO (ServiceResult WeatherService)
getWeather = do
  ...

  response <- httpLbs request manager

  ...

  -- work thru the response
  return $ case ((maybeCityName response, maybeTemp response)) of
    (Just name, Just temp) -> success name temp
    bork                   -> err ("borked data >:( " ++ show bork))

  where
    showStatus r    = show $ statusCode $ responseStatus r
    maybeCityName r = (responseBody r)^?key "name"._String
    maybeTemp r     = (responseBody r)^?key "main".key "temp"._Double
    success n t     = Right (GetCityWeather (T.unpack n) t)
    err e           = Left (SimpleServiceError e)

I stuck optimizing the JSON parsing part in maybeCityName, and maybeTemp, my thoughts are:

  1. Currently the JSON is parsed twice (I apply ^? two times on the raw response responseBody r).
  2. I would like to get the data in "one shot". ?.. is able to get a list of values. But I extract different types (String, Double) so the ?.. does not fit here.

I'm looking for more elegant / more natural ways to safely parse JSON, read desired the values and apply them to the data constructor GetCityWeather. Thanks in advance for any help and feedback.

Update: using Folds I am able to solve the problem with two case matches

getWeather :: IO (ServiceResult WeatherService)
getWeather = do
  ...
  let value = decode $ responseBody response
  return $ case value of
    Just v -> case (v ^? weatherService) of 
        Just wr -> Right wr
        Nothing -> err "incompatible data"
    Nothing     -> err "bad json"

  where
     err t = Left (SimpleServiceError t)

weatherService :: Fold Value WeatherService
weatherService = runFold $ GetCityWeather
  <$> Fold (key "name" . _String . unpacked)
  <*> Fold (key "main" . key "temp" . _Double)

Solution

  • As @jpath point out, the real problem you have here is one about lens and JSON handling. The crux of the issue seems to be that you want to do the lens operation all at once. For that, check out the handy ReifiedFold: the "parallel" functionality you want is packed into the Applicative instance.

    import Control.Lens
    import Data.Aeson
    import Data.Aeson.Lens
    import Data.Text.Lens ( unpacked )
    
    -- | Extract a `WeatherService` from a `Value` if possible
    weatherService :: Fold Value WeatherService
    weatherService = runFold $ GetCityWeather
      <$> Fold (key "name" . _String . unpacked)
      <*> Fold (key "main" . key "temp" . _Double))
    

    Then, you can try to get your WeatherService all at once:

    ...
    -- work thru the response
    let body = responseBody r
    return $ case body ^? weatherService of
      Just wr -> Right wr
      Nothing -> Left (SimpleServiceError ("borked data >:( " ++ show body))
    

    However, for the sake of error messages, it might be a better idea to take advantage of aeson's ToJSON/FromJSON if you plan on scaling this more.