How can I untangle multiple Maybes nested at different levels?

Question

Working with Maybe seems really difficult in Haskell. I was able to implement the function I need after many frustrating compile errors, but it's still completely disorganized and I don't know how else can I improve it.

I need to:

  • extract multiple nested ... Maybes into one, final Maybe ...
  • Do a -> b -> IO () with Just a and Just b or nothing (*)

Here is example with IO part removed. I need a -> b -> IO (), not (a,b) -> IO () later but I couldn't figure out how to pass both arguments otherwise (I can mapM_ with one argument only).

import Network.URI

type URL = String
type Prefix = String

fubar :: String -> Maybe (Prefix, URL)
fubar url = case parseURI url of
    Just u -> (flip (,) $ url)
               <$> (fmap ((uriScheme u ++) "//" ++ ) ((uriRegName <$> uriAuthority u)))
    _ -> Nothing

Result:

> fubar "https://hackage.haskell.org/package/base-4.9.0.0/docs/src/Data.Foldable.html#mapM"
Just ("https://hackage.haskell.org"
     ,"https://hackage.haskell.org/package/base-4.9.0.0/docs/src/Data.Foldable.html#mapM"
     )

(*) printing what failed to parse wrong would be nice


Show source
| haskell   2017-01-06 22:01 3 Answers

Answers ( 3 )

  1. 2017-01-06 23:01

    This is pretty simple written with do notation:

    fubar :: String -> Maybe (Prefix, URL)
    fubar url = do
      u <- parseURI url
      scheme <- uriScheme u
      domain <- uriRegName <$> uriAuthority u
      return $ (scheme ++ "//" ++ domain, url)
    

    Monads in general (and Maybe in particular) are all about combining m (m a) into m a. Each <- binding is an alternate syntax for a call to >>=, the operator responsible for aborting if it sees a Nothing, and otherwise unwrapping the Just for you.

  2. 2017-01-06 23:01

    First note that you're just stacking multiple fmaps there, with α <$> (fmap β (γ <$> uriAuthority u)). This can (functor laws!) be rewritten α . β . γ <$> uriAuthority u, i.e.

    {-# LANGUAGE TupleSections #-}
    
       ...
        Just u -> (,url) . ((uriScheme u++"//") ++ ) . uriRegName <$> uriAuthority u
    

    It might be better for legibility to actually keep the layers separate, but then you should also give them names as amalloy suggests.

    Further, more strongly:

    Extract multiple nested M into one, final M

    Well, sounds like monads, doesn't it?

    fubar url = do
       u <- parseURI url
       (,url) . ((uriScheme u++"//") ++ ) . uriRegName <$> uriAuthority u
    
  3. 2017-01-06 23:01

    I'm not entirely clear on precisely what you're asking, but I'll do my best to answer the questions you have presented.

    To extract multiple nested Maybes into a single final Maybe is taken care of by Maybe's monad-nature (also applicative-nature). How specifically to do it depends on how they are nested.

    Simplest example:

    Control.Monad.join :: (Monad m) => m (m a) -> m a
    -- thus 
    Control.Monad.join :: Maybe (Maybe a) -> Maybe a
    

    A tuple:

    squishTuple :: (Maybe a, Maybe b) -> Maybe (a,b)
    squishTuple (ma, mb) = do  -- do in Maybe monad
        a <- ma
        b <- mb
        return (a,b)
    
    -- or
    squishTuple (ma, mb) = liftA2 (,) ma mb
    

    A list:

    sequenceA :: (Applicative f, Traversable t) => t (f a) -> f (t a)
    -- thus
    sequenceA :: [Maybe a] -> Maybe [a]
    -- (where t = [], f = Maybe)
    

    Other structures can be flattened by composing these and following the types. For example:

    flattenComplexThing :: (Maybe a, [Maybe (Maybe b)]) -> Maybe (a, [b])
    flattenComplexThing (ma, mbs) = do
        a <- ma
        bs <- (join . fmap sequenceA . sequenceA) mbs
        return (a, bs)
    

    That join . fmap sequenceA . sequenceA line is a bit complex, and it takes some getting used to to know how to construct things like this. My brain works in a very type-directed way (read the composition right-to-left):

    [Maybe (Maybe b)]
          |
       sequenceA :: [Maybe _] -> Maybe [_]
          ↓
    Maybe [Maybe b]
          |
        -- sequenceA :: [Maybe b] -> Maybe [b]
        -- fmap f  makes the function f work "inside" the Maybe, so
       fmap sequenceA :: Maybe [Maybe b] -> Maybe (Maybe [b])
          ↓
    Maybe (Maybe [b])
          |
       join :: Maybe (Maybe _) -> Maybe _
          ↓
    Maybe [b]
    

    As for the second question, how to do an a -> b -> IO () when you have Maybe a and Maybe b, presuming you don't want to do the action at all in the case that either one is Nothing, you just do some gymnastics:

    conditional :: (a -> IO ()) -> Maybe a -> IO ()
    conditional = maybe (return ())
    
    conditional2 :: (a -> b -> IO ()) -> Maybe a -> Maybe b -> IO ()
    conditional2 f ma mb = conditional (uncurry f) (liftA2 (,) ma mb)
    

    Again I found conditional2 in a mainly type-directed way in my mind.

    It takes some time to develop your type gymnastics, but then it starts to get really fun. To make code like this readable, it's important to use helper functions, e.g. conditional above, and name them well (which is arguable about conditional :-). You'll gradually get familiar with the standard library's helpers. There's no magic bullet here, you just have to get used to it -- it's a language. Work with it, strive for clarity, if something is too ugly try your best to make it prettier. And ask more specific questions :-)

◀ Go back