Search code examples
haskellnestedfunctor

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?


Solution

  • 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.