Nicolas Heimann
Nicolas Heimann

Reputation: 2581

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)

Upvotes: 0

Views: 226

Answers (1)

Alec
Alec

Reputation: 32309

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.

Upvotes: 1

Related Questions