Reputation: 2581
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:
^?
two times on the raw response responseBody r
).?..
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
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