Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I get a handle on deep stacks of functors in Haskell?

Now and then I find myself mapping over a deep stack of functors, e.g. a parser for some collection of optional values:

-- parse a rectangular block of characters to a map of
-- coordinate to the character, or Nothing for whitespace
parseRectangle :: Parser (Map (Int, Int) (Maybe Char))

data Class = Letter | Digit | Other

classify :: Char -> Class

parseClassifiedRectangle :: Parser (Map (Int, Int) (Maybe Class))
parseClassifiedRectangle = fmap (fmap (fmap classify)) parseRectangle

What are some good ways around the nested fmaps? Oftentimes it's not as clear as here, and I end up adding fmaps until the code type checks. Simple code ends up as a mess of fmap boilerplate, where what I really want to express is "lift this function to the appropriate depth and apply it to the contained type".

Some ideas, none of which I've found particularly satisfactory so far:

  • define fmap2 :: (Functor f, Functor g) => (a -> b) -> g (f a) -> g (f b) and friends
  • define concrete helpers, like mapMaybeMap :: (a -> b) -> Map k (Maybe a) -> Map k (Maybe b)
  • introduce newtype wrappers for the functor stack, and make those instances of Functor, like newtype MaybeMapParser a = Parser (Map (Int, Int) (Maybe a))

Do others run into this problem in large codebases? Is it even a problem? How do you deal with it?

like image 677
robx Avatar asked Aug 02 '18 11:08

robx


1 Answers

Let me break the ice on this interesting question that people seem shy about answering. This question probably comes down to more of a matter of style than anything, hence the lack of answers.

My approach would be something like the following:

parseClassifiedRectangle :: Parser (Map (Int, Int) (Maybe Class))
parseClassifiedRectangle = doClassify <$> parseRectangle
  where
    doClassify = Map.map (fmap classify)

I try to use <$> for the top level Functor, and save fmap for interior functors; although that doesn't always work too well in practice.

I've used a local named binding. But even if doClassify were left as f it sometimes helps clarify a high level view of whats happening: "on the parsed value we are doing a thing, see below for what thing does." I don't know what the efficiency concerns are for making a binding.

I've also used the specific instance of fmap for the Map instance. This helps orient me within the stack and gives a signpost for the final fmap.

Hope this helps.

like image 95
trevor cook Avatar answered Nov 04 '22 08:11

trevor cook