purescript / purescript-contravariant

Contravariant functors

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

`Comparison` could work for any monoid not just Ordering

safareli opened this issue · comments

-- | An adaptor allowing `>$<` to map over the inputs of a comparison function.
newtype Comparison o a = Comparison (a -> a -> o)

derive instance newtypeComparison :: Newtype (Comparison o a) _

instance contravariantComparison :: Contravariant (Comparison o) where
  cmap f (Comparison g) = Comparison (g `on` f)

instance semigroupComparison :: Semigroup o => Semigroup (Comparison o a) where
  append (Comparison p) (Comparison q) = Comparison (p <> q)

instance monoidComparison :: Monoid o => Monoid (Comparison o a) where
  mempty = Comparison (\_ _ -> mempty) -- mempty of `Ordering` is `EQ`

-- | The default comparison for any values with an `Ord` instance.
defaultComparison :: forall a. Ord a => Comparison Ordering a
defaultComparison = Comparison compare

we can do something like this

newtype On m a = On (a -> a -> m)

derive instance newtypeEquivalence :: Newtype (On m a) _

instance contravariantEquivalence :: Contravariant (On m) where
  cmap f (On g) = On (g `on` f)

instance semigroupEquivalence :: (Semigroup m) => Semigroup (On m a) where
  append (On p) (On q) = On (\a b -> p a b <> q a b)

instance monoidEquivalence :: (Monoid m) => Monoid (On m a) where
  mempty = On mempty


type Equivalence = On (Conj Boolean)
type Comparison = On Ordering

Ordering is a Monoid; the instance just doesn't show up on the data type because it's defined downstream, in the Data.Monoid module.

good point, updated my comment

That is pretty neat. Shame it would be a breaking change. I think it's probably worth doing, though.

Probably we can schedule this change for next braking change then (let me know when that comes and will open PR)

@safareli Would you mind submitting a PR that adds this? We'll be doing breaking changes before v0.14.0 is released.

Unfortunately I'm busy this month :(

This doesn't work out when I try to implement the Decide instance for Comparison via On:

instance chooseOn :: Decide (On m) where
  choose f (On g) (On h) = On \a b -> case f a of
    Left c -> case f b of
      Left d -> g c d
      Right _ -> LT
    Right c -> case f b of
      Left _ -> GT
      Right d -> h c d

Fails with this error. We could supply mempty, but then the instance's implementation has been changed. The Equivalence version supplies false on the Left-Right and Right-Left branches:

1/1 TypesDoNotUnify] src/Data/Decide.purs:19:15

                    v
  19      Left c -> case f b of
  20        Left d -> g c d
  21        Right _ -> LT
                        ^
  
  Could not match type
  
    Ordering
  
  with type
  
    m3
  
  while checking that expression LT
    has type t2
  while applying a function On
    of type (t0 -> t0 -> t1) -> On t1 t0
    to argument \a ->
                  \b ->
                    case (f a) of
                      (Left c) -> ...
                      (Right c) -> ...
  in value declaration chooseOn
  
  where m3 is a rigid type variable
          bound at (line 0, column 0 - line 0, column 0)
        t0 is an unknown type
        t1 is an unknown type
        t2 is an unknown type

Thanks for looking into this @JordanMartinez. After looking at your PR and thinking about this a little bit more, and I’m not quite as keen as I was a year ago for a couple of reasons:

  • The specialised behaviour in the Divide instances for Equivalence and Comparison makes it feel like maybe they should be kept separate after all?
  • If we are wanting to factor out the basic pattern for what the Comparison and Equivalence instances have in common, I feel like that behaviour might be better to hang on the Op data type, since a function of two arguments is really just a function which returns a function. In fact I think Op has the relevant instances already:
newtype Op a b = Op (b -> a)

derive newtype instance semigroupOp :: Semigroup a => Semigroup (Op a b)
derive newtype instance monoidOp :: Monoid a => Monoid (Op a b)

So then I think On m a is “just” Op (Op m a) a.

The specialised behaviour in the Divide instances for Equivalence and Comparison makes it feel like maybe they should be kept separate after all?

That was my thinking.

So then I think On m a is “just” Op (Op m a) a.

So, would we still support On? I'm not sure what the next action here would be.

Since we already have Op which works in a similar way for any monoid, and since Equivalence and Comparison have specialized Divide instances, I’m tempted to leave everything as-is. I’m leaning towards not introducing On, since it’s a special case of Op.

Should we close this then?

Yes, I think so.