我正在编写一个小型库,用于与一些外部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?
答案 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 ..
并且可以工作。