3

The default monoid for lists in the GHC Prelude is concatenation.

[1,2,3] <> [4,5,6] becomes [1,2,3] ++ [4,5,6] and thus [1,2,3,4,5,6]

I want to write a ZipList Monoid instance that behaves like this:

[
  1 <> 4
, 2 <> 5
, 3 <> 6
]

The result is [5,7,9] assuming I am using the sum monoid. Note this behaves like zipWith (+)

Potentially it would behave like this:

[
  Sum 1 <> Sum 4
, Sum 2 <> Sum 5
, Sum 3 <> Sum 6
]

I need to create a newtype around the ZipList newtype and the Sum newtype in order to create an instance for Monoid, Arbitrary, and EqProp. Thus avoiding orphan instances. This is how both the ZipList and the Sum looks like in the Prelude:

newtype ZipList a = ZipList { getZipList :: [a] }
newtype Sum a = Sum { getSum :: a }

This is how my newtype MyZipList looks: Does it look right?

newtype MyZipList a =
  MyZipList (ZipList [a])
  deriving (Eq, Show)

instance Monoid a => Monoid (MyZipList a) where
  mempty = MyZipList (ZipList [])

  mappend (MyZipList z) (MyZipList z') =
    MyZipList $ liftA2 mappend z z'

instance Arbitrary a => Arbitrary (MyZipList a) where
  arbitrary = MyZipList <$> arbitrary

instance Eq a => EqProp (MyZipList a) where
  (=-=) = eq

This is how my newtypeMySum looks like: Does this look right?

newtype MySum a =
  MySum (Sum a)
  deriving (Eq, Show)

 instance (Num a, Monoid a) => Monoid (MySum a) where
   mempty = MySum mempty

   mappend (MySum s) (MySum s') = MySum $ s <> s'

 instance Arbitrary a => Arbitrary (MySum a) where
   arbitrary = MySum <$> arbitrary

I would like assistance in finding out where I went wrong.

duplode
  • 33,731
  • 7
  • 79
  • 150
Kevin Kamau
  • 236
  • 3
  • 11
  • 1
    But `x <> mempty == mempty <> x == x` (that is one of the properties of a monoid), in your case `mempty <> x == mempty`. I think in case of a `ZipList`, the `mappend` should be equal to an infinite list of `mempty`s (of the `a` type). – Willem Van Onsem May 02 '18 at 08:40
  • Why do you create a new newtype instead of just adding a Monoid instance for `ZipList a`? – Dominique Devriese May 02 '18 at 09:08
  • 1
    @DominiqueDevriese "I need to create a newtype ... *Thus avoiding orphan instances.*" – luqui May 02 '18 at 09:19
  • 4
    If your goal was for `MySum` to behave exactly like `Sum` then that at least looks right. Although I'm not sure why you're wrapping `Sum` and `ZipList` if you're going to re-implement their behaviour anyway - you might as well just build `MySum` and `MyZipList` directly. That being said, you said you want assistance but you didn't actually say what your problem was. You explained pretty well what you actually did, but your question is missing a description of what's actually going wrong. – Cubic May 02 '18 at 09:55

1 Answers1

9

First note that ZipList’s Applicative instance already has the zippy behaviour you want.

ghci> liftA2 (<>) (Sum <$> ZipList [1,2,3]) (Sum <$> ZipList [4,5,6]) :: ZipList Int
ZipList [Sum 5, Sum 7, Sum 9]

Then use the fact that any Applicative gives rise to a Monoid by lifting the monoidal behaviour of its contents through the monoidal functor itself. The plan is to abstract the liftA2 (<>) pattern from the expression I wrote above.

newtype Ap f a = Ap { getAp :: f a }
instance (Applicative f, Monoid a) => Monoid (Ap f a) where
    mempty = Ap $ pure mempty
    Ap xs `mappend` Ap ys = Ap $ liftA2 mappend xs ys

(As far as I know this newtype is missing from base, which seems like an oversight to me, though there may be a good reason for it. In fact, I’d argue that ZipList should have a zippy Monoid instance out of the box, but, alas, it doesn’t.)

Your desired Monoid is then just Ap ZipList (Sum Int). This is equivalent to the MyZipList Monoid you wrote by hand (except for the mistake in your mempty - it should be MyZipList $ ZipList $ repeat mempty), but composing it out of reusable newtypes like this is less ad-hoc and requires less boilerplate.

Benjamin Hodgson
  • 42,952
  • 15
  • 108
  • 157
  • Is there a way to use `Compose` here? – chepner May 02 '18 at 13:31
  • @chepner I guess `Ap f = Compose f Identity`, but sadly [`Compose` doesn't have the `Monoid` instance we'd want either](http://hackage.haskell.org/package/base-4.11.1.0/docs/Data-Functor-Compose.html) – Benjamin Hodgson May 02 '18 at 13:36
  • @BenjaminHodgson How would you implement an `Arbitrary (Ap f a)` for `quickBatch` testing? I've tried `instance Arbitrary a => Arbitrary (Ap f a) where arbitrary = liftA2 (<>) Ap arbitrary arbitrary` and a few other variations, but none worked.. – maxloo Jan 15 '21 at 14:37
  • @BenjaminHodgson You can also check out what I'm trying to do at https://stackoverflow.com/questions/65716919/haskell-quickbatch-testing-ziplist-monoid-at-mconcat-results-in-stack-overflow. – maxloo Jan 15 '21 at 14:46
  • 1
    You'd wanna reuse `ZipList`'s `Arbitrary`, right? `instance Arbitrary (f a) => Arbitrary (Ap f a) where arbitrary = Ap arbitrary` – Benjamin Hodgson Jan 15 '21 at 15:55
  • @BenjaminHodgson Thanks! I tried that, but got this error: `Couldn't match expected type ‘Test.QuickCheck.Gen.Gen (Ap f a)’ with actual type ‘Ap Test.QuickCheck.Gen.Gen a0’ In the expression: Ap arbitrary` – maxloo Jan 15 '21 at 17:42
  • 1
    `arbitrary = Ap <$> arbitrary`, my mistake – Benjamin Hodgson Jan 15 '21 at 18:41
  • @BenjaminHodgson Thanks, I've just posted my attempt at quickBatch. Could you help with the `EqProp (Ap f a)`? Is my `quickBatch $ monoid app` going in the right direction? – maxloo Jan 16 '21 at 11:31
  • @BenjaminHodgson I've posted my attempt with quickBatch at https://stackoverflow.com/questions/65752398/haskell-quickbatch-testing-applicative-monoid-ziplist. Hope you can help with that. – maxloo Jan 16 '21 at 17:24
  • @Dharman Sorry for my mis-post here. I've deleted the post. – maxloo Jan 16 '21 at 17:25
  • Since you wrote this, `Ap` has been added to `Data.Monoid` (in `base-4.12.0.0`). – dfeuer Jan 16 '21 at 18:00
  • @BenjaminHodgson Sorry, I'm now wondering why `arbitrary = Ap <$> arbitrary` works, but `arbitrary = Ap arbitrary` doesn't.. could you or @dfeuer help explain? – maxloo Jan 16 '21 at 20:10