我有以下代码,它从分页的API端点抓取两页数据。我想修改query
函数以继续获取页面,直到它找不到更多数据(因此请使用查看API响应的内容替换下面代码中的take 2
。)
我的问题是,可以在不将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
是SetItemDataSources
- 仍然觉得这很奇怪,因为我看到这个函数是一个过滤器,但输入列表和输出列表是不同的(一个有monad围绕它而另一个没有&);)
答案 0 :(得分:5)
对于这些monadic迭代的情况,我通常会转向streaming库。它的界面让人想起纯粹的列表,同时仍然允许效果:
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
,take
和toList_
函数。
如果只需要第一个成功的结果,我们可以使用Alternative
变换器的Data.Foldable
实例与IO
中的ExceptT
结合使用来执行错误列表行动,直到其中一个成功。
Alternative
本身有一个IOException
实例,可以返回第一个&#34;成功&#34;,其中&#34;失败&#34;意味着抛出{{1}}。
答案 1 :(得分: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
进行一些其他更改。 编辑:请参阅下面的内容,了解我在公共汽车上的洞察力。首先,我们需要解除状态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
答案 2 :(得分:1)
如果您需要将ClientM
个对象分开(要么是在干净的状态下运行它们,要么是类似的),最好的方法是将操作链接在一起。
在此特定情况下,runClientM query ...
IO操作返回Either String [BlogPost]
。这意味着停止条件从其中一个计算中接收Left String
。
使用手工制作的 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'
希望它可以提供帮助! :)