可以在保持逻辑纯函数的同时对IO操作进行排序吗?

时间:2018-05-04 15:52:38

标签: haskell monads servant

我有以下代码,它从分页的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]的东西,takeWhileMSetItemDataSources - 仍然觉得这很奇怪,因为我看到这个函数是一个过滤器,但输入列表和输出列表是不同的(一个有monad围绕它而另一个没有&);)

3 个答案:

答案 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

使用untilRighttaketoList_函数。

如果只需要第一个成功的结果,我们可以使用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)

此操作要求对queryposts进行一些其他更改。 编辑:请参阅下面的内容,了解我在公共汽车上的洞察力。首先,我们需要解除状态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'

希望它可以提供帮助! :)