我正在编写一个小型库,用于与一些外部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)
Run Code Online (Sandbox Code Playgroud)
- 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))
Run Code Online (Sandbox Code Playgroud)
是否有可能将getLocation线程化并一起运行而不需要使用显式模式匹配来"淘汰"Maybe Monad?
你可以愉快地嵌套do对应于不同monad的块,所以在块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)
Run Code Online (Sandbox Code Playgroud)
这个简单的模式do a <- r; return f a表明你根本不需要monad实例Maybe- 一个简单fmap就足够了
runMyLocation :: IO (Maybe Weather)
runMyLocation = do
r <- getLocation
return (fmap getWeather r)
where
getWeather = run . (uncurry constructQuery) . (city &&& region)
Run Code Online (Sandbox Code Playgroud)
现在你看到同样的模式再次出现,所以你可以写
runMyLocation :: IO (Maybe Weather)
runMyLocation = fmap (fmap getWeather) getLocation
where
getWeather = run . (uncurry constructQuery) . (city &&& region)
Run Code Online (Sandbox Code Playgroud)
外部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)
Run Code Online (Sandbox Code Playgroud)
在这种情况下你可以写
runMyLocation :: IO (Maybe Weather)
runMyLocation = flatten $ fmap (fmap getWeather) getLocation
where
getWeather = run . (uncurry constructQuery) . (city &&& region)
Run Code Online (Sandbox Code Playgroud)
哪个应该是正确的类型.如果您要链接多个这样的函数,则需要多次调用flatten,在这种情况下,构建monad变换器堆栈可能更容易(在@dfeuer的答案中有警告).
我在变换器或mtl库中称之为"flatten"的函数可能有一个规范名称,但目前我找不到它.
请注意该函数fromMaybe从Data.Maybe本质上是做案例分析你,但它抽象成一个函数.
有些人认为这是一种反模式,但您可以MaybeT IO a使用IO (Maybe a). 问题是你只处理一种getLocation可能失败的方法——它也可能抛出IO异常。从这个角度来看,如果解码失败,您不妨放弃Maybe并抛出自己的异常,并在任何您喜欢的地方捕获它。