jcpetruzza / barbies

BSD 3-Clause "New" or "Revised" License
92 stars 15 forks source link

Laws for ProductB are overly restrictive #17

Closed Shimuuar closed 4 years ago

Shimuuar commented 5 years ago

This issue didn't arise from any practical problem. I just studied library and noticed that laws for ProductB are overly strict. Let me explain why

Let start from Applicative:

class Functor f => Applicaitve f where
  pure :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b

But we can rewrite it in slightly different but equivalent form:

class Functor f => Applicaitve f where
  pure :: a -> f a
  (<*>) :: f a -> f a -> f (a, b)

And ProductB look exactly like second variant of Applicative except for different kind. If we look at ProductB as different-kinded applicative then Bad from documentation admits writer-like instance:

data Bad f = Bad (f String) Int

instance FunctorB Bad where
  bmap f (Bad g i) = Bad (f g) i

instance ProductB Bad where
  buniq f = Bad f 0
  bprod (Bad f i) (Bad g j) = Bad (Pair f g) (i + j)

In fact we can turn any Applicative instance into ProductB instance (admittedly quite useless):

newtype BAp m a f = BAp (m (f a))

instance Functor m => FunctorB (BAp m a) where
  bmap f (BAp b) = BAp (fmap f b)

instance Applicative m => ProductB (BAp m a) where
  buniq x = BAp (pure x)
  bprod (BAp f) (BAp g) = BAp $ liftA2 Pair f g

I didn't checked how applicative laws translate for ProductB but I don't expect any problems

jcpetruzza commented 5 years ago

Thanks for bringing this up! I too saw ProductB as a stronger "applicative", but was somehow convinced that we were in need of the the uniqueness properties that follow from the laws most of the time, so adding an intermediate ApplicativeB class was not worth it.

So I went to see those places where I thought we were using this, and ended up with the exact opposite conclusion: ProductB just captures those types whose unique "applicative" instance can be generated automatically, but I don't think we are actually using the extra "uniqueness" guarantees anywhere else. This is great, I wished I had noticed it before :slightly_smiling_face:

So, how should an ApplicativeB class look like? I don't think we can have a direct equivalent of <*> (apply) without something like impredicative-types. So without apply, this would be probably better named MonoidalB.

class FunctorB b => MonoidalB b where
  bunit :: b Unit
  bprod :: b f -> b g -> b (f `Product` g)

With laws (following Applicative programming with effects)

Shimuuar commented 5 years ago

I think ProductB is a different kinded applicative (provided that we relax its laws). We can't define analog of <*> so have to use bprod. And buniq and bunit are equivalent:

bunit   = buniq Unit
buniq f = bmap (\_ -> f) bunit

Should we use buinq we'll have to change identity laws. This should work I think:

bmap (\(Pair _ ga) -> ga) (buniq Unit `bprod` bg) = bg
bmap (\(Pair fa _) -> fa) (bf `bprod` buniq Unit) = bf

So we already have an applicative. Its name obscures in connection to Applicative type class and maybe it should be renamed but I have no opinion on that. All in all only thing needed to add Applicative is to change documentation.

P.S. It always amazed me how much one can deduce just by looking at documentation!

jcpetruzza commented 5 years ago

This is now fixed on master. ProductB was renamed to ApplicativeB (also ProductBC got removed, wasn't really needed these days). I think I managed to do this without completely breaking backwards compatibility: Data.Barbie is now deprecated but should be exposing the previous API.

jcpetruzza commented 4 years ago

Release 2.0.0.0 includes these changes