sevo
sevo

Reputation: 4609

How can I untangle multiple Maybes nested at different levels?

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:

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

Upvotes: 1

Views: 782

Answers (3)

amalloy
amalloy

Reputation: 91837

This is pretty simple written with do notation:

fubar :: String -> Maybe (Prefix, URL)
fubar url = do
  u <- parseURI url
  scheme <- uriScheme u
  authority <- uriAuthority u
  return (scheme ++ "//" ++ uriRegName authority, 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.

Upvotes: 13

luqui
luqui

Reputation: 60463

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

Upvotes: 3

leftaroundabout
leftaroundabout

Reputation: 120711

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

Upvotes: 5

Related Questions