Home > Back-end >  Combining two maps - is it this ugly?
Combining two maps - is it this ugly?

Time:04-09

Today I had two maps I needed to combine: consMaps :: Map k a -> Map k b -> Map k (a, b). Seeing nothing in Data.Map, I set out to implement this and came up with something unexpectedly ugly:

consMaps :: Map k a -> Map k b -> Map k (a, b)
consMaps ma mb = mapMapMaybe g (Map.unionWith f a' b')
  where
    a' :: Map k (Maybe (Maybe a, Maybe b))
    a' = fmap (\a -> Just (Just a, Nothing)) ma
    b' :: Map k (Maybe (Maybe a, Maybe b))
    b' = fmap (\b -> Just (Nothing, Just b)) mb
    f :: Maybe (Maybe a, Maybe b) -> Maybe (Maybe a, Maybe b) -> Maybe (Maybe a, Maybe b)
    f (Just (Just a, _)) (Just (_, Just b)) = Just (Just a, Just b)
    f (Just (_, Just b)) (Just (Just a, _)) = Just (Just a, Just b)
    -- f (Just a, Just b) _ = Just (a, b) -- impossible in this context
    -- f _ (Just a, Just b) = Just (a, b) -- impossible in this context
    f _ _ = Nothing
    g :: Maybe (Maybe a, Maybe b) -> Maybe (a, b)
    g (Just (Just a, Just b)) = Just (a, b)
    g _ = Nothing


mapMapMaybe :: (a -> Maybe b) -> Map k a -> Map k b
mapMapMaybe f mp = snd (mapEither (maybe (Left ()) Right . f) mp)

Am I missing something? Is this as good as this gets?

CodePudding user response:

It looks like your consMaps implementation, with the signature you've given, is just

consMaps :: Map k a -> Map k b -> Map k (a, b)
consMaps = intersectionWith (,)

If instead you wanted a Map k (Maybe a, Maybe b), I might write that as

consMaps :: Map k a -> Map k b -> Map k (Maybe a, Maybe b)
consMaps ma mb = unionWith combine ma' mb' where
  ma' = fmap (\ a -> (Just a, Nothing)) ma
  mb' = fmap (\ b -> (Nothing, Just b)) mb
  combine (a, _) (_, b) = (a, b)

CodePudding user response:

If you want Map (a, b) out then use the other answer (intersectionWith). If you want a Map (Maybe a, Maybe b) then that specialized function won't work. Instead, containers has a merge function, which covers "basically all" ways to combine Maps. It takes three strategies: what to do if a key is only in the left map, what to do if a key is only in the right map, and what to do if a key is in both. The strategies are built using helper functions. The idea is that merge does exactly one traversal of the inputs, so is more efficient than e.g. mapping the inputs and then combining.

import Data.Map.Merge.Lazy

catMaps :: Ord k => Map k a -> Map k b -> Map k (Maybe a, Maybe b)
catMaps = merge left right both
  where left  = mapMissing $ \_ a -> (Just a, Nothing)
        right = mapMissing $ \_ b -> (Nothing, Just b)
        both  = zipWithMatched $ \_ a b -> (Just a, Just b)

Note that the "right" type for the output in this version is actually Map k (These a b), where These models "inclusive or":

data These a b = This a | That b | These a b
theseMaps :: Ord k => Map k a -> Map k b -> Map k (These a b)
theseMaps = merge left right both
  where left  = mapMissing $ const This
        right = mapMissing $ const That
        both  = zipWithMatched $ const These
  • Related