类型IO的链接函数(可能是a)

时间:2015-09-07 15:41:34

标签: haskell

我正在编写一个小型库,用于与一些外部API进行交互。一组函数将构造对yahoo api的有效请求,并将结果解析为数据类型。另一组功能将基于IP查找用户当前位置并返回表示当前位置的数据类型。虽然代码有效,但似乎必须显式模式匹配才能对IO类型的多个函数(可能是a)进行排序。

-- Yahoo API

constructQuery :: T.Text -> T.Text -> T.Text
constructQuery city state = "select astronomy,  item.condition from weather.forecast" <>
                            " where woeid in (select woeid from geo.places(1)" <>
                            " where text=\"" <> city <> "," <> state <> "\")"

buildRequest :: T.Text -> IO ByteString
buildRequest yql = do
    let root = "https://query.yahooapis.com/v1/public/yql"
        datatable = "store://datatables.org/alltableswithkeys"
        opts = defaults & param "q" .~ [yql]
                          & param "env" .~ [datatable]
                          & param "format" .~ ["json"]
    r <- getWith opts root
    return $ r ^. responseBody

run :: T.Text -> IO (Maybe Weather)
run yql = buildRequest yql >>= (\r -> return $ decode r :: IO (Maybe Weather))


-- IP Lookup
getLocation:: IO (Maybe IpResponse)
getLocation = do
    r <- get "http://ipinfo.io/json"
    let body = r ^. responseBody
    return (decode body :: Maybe IpResponse)

- Combinator

runMyLocation:: IO (Maybe Weather)
runMyLocation = do
    r <- getLocation
    case r of
        Just ip -> getWeather ip
        _ ->  return Nothing
    where getWeather = (run . (uncurry constructQuery) . (city &&& region))

是否有可能将getLocation线程化并一起运行而不需要使用显式模式匹配来“淘汰”Maybe Monad?

2 个答案:

答案 0 :(得分:5)

您可以愉快地嵌套与不同monad对应的do块,因此在Maybe Weather块的中间放置IO (Maybe Weather)类型的块就可以了。

例如,

runMyLocation :: IO (Maybe Weather)
runMyLocation = do
    r <- getLocation
    return $ do ip <- r; return (getWeather ip)
  where
    getWeather = run . (uncurry constructQuery) . (city &&& region)

这个简单的模式do a <- r; return f a表示你根本不需要Maybe的monad实例 - 一个简单的fmap就足够了

runMyLocation :: IO (Maybe Weather)
runMyLocation = do
    r <- getLocation
    return (fmap getWeather r)
  where
    getWeather = run . (uncurry constructQuery) . (city &&& region)

现在你看到同样的模式再次出现,所以你可以写

runMyLocation :: IO (Maybe Weather)
runMyLocation = fmap (fmap getWeather) getLocation
  where
    getWeather = run . (uncurry constructQuery) . (city &&& region)

外部fmap映射到您的IO操作,内部fmap映射到您的Maybe值。

我误解了getWeather的类型(请参阅下面的评论),这样您最终会得到IO (Maybe (IO (Maybe Weather)))而不是IO (Maybe Weather)

您需要的是通过双层monad堆栈“加入”。这基本上是monad变换器为你提供的(参见@dfeuer的回答)但是在Maybe的情况下可以手动编写这个组合器 -

import Data.Maybe (maybe)

flatten :: (Monad m) => m (Maybe (m (Maybe a))) -> m (Maybe a)
flatten m = m >>= fromMaybe (return Nothing)

在这种情况下你可以写

runMyLocation :: IO (Maybe Weather)
runMyLocation = flatten $ fmap (fmap getWeather) getLocation
  where
    getWeather = run . (uncurry constructQuery) . (city &&& region)

应具有正确的类型。如果您要链接多个这样的函数,则需要多次调用flatten,在这种情况下,构建monad变换器堆栈可能更容易(在@dfeuer的答案中有警告)。

在变换器或mtl库中我可能称之为“flatten”的函数可能有一个规范名称,但目前我找不到它。

请注意,来自fromMaybe的函数Data.Maybe基本上会为您进行案例分析,但会将其抽象为函数。

答案 1 :(得分:0)

将getWeather更改为Maybe IpResponse->IO..并使用>>=来实现它,然后您可以执行getLocation >>= getWeather。 getWeather中的>>=是来自Maybe的那个,它将处理Just和Nothing以及来自IO的另一个getLocation>>= getWeather。 你甚至可以从Maybe中摘要并使用任何Monad: getWeather :: Monad m -> m IpResponse -> IO ..并且可以工作。