7

我有以下代码从分页 API 端点获取两页数据。我想修改query函数以继续获取页面,直到它找不到更多数据(因此take 2在下面的代码中替换为查看 API 响应的内容)。

我的问题是是否可以在不将query功能更改为功能的情况下实现这一目标IO。如果是这样,我将如何去做。如果没有,有没有办法在不编写递归函数的情况下做到这一点?

这是代码:

#!/usr/bin/env stack

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}

import Servant.Client
import Network.HTTP.Client (newManager, defaultManagerSettings)

import Data.Proxy
import Servant.API

import Data.Aeson
import GHC.Generics


-- data type
data BlogPost = BlogPost
  { id :: Integer
  , title :: String
  } deriving (Show, Generic)

instance FromJSON BlogPost


-- api client
type API = "posts" :> QueryParam "_page" Integer :> Get '[JSON] [BlogPost]
api :: Proxy API
api = Proxy
posts :: Maybe Integer -> ClientM [BlogPost]
posts = client api


-- query by page
query :: ClientM [[BlogPost]]
query = sequence $ take 2 $ map posts pages
  where
    pages = [Just p | p <- [1..]]

-- main
main :: IO ()
main = do
  manager' <- newManager defaultManagerSettings
  let url = ClientEnv manager' (BaseUrl Http "jsonplaceholder.typicode.com" 80 "")
  posts' <- runClientM query url
  print posts'

我试图用它takeWhileM来做到这一点,最终使查询成为一个IO函数并传递url给它。它开始看起来很糟糕,我无法匹配类型(我觉得我需要更像(a -> m Bool) -> m [a] -> m [a]而不是(a -> m Bool) -> [a] -> m [a]什么东西takeWhileM- 仍然觉得这很奇怪,因为我将此功能视为过滤器,但输入list 和 output list 是不同的(一个有 monad,另一个没有)。

4

3 回答 3

5

对于这些一元迭代的情况,我通常会求助于流式库。它的界面让人想起纯列表,同时仍然允许效果:

import           Streaming
import qualified Streaming.Prelude               as S

repeatAndCollect :: Monad m => m (Either a r) -> m [a]
repeatAndCollect = S.toList_ . Control.Monad.void . S.untilRight

repeatAndCollectLimited :: Monad m => Int -> m (Either a r) -> m [a]
repeatAndCollectLimited len = S.toList_ . S.take len . S.untilRight

使用untilRight,taketoList_函数。


当只需要第一个成功的结果时,我们可以使用转换器的Alternative实例ExceptT结合asumfromData.Foldable来执行一系列可能出错的操作,直到其中一个成功。

IO本身有一个Alternative返回第一个“成功”的实例,其中“失败”意味着抛出一个IOException.

于 2018-05-04T19:16:06.780 回答
2

你试过unfoldM吗?

unfoldM :: Monad m => m (Maybe a) -> m [a]

让我们posts这样更新

posts :: Maybe Integer -> ClientM (Maybe [BlogPost])
posts = fmap notNil . client api where
  notNil [] = Nothing
  notNil bs = Just bs

我们的想法是进行更新query,以便您可以使用unfoldM query并取回ClientM [[BlogPost]]. 为此,类型query必须是

query :: ClientM (Maybe [BlogPost])

意思是,页码必须来自环境:

query = forever $ page >>= posts

显然,这里发生了某种形式的状态,因为我们需要一种方法来跟踪当前页码。我们可以将客户端操作包装在StateT

type ClientSM = StateT Integer ClientM

page :: ClientSM Integer
page = get <* modify (+1)

此操作需要对query和进行一些额外的更改posts编辑:见下文,了解我在公共汽车上获得的洞察力。首先,我们需要在 state monad 中解除客户端动作:

posts :: Integer -> ClientSM (Maybe [BlogPost])
posts = fmap notNil . lift . client api . Just  where
  notNil [] = Nothing
  notNil xs = Just xs

只是query需求的类型发生变化

query :: ClientSM (Maybe [BlogPost])

最后,主要操作只需要剥离 monad 堆栈并展开查询:

main = do
  manager' <- newManager defaultManagerSettings
  let url = mkClientEnv manager' (BaseUrl Http "jsonplaceholder.typicode.com" 80 "")
  result <- flip runClientM url $ flip runStateT 1 $ unfoldM query
  case result of
    Left error -> print error
    Right (posts, _) -> print posts

我没有测试过这个,但它编译


posts对国家视而不见,并且应该保持这种状态。因此,在不更改我上面的原始版本的情况下,您只需要加入query

query :: ClientSM (Maybe [BlogPost])
query = forever $ page >>= lift . posts . Just
于 2018-05-05T08:44:04.127 回答
1

如果您需要将ClientM对象分开(以干净的状态运行它们,或类似的),最好的方法是将您的操作链接在一起。

在这种特殊情况下,runClientM query ...IO 操作返回一个Either String [BlogPost]. 这意味着停止条件正在接收Left String来自其中一个计算的 a。

使用手工制作的eitherM助手,它根据Either构造函数运行两个动作之一,这里是一个相对简单的例子:
使用好的旧的或者使这个相对简单:

queryAll :: ClientEnv -> [Int] -> IO [[BlogPost]]
queryAll _ [] = return []
queryAll url (x:xs) = runClientM (posts x) url >>= either ((const.pure) []) (\b -> (b:) <$> queryAll url xs)

main :: IO ()
main = do
  manager' <- newManager defaultManagerSettings
  let url = ClientEnv manager' (BaseUrl Http "jsonplaceholder.typicode.com" 80 "")
  posts' <- queryAll url [1..]
  print posts'

希望它可以帮助!:)

于 2018-05-04T21:13:07.387 回答