连接类型为 IO (Maybe a) 的函数

5

我正在编写一个小型库,用于与几个外部API进行交互。其中一组函数将构建一个有效的请求到Yahoo API,并解析结果到一个数据类型中。另一组函数将根据IP查找用户当前位置,并返回表示当前位置的数据类型。虽然代码可以工作,但似乎必须显式地模式匹配以对多个类型为IO (Maybe 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)

-- 组合器

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

在不使用显式模式匹配“退出”Maybe Monad的情况下,是否可能同时运行getLocation和run线程?

3个回答

5

你可以愉快地嵌套与不同monads对应的do块,因此在IO (Maybe Weather)块中间有一个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)

你需要通过两层单子栈进行 "join"。这本质上就是一个单子变换器为你提供的(参见 @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,在这种情况下,建立一个单子变换器堆栈可能更容易(具体可参考 @dfeuer 的回答)。

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

请注意,Data.Maybe 中的函数 fromMaybe 实际上为您进行了案例分析,但将其抽象为一个函数。


感谢回复,但这将导致一个类型为 runMyLocation :: IO(Maybe(IO(Maybe Weather)))的函数。 有没有办法避免嵌套IO(Maybe(IO Maybe v))? - user2726995
抱歉,我误读了getWeather的类型,认为它是IpResponse -> Weather而不是IpResponse :: IO (Maybe Weather)。我会提供一个澄清的编辑。 - Chris Taylor

3

有些人认为这是一种反模式,但是您可以使用MaybeT IO a代替IO (Maybe a)。问题在于您只处理了getLocation可能失败的一种方式——它也可能抛出一个IO异常。从这个角度来看,如果解码失败,您可以放弃Maybe并自己抛出异常,在任何需要捕获它的地方进行捕获。


2
顺便问一下,为什么这是反模式? - Yuuri
4
@Yuuri,因为这会让人觉得你总是会得到一个结果或者是“空”,但实际上你可能会遇到某些异常,例如网络超时异常。 - dfeuer
我不明白为什么你认为它看起来像“总是一个Nothing的结果”。大多数Haskeller应该足够熟悉单子变换器,知道它们如何“里外”工作。 - leftaroundabout
@leftaroundabout,我确信我曾经读过一篇反对那种风格的长篇抨击文章,但是我似乎找不到它了。我还没有找到自己最喜欢的错误处理方法——它们似乎都有不同的糟糕之处(在所有语言中都是如此)。 - dfeuer

0
将getWeather更改为具有Maybe IpResponse-> IO..,并使用>>=来实现它,然后您可以执行getLocation >>= getWeather。 getWeather中的>>=是来自Maybe的那个,它将处理Just和Nothing以及其他getLocation>>= getWeather来自IO的那个。
您甚至可以从Maybe中抽象出来,并使用任何Monad:getWeather :: Monad m -> m IpResponse -> IO ..,都可以正常工作。

我不确定你在这里想要表达什么,但无论是什么似乎都缺失了。 - dfeuer
将 getWeather 更改为具有 Maybe IpResponse->IO 的形式,并使用 >>= 实现它,然后您可以执行 getLocation >>= getWeather。getWeather 中的 >>= 是来自 Maybe 的那个,它将处理 Just 和 Nothing,而其他 getLocation>>= getWeather 则是来自 IO 的那个。 - Massyl Nait
你可以编辑你的回答并包含这个建议。我个人不喜欢它,但它是合法的。就目前而言,你的回答不是一个答案。 - dfeuer
我同意这并不优雅,但我也不喜欢你的建议,即仅用转换器处理这种情况。无论如何。 - Massyl Nait
有了我的建议,你甚至可以抽象出Maybe。getWeather :: Monad m -> m IpResponse -> IO .. 就能够工作。 - Massyl Nait

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接