Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Attempt to provide some intuition for contravariant and divisible #33

Merged
merged 8 commits into from
Oct 18, 2017

Conversation

ocharles
Copy link
Contributor

@ocharles ocharles commented Sep 20, 2017

There was recently confusion (and, as I read it, frustration) on Twitter regarding Contravariant and Divisible.

  • Exhibit A - Will felt he had little more than a type signature to go on for Contravariant.

  • Exhibit B - points out that 'Divisible' has nothing but a dense technical explanation. I posture that barely 1% of the people who use Haskell have even heard of a presheave, let alone understanding what the category Presheave even is.

I provided a few tweet replies, but this comes up time and time again. @tomjaguarpaw has explained 'Contravariant' once (https://ocharles.org.uk/blog/guest-posts/2013-12-21-24-days-of-hackage-contravariant.html), @phaazon spoke about divisible (https://phaazon.blogspot.co.uk/2015/08/contravariance-and-luminance-to-add.html), but I think it's about time we bought at least some of this into contravariant proper.

Yes, the examples do not do justice to what a Contravariant functor truly is, but I believe they provide at least a gentle ramp to begin wading towards the deep end. I will understand if you feel this is the wrong place for this documentation.

@jb55 this is written for you, so I'd appreciate knowing if it even helps. @acowley expressed not knowing what Divisible even was, so likewise feedback would be appreciated.

-- category of copresheaves from Hask to Hask, equipped with Day convolution mapping the
-- Cartesian product of the source to the Cartesian product of the target.
-- Continuing the intuition that 'Contravariant' functors consume input, a 'Divisible'
-- contravariant functor also has the ability to composed "beside" another contravariant

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'the ability to be composed' I think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, thanks!

-- 'divide' is a generalization by also taking a 'contramap' like function to
-- split any @a@ into a pair. This conveniently allows you to target fields of
-- a record, for instance, by extracting the values under two fields and
-- combining them into a tuple.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this lacks an example, so can I write serializeStringAndInt using divide, how?

-- on a persons bank balance to work out if they are currently overdrawn:
--
-- @
-- newtype Predicate a = Predicate (a -> Bool)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd define it as in the module

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

person's

Copy link
Contributor

@phadej phadej left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Could you add links in @@ sections around contramap Contravariant etc.

@tomjaguarpaw
Copy link

This is sorely needed.

@acowley
Copy link

acowley commented Sep 20, 2017

Thanks so much for taking action on this @ocharles! I had forgotten about @phaazon’s blog post, and it really is an excellent example. If linking to such things from the docs is viewed as okay, I’d be in favor of doing so.

--
-- As an example, consider the type of predicate functions @a -> Bool@. One
-- such predicate might be @negative x = x < 0@, which
-- classisifies integers as to whether they are negative. However, given this
Copy link

@VitorCBSB VitorCBSB Sep 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Classifies, I think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup

-- structure of the source category. (Here fst and snd are used in lieu of the more restricted
-- lambda and rho, but this construction works with just a monoidal category.)
-- That is, given a serializer for @a@ (@s :: Serializer a@), and a way to turn
-- @b@s into @a@s (a mapping @f :: b -> a@), we have a serializer for @b@:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence is what made it click for me, the only difficulty I had when reading this was that a and b are reversed compared to the type signature of contramap.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically, this is what made Contravariant click for me. 👍

@Blaisorblade
Copy link

Thanks a lot for this! That's the sort of thing we need more of. If I may ask: do you also understand Decidable?

@jb55
Copy link

jb55 commented Sep 20, 2017 via email

@hadronized
Copy link

hadronized commented Sep 20, 2017

Feel free to use my blog post, of course! :)

I just re-read it, and found the comments below. Oh my. :D

@ekmett
Copy link
Owner

ekmett commented Sep 21, 2017

I have no particular objection to including links to blog posts about the topic.

@ocharles
Copy link
Contributor Author

@Blaisorblade yes, I'll do Decidable too, if people feel this is useful. It's not much of a step up from Divisible.

@ocharles
Copy link
Contributor Author

Ok, I've added an explanation for Decidable too, and addressed @phadej's point that my Divisible example doesn't actually show a use of divide.

-- iBytes = runSerializer serializeB b
-- in sBytes <> iBytes
--
-- stringAndInt :: Serializer (String, Int)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StringAndInt

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, type checker!

@RyanGlScott
Copy link
Collaborator

This looks nice overall. I have one gripe in that these changes move the laws for Decidable and Divisible away from their Haddocks and sequesters them elsewhere. I'd prefer to keep the laws close to the classes themselves, although I don't care if they're accompanied by the current category theoretic jargon or not. I'd be OK with repeating the laws in the additional section you've added, but explained in further detail.

@ocharles
Copy link
Contributor Author

ocharles commented Sep 24, 2017 via email

@mitchellwrosen
Copy link

mitchellwrosen commented Oct 17, 2017

Is this still being worked on? If so, there's a pattern I've used in the past to make Divisible feel more like Applicative that might be worth mentioning in some sort of tutorial. I'm not sure.

-- Right fixity, higher than >$<
infixr 5 >*<
(>*<) :: Divisible f => f a -> f b -> f (a, b)
(>*<) = divided

Here's how you use it:

data Person = Person
  { name :: String
  , age :: Int
  , living :: Bool
  }

longName :: Predicate String
longName = Predicate (\name -> length name > 8)

old :: Predicate Int
old = Predicate (\age -> age > 60)

alive :: Predicate Bool
alive = Predicate id

oldAndAliveWithLongName :: Predicate Person
oldAndAliveWithLongName =
  (\p -> (name p, (age p, living p)))
    >$< longName
    >*< old
    >*< alive

@tomjaguarpaw
Copy link

tomjaguarpaw commented Oct 17, 2017

@mitchellwrosen I have to say I don't like that at all. Why not

(<||>) :: Divisible f => f a -> f a -> f a
(<||>) = divide (\x -> (x, x))

oldAndAliveWithLongName :: Predicate Person
oldAndAliveWithLongName = contramap name longName
                     <||> contramap age old
                     <||> contramap living alive

[Not type checked!]

@jb55
Copy link

jb55 commented Oct 17, 2017

@tomjaguarpaw yours is cleaner, @mitchellwrosen's fits the Applicative analogy a bit better from a learning perspective

@tomjaguarpaw
Copy link

It only feels like Applicative until you actually try to use it, at which point you notice you can never get rid of the tuple nesting because they type parameter is contravariant. Then it becomes too painful to use.

@mitchellwrosen
Copy link

That's a good operator too, but it looks more like <> for the Monoid that every Divisible gives rise to, so I don't think it needs its own name.

mempty = conquer
mappend = divide (\x -> (x, x))

(except that Predicate isn't a Semigroup/Monoid right now)

@phadej
Copy link
Contributor

phadej commented Oct 17, 2017

For Predicate, deriving via a -> All or a -> Any instances also makes sense. So it's not an easy choice.

EDIT or will they boil down to the same instance?

@mitchellwrosen
Copy link

Wouldn't the more natural/obvious Monoid instance correspond to a -> All, per how Predicate's Divisible instance works?

@tomjaguarpaw
Copy link

Maybe, but that's just shifting the observation to "there are two choices of Divisible instance"!

@mitchellwrosen
Copy link

mitchellwrosen commented Oct 17, 2017 via email

@ocharles
Copy link
Contributor Author

Yes, this is being worked on, but the scope of this PR is to just improve documentation. All I need to do is add the laws, and I'm done. I'll happily take a patch from anyone else adding those laws, too.

@ekmett
Copy link
Owner

ekmett commented Oct 18, 2017

I'd say the Monoid for any of these should match up with the conquer and divide delta if they are missing. This corresponds exactly to what you'd derive if you used (a -> All) and (a -> a -> All) and (a -> a -> Ordering), etc. I'm actually surprised I didn't add them.

@ekmett
Copy link
Owner

ekmett commented Oct 18, 2017

Ah, we're just missing the Predicate one. Adding it. (Off topic digression ends)

@ekmett ekmett merged commit 20994af into ekmett:master Oct 18, 2017
@ekmett
Copy link
Owner

ekmett commented Oct 18, 2017

Merged what you have. We can do more intensive edits as we go.

@ekmett
Copy link
Owner

ekmett commented Oct 18, 2017

@ocharles you have commit access to the repo now, feel free to push further updates directly.

@jb55
Copy link

jb55 commented Oct 18, 2017

Thanks guys!

@hadronized
Copy link

\o/

RyanGlScott added a commit that referenced this pull request Jan 8, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.