Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fold a Foldable of Maybe (Monoid) ignoring the missing values in Haskell

I am working with some data that has missing values, which is simply represented as lists of Maybe values. I would like to perform various aggregates/statistical operations, which simply ignore the missing values.

This is related to the following questions:

Idiomatic way to sum a list of Maybe Int in haskell

How to use the maybe monoid and combine values with a custom operation, easily?

However, the former question is content with returning Nothing if any value is missing, which is not an option in my case. I have a solution which involves creating a Num instance for Maybe. However, that means it is specific to addition and multiplication and it has some other problems, too.

instance Num a => Num (Maybe a) where
  negate      = fmap negate
  (+)         = liftA2 (+)
  (*)         = liftA2 (*)
  fromInteger = pure . fromInteger
  abs         = fmap abs
  signum      = fmap signum

Based on that, we can do something like this:

maybeCombineW :: (a -> a -> a) -> Maybe a -> Maybe a -> Maybe a
maybeCombineW f (Just x)  (Just y)  = Just (f x y)
maybeCombineW _ (Just x)  Nothing   = Just x
maybeCombineW _ Nothing   (Just y)  = Just y
maybeCombineW _ Nothing   Nothing   = Nothing


maybeCombineS :: (a -> a -> a) -> Maybe a -> Maybe a -> Maybe a
maybeCombineS f (Just x)  (Just y)  = Just (f x y)
maybeCombineS _ _          _        = Nothing


class (Num a) => Num' a where
  (+?) :: a -> a -> a
  (*?) :: a -> a -> a
  (+!) :: a -> a -> a
  (*!) :: a -> a -> a
  (+?) = (+)
  (*?) = (*)
  (+!) = (+)
  (*!) = (*)

instance {-# OVERLAPPABLE  #-} (Num a) => Num' a
instance {-# OVERLAPPING  #-} (Num' a) => Num' (Maybe a) where
  (+?) = maybeCombineW (+?)
  (*?) = maybeCombineW (*?)
  (+!) = maybeCombineS (+!)
  (*!) = maybeCombineS (*!)


sum' :: (Num' b, Foldable t) => t b -> b
sum' = foldr (+?) 0

sum'' :: (Num' b, Foldable t) => t b -> b
sum'' = foldr (+!) 0

What I like about this: It gives me two functions, a lenient sum' and a strict sum'', from which I can choose as needed. I can use the same functions to sum any Num instances, so I can reuse the same code for lists without Maybe without converting them first.

What I don't like about this: The instance overlap. Also, for any operation other than addition and multiplication, I have to specify a new type class and make new instances.

Therefore, I was wondering whether it is somehow possible to get a nice and general solution, perhaps along the lines suggested in the second question, which treats Nothing as the mempty for any operation in question.

Is there a nice idiomatic way of doing this?

Edit: Here is the best solution so far:

inout i o = ((fmap o) . getOption) . foldMap (Option . (fmap i))
sum' = Sum `inout` getSum
min' = Min `inout` getMin
-- etc.
like image 627
kloffy Avatar asked Mar 10 '23 23:03

kloffy


2 Answers

There is an instance of Monoid that does exactly the right thing:

instance Monoid a => Monoid (Maybe a) where
  mempty = Nothing
  Nothing `mappend` m = m
  m `mappend` Nothing = m
  Just m1 `mappend` Just m2 = Just (m1 `mappend` m2)

It's in Data.Monoid.

Thus,

foldMap (liftA Sum) [Just 1, Nothing, Just 2, Nothing, Just 3] = 
   fold [Just (Sum 1), Nothing, Just (Sum 2), Nothing, Just (Sum 3)] = 
      Just (Sum 6)

For strict left-folding versions, instead of fold one can use foldl' mappend mempty, and instead of foldMap f use foldl' (mappend . f) mempty. In the Maybe monoid, mempty is Nothing.

like image 177
n. 1.8e9-where's-my-share m. Avatar answered Apr 30 '23 10:04

n. 1.8e9-where's-my-share m.


How about just using catMaybes from Data.Maybe to discard all the Nothing values? Then you can run any aggregations and calculations on a list of just plain values.

like image 40
shang Avatar answered Apr 30 '23 10:04

shang