-
Notifications
You must be signed in to change notification settings - Fork 14
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
Add VoidProfunctor class #54
base: master
Are you sure you want to change the base?
Conversation
with Tom Ellis
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little thrown by the way some functions end in P
, some end in PP
, and some don't have a special suffix; some names come from the algebraic operation (SumProfunctor
, ProductProfunctor
); some come from the types they manipulate (VoidProfunctor
); and some are inspired by the contravariant
universe (loseP
, lostP
).
I'm not in a huge rush to start playing with this stuff, and I think it's worth taking the time to get things as consistent as possible. If we had unlimited time and no backwards-compatibility constraints, what should the hierarchy look like? Having decided that, how close can we get through sensible version changes and careful deprecations?
Option 1: Name everything algebraically
This is the simplest
-- With associative laws over tuples
class Profunctor p => ProductProfunctor p where
(***!) :: p a c -> p b d -> p (a, b) (c, d)
-- Adds unit laws wrt (***!)
class ProductProfunctor p => UnitProfunctor p where
unit :: p () ()
-- With associative laws over Either
class Profunctor p => SumProfunctor p where
(+++!) :: p a c -> p b d -> p (Either a b) (Either c d)
-- Adds unit laws wrt (+++!)
-- If also ProductProfunctor, adds distributive law that you can distribute (+++!) over (***!)
class SumProfunctor p => VoidProfunctor p where
void :: p Void Void
Option 2: Name everything after the common operations
-- Laws will probably mirror Apply from semigroupoids
class Profunctor p => ApplyProfunctor p where
(****) :: p a (b -> c) -> p a b -> p a c
-- Add the rest of the Applicative laws
class ApplyProfunctor p => ApplicativeProfunctor p where
pureP :: b -> p a b
-- Laws mirror the ones from Decidable without unit
class Profunctor p => DecideProfunctor p where
decideP :: (a -> Either b c) -> p b x -> p c x -> p a x
-- Add the unit laws from Decidable
class DecideProfunctor p => ConcludeProfunctor p where
concludeP :: (a -> Void) -> p a Void
I think this gets us operations of equivalent power, but answers the "ok, but what can I do with these?" a bit more directly. The missing DivisibleProfunctor p
and AlternativeProfunctor p
still feel surprising.
Note: I believe semigroupoids
had long-term plans to change all the names like Apply -> SemiApplicative
, SemiFoldable
, SemiMonad
etc. If we want to go down this route, we should check in and make sure we're not copying deprecated names?
-- | 'Data.Profunctor.Profunctor' version of | ||
-- 'Data.Functor.Contravariant.Divisible.lose'. @'lost' = loseP id@ | ||
-- is the unit of @('+++!')@. | ||
loseP :: (a -> Void) -> p a b |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kinda matches the shape of the proposed conclude :: (a -> Void) -> f a
, but obviously in the first position. Makes hypothetical instance VoidProfunctor p => Conclude (Flip p a)
seem easy.
@@ -94,7 +95,18 @@ class Profunctor p => ProductProfunctor p where | |||
f ***! g = (,) `Profunctor.rmap` Profunctor.lmap fst f | |||
**** Profunctor.lmap snd g | |||
|
|||
-- | In the future 'VoidProfunctor' will be a superclass of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be the other way around? Looking at the semigroupoids
PR, they have:
Contravariant
/ \
/ \
(semigroupoid Divisible) Divise Decide (semigroupoid Decidable)
|
v
Conclude (Decide + lose)
Should the eventual goal be a hierarchy more like:
Profunctor
/ \
/ \
ProductProfunctor SumProfunctor
| |
v v
UnitProfunctor VoidProfunctor
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting. I didn't expect that. If we really want to do the superclassing in that direction then we can do it now with no backwards compatibility changes!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we have a superclass relationship between SumProfunctor
and VoidProfunctor
, it should be this one. "Providing a unit" is almost always done with a subclass (Semigroup => Monoid
etc.) as it lets you write laws in terms of superclass operations that you know you have.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that rationale makes sense to me, thanks! I guess I can also add a superclass of ProductProfunctor
with method pureP
. Perhaps I can add purePP = pureP
as a default and eventually remove purePP
.
On the other hand this suggests the names need work. The names UnitProfunctor
and VoidProfunctor
no longer capture everything the classes do.
ApplicativeProfunctor
from your "Option 2" now sounds good to me, but I prefer DecidableProfunctor
to ConcludeProfunctor
. What do you think?
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you have pureP
, you have unitP
, so it needs to be on the UnitProfunctor
subclass, becuase
pureP a = dimap (const ()) (const a) unitP
unitP = pureP ()
I think I like the {Sum,Void,Product,Unit}Profunctor
names better, because to me that's the fundamental idea being captured: you're tying pairs of profunctors together in interesting ways, and sometimes you have units for these operations. From there, you can recover a bunch of familiar-looking operations from the Applicative
/Divisible
/Decidable
world, but I don't feel like any of them are compelling enough to take over the class names. Also, it makes the package name confusing if there's no ProductProfunctor
class any more.
Thanks for the great feedback. If you're happy to wait them I'm happy to discuss and polish until we feel we have the right design. I agree that If we're going for maximal consistency then I personally prefer naming everything after the standard Haskell typeclasses that exhibit the same properties. Curiously it seems that both |
I think class Contravariant f => Divisible f where
divide :: (a -> (b, c)) -> f b -> f c -> f a
conquer :: f a If we profunctorise it, we expect that behaviour on the first parameter, so we want something like: divideP :: (a -> (b, c)) -> p b x -> p c x -> p a x We can get tuples in the first position of So I think you can recover a profunctor-flavoured divideP :: (Semigroup x, ApplyProfunctor p) => (a -> (b, c)) -> p b x -> p c x -> p a x
divideP f p q = dimap f (uncurry (<>)) $ p2 p q
conquerP :: (Monoid x, ApplicativeProfunctor p) => p a x
conquerP = pureP mempty Symmetrically, can we construct a profunctor-flavoured -- Using "semigroupoids"-inspired classes because I think the 'Applicative' superclass might be a red herring?
class Functor f => Alt f where
-- (<!>) :: f a -> f a -> f a
alt :: f a -> f b -> f (Either a b)
alt left right = (Left <$> left) <!> (Right <$> right)
class Alt f => Plus f where
zero :: f a Profunctorise: altP :: p x a -> p x b -> p x (Either a b) We can then use
This seems surprising and strange. I don't know if I'm thinking too narrowly and this is only one possible monoid over profunctors of a certain subclass, or we're butting up against Mysterious Incomposability when It does make me lean towards favouring the |
Had an interesting talk with @gwils about this, and why it all looks so weird and asymmetrical. Basically, to recover one of (Applicative, Alternative, Divisible, Decidable) we take either the sum or product of Let's look at recovering the divideP :: (Semigroup x, ApplyProfunctor p) => (a -> (b, c)) -> p b x -> p c x -> p a x
divideP f p q = dimap f (uncurry (<>)) $ p2 p q
conquerP :: (Monoid x, ApplicativeProfunctor p) => p a x
conquerP = pureP mempty Why doesn't a class Comonoid w where
comappend :: w -> (w, w)
comempty :: w -> () Both of which are trivial for any class MonoidE a where
mappendE :: Either a a -> a
memptyE :: Void -> a Like comonoids using (,), the And this is why we struggled to implement class ComonoidE where
comappendE :: a -> Either a a
comemptyE :: a -> Void
|
Just like |
Revisiting this again after some haskell-cafe discussion: I think the naming principles that are currently making me the happiest are:
class Profunctor p => ProductProfunctor p where
-- Provide default in terms of (****)
(***!) :: p a c -> p b d -> p (a,b) (c,d)
-- Derived operations
(****) :: p x (a -> b) -> p x a -> p x b
-- In some cases (e.g., lifting an 'Applicative' via 'Joker'), the 'Semigroup' constraint is not required.
-- But I think it's almost always necessary and so easy to get that it's probably okay to require it.
divideP :: Semigroup x => (a -> (b,c)) -> p b x -> p c x -> p a x
-- Alternate name: liftA2P?
liftP2 :: (a -> b -> c) => p x a -> p x b -> p x c
class ProductProfunctor p => UnitProfunctor p where
unitP :: P () ()
-- Derived operations
pureP :: a -> p x a
conquerP :: Monoid x => p a x
class Profunctor p => SumProfunctor p where
(+++!) :: p a c -> p b d -> p (Either a b) (Either c d)
-- Derived operations
decideP :: (a -> Either b c) -> p b x -> p c x -> p a x
class SumProfunctor p => VoidProfunctor p where
voidP :: p Void Void
-- Derived operations
-- Returning @p a x@ instead of @p a Void@ keeps symmetry with 'pureP'.
-- This seems fine, as the @f@ argument essentially says "@a@ is impossible".
concludeP :: (a -> Void) -> p a x
concludeP f = dimap f absurd voidP |
Thanks, I like this rationale. I'm uneasy about Minor preference: |
I would also be happy with: class Profunctor p => SemiproductProfunctor p -- Formerly ProductProfunctor
class SemiproductProfunctor p => ProductProfunctor p -- Formerly UnitProfunctor (and similar for sums)
I assume you mean |
OK, great. This is beginning to take shape.
Ah yes indeed. In any case it is a very minor objection and I'm sure it can be resolved easily.. |
I think I'm missing something, but what's the motivation (maybe somewhere in this comment thread) for why there's no common superclass of Speaking as someone jumping into profunctors without the benefit of documentation or tutorials outside of what I can glean from papers, blogs and sporadic reddit threads --- please correct me if I'm wrong --- I also don't really understand exactly why all of this (the |
In theory, you could build out this hierarchy: flowchart TD
A(SemigroupalProfunctor) --> B
A --> C
A --> D
B(SemiproductProfunctor) --> E(ProductProfunctor)
C(MonoidalProfunctor) --> E
C --> F
D(SemisumProfunctor) --> F(SumProfunctor)
However, it gets awkward to use: import qualified Control.Category.Monoidal as C -- from package `categories`
class C.Associative (->) m => SemigroupalProfunctor m p | p -> m where
(!!!!) :: p a c -> p b d -> p (m a c) (m b d)
class (C.Monoidal (->) m, SemigroupalProfunctor m p) => MonoidalProfunctor m p | p -> m where
unit :: p (C.Id (->) m) (C.Id (->) m)
-- These merge their two arguments using (!!!!), then pull the unit off using
-- typeclass methods from `Monoidal`
lunit :: p (C.Id (->) m) (C.Id (->) m) -> p a b -> p a b
runit :: p a b -> p (C.Id (->) m) (C.Id (->) m) -> p a b Even so, you can't put more than one Ironically, the reason I got into hacking on this package is that I want to do bidirectional printing/parsing. That is, you staple an encoder to a decoder like So overall I'm not sure what benefit a common monoidal profunctor superclass gives you here. Hope that illuminates somewhat; let me know if it doesn't. |
I think I understand -- the same reasoning behind why e.g. defining a
In the short run, perhaps not much, but what about generally For example, in Elliot 2018 the |
I'm not quite sure how
I'm not sure what problems you foresee.
I think it's more likely that |
Also: What Elliot calls |
with Tom Ellis