Merging records of custom data type in Haskell

Viewed 892

I have some data type

data SomeType = SomeType { a :: Maybe Int
                         , b :: Maybe String
                         , c :: Maybe OtherType }

and two variables of the type

st1 = SomeType (Just 1) (Just "hello") Nothing
st2 = SomeType Nothing (Just "world") Nothing

How would I merge them prioritising the second one?

merged = SomeType (Just 1) (Just "world") Nothing

Here a st2 is Nothing, so Just 1 from a st1 is preferred.
For b, Just "world" from st2 overrides st1's Just "hello".

My simple approach would be to do something like

merge :: SomeType -> SomeType -> SomeType
merge (SomeType a1 b1 c1) (SomeType a2 b2 c2) = 
  SomeType { a = maybe a1 pure a2 
           , b = maybe b1 pure b2
           , c = maybe c1 pure c2 }

The actual type is larger than this example and c :: Maybe OtherType needs to be merged recursively as well.

Edit: Also, I'm aware of record field update in the following form

st1 { b = Just "world" } 

to create new record with updated fields. Not sure if this helps in my case.

3 Answers

A function with the type SomeType -> SomeType -> SomeType looks like a candidate for a Semigroup, or at least something that can be implemented with Semigroup. There's a couple of options.

Explicit merge

If you keep SomeType as in the OP, you could write an explicit merge function like this:

merge :: SomeType -> SomeType -> SomeType
merge x y = toSomeType $ toTriple x <> toTriple y
  where
    toTriple (SomeType a b c) = (Last <$> a, Last <$> b, c)
    toSomeType (a, b, c) = SomeType (getLast <$> a) (getLast <$> b) c

This converts each SomeType instance to a triple (three-tuple), for which a Semigroup instance exists if all three elements have Semigroup instances.

There's more than one Semigroup instance for Maybe, but (from GHC 8.4) any Maybe a is a Semigroup (and Monoid) instance when a is a Semigroup instance.

The Semigroup instance that favours the last of two values is Last, so toTriple maps a and b to Maybe Last values. It doesn't, however, map c, because in this implementation it assumes that OtherType is already a Semigroup instance (see below).

Since the resulting triples are themselves Semigroup instances, they can be combined with the <> operator. This gives you a resulting triple that you can convert back to a SomeType value with toSomeType.

Semigroup instances

You could also simply make the types themselves Semigroup instances. I think this is best if there's no ambiguity. In itself, Maybe can be more than one Semigroup instance, e.g. favouring the First or Last value, respectively.

If you always want to favour the Last value, however, you can make this explicit in the type. Here's one way OtherType could look:

data OtherType =
  OtherType { foo :: Maybe (Last Int), bar :: Maybe (Last String) } deriving (Eq, Show)

Notice that the fields aren't just Maybe values, but explicitly Maybe Last values. This gives rise to an unambiguous Semigroup instance:

instance Semigroup OtherType where
  (OtherType foo1 bar1) <> (OtherType foo2 bar2) =
    OtherType (foo1 <> foo2) (bar1 <> bar2)

You could also follow the same design principle for SomeType, which would make the explicit merge function redundant.

Examples

You can try out the above functionality in GHCi:

*Q54068475> merge st1 st2
SomeType {a = Just 1, b = Just "world", c = Nothing}
*Q54068475> ot1 = OtherType (Just (Last 42)) (Just (Last "foo"))
*Q54068475> ot2 = OtherType (Just (Last 1337)) Nothing
*Q54068475> merge (SomeType (Just 1) (Just "hello") (Just ot1))
                  (SomeType Nothing (Just "world") (Just ot2))
SomeType {a = Just 1,
          b = Just "world",
          c = Just (OtherType {foo = Just (Last {getLast = 1337}),
                               bar = Just (Last {getLast = "foo"})})}

(I've added a few line breaks to the GHCi session to make it more readable...)

Monoids

These types could also be Monoid instances:

instance Monoid OtherType where
  mempty = OtherType Nothing Nothing

This could come in useful, so you might as well consider adding those instances as well...

You can make use of the Alternative instance for Maybe types.

import Control.Applicative  -- for <|>
merge (SomeType a1 b1 c1) (SomeType a2 b2 c2)
    = SomeType (a2 <|> a1) (b2 <|> b1) (c2 <|> c1)

For Maybe types, (<|>) returns its first argument if it is not Nothing, otherwise it returns its second arguments. To prioritize the second argument of merge, use its component as the first argument for <|> in each case.

In order to handle both SomeType and OtherType, you might want to use a type class.

class Mergeable a where
    merge a1 a2 :: a -> a -> a

instance Mergeable SomeType where
    merge (SomeType a1 b1 c1) (SomeType a2 b2 c2)
    = SomeType (a2 <|> a1) (b2 <|> b1) (merge <$> c1 <*> c2)  -- not merge c2 c1

instance Mergeable OtherType where
    merge (OtherType a1 b1) (OtherType a2 b2) = ...

For simple case you can use orElse.

If you need to merge data if it presents in both objects you can create helper

mergeHelper :: (a -> a-> a) -> Maybe a -> Maybe a -> Maybe a
mergeHelper _ None x = x
mergeHelper _ (Maybe x) _ = Maybe x
mergeHelper f (Maybe x) (Maybe y) = Maybe $ f x y
Related