Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Chaining functions of type IO (Maybe a )

Tags:

haskell

I am writing a small library for interacting with a few external APIs. One set of functions will construct a valid request to the yahoo api and parse the result to a data type. Another set of functions will look up the users current location based on IP and return a data type representing the current location. While the code works, it seems having to explicitly pattern match to sequence multiple functions of type 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)

-- 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))

Is it possible to thread getLocation and run together without resorting to explicit pattern matching to "get out" of the Maybe Monad?

like image 465
user2726995 Avatar asked Sep 07 '15 15:09

user2726995


2 Answers

You can happily nest do blocks that correspond to different monads, so it's just fine to have a block of type Maybe Weather in the middle of your IO (Maybe Weather) block.

For example,

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

This simple pattern do a <- r; return f a indicates that you don't need the monad instance for Maybe at all though - a simple fmap is enough

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

and now you see that the same pattern appears again, so you can write

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

where the outer fmap is mapping over your IO action, and the inner fmap is mapping over your Maybe value.


I misinterpreted the type of getWeather (see comment below) such that you will end up with IO (Maybe (IO (Maybe Weather))) rather than IO (Maybe Weather).

What you need is a "join" through a two layer monad stack. This is essentially what a monad transformer provides for you (see @dfeuer's answer) but it is possible to write this combinator manually in the case of Maybe -

import Data.Maybe (maybe)

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

in which case you can write

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

which should have the correct type. If you are going to chain multiple functions like this, you will need multiple calls to flatten, in which case it maybe be easier to build a monad transformer stack instead (with the caveat's in @dfeuer's answer).

There is probably a canonical name for the function I've called "flatten" in the transformers or mtl libraries, but I can't find it at the moment.

Note that the function fromMaybe from Data.Maybe essentially does the case analysis for you, but abstracts it into a function.

like image 138
Chris Taylor Avatar answered Nov 05 '22 01:11

Chris Taylor


Some consider this an anti-pattern, but you could use MaybeT IO a instead of IO (Maybe a). The problem is that you only deal with one of the ways getLocation can fail—it could also throw an IO exception. From that perspective, you might as well drop the Maybe and just throw your own exception if decoding fails, catching it wherever you like.

like image 3
dfeuer Avatar answered Nov 05 '22 00:11

dfeuer