优化基于镜头的 JSON 处理

Optimize lens based JSON handling

在我当前的 "learning haskell" 项目中,我尝试从第三方 api 获取天气数据。我想从以下响应正文中提取 namemain.temp 值:

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

我编写了一个 getWeather 服务来执行 IO 并转换响应以构建 GetCityWeather 数据:

....

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)

我在maybeCityNamemaybeTemp中坚持优化JSON解析部分,我的想法是:

  1. 目前 JSON 被解析了两次(我在原始响应 responseBody r 上应用了两次 ^?)。
  2. 我想获取"one shot"中的数据。 ?.. 能够获取值列表。但是我提取了不同的类型(StringDouble),所以 ?.. 不适合这里。

我正在寻找更优雅/更自然的方法来安全地解析 JSON,读取所需的值并将它们应用于数据构造函数 GetCityWeather。在此先感谢您的帮助和反馈。

更新:使用 Folds 我可以通过两个大小写匹配来解决问题

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)

正如@jpath 指出的那样,您在这里遇到的真正问题是关于 lens 和 JSON 处理的问题。问题的症结似乎是您想一次完成镜头操作。为此,请查看方便的 ReifiedFold:您想要的 "parallel" 功能已打包到 Applicative 实例中。

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))

然后,您可以尝试一次性获取您的 WeatherService

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

但是,为了错误消息的缘故,如果您计划进一步扩展它,那么利用 aesonToJSON/FromJSON 可能是更好的主意。