ekmett / contravariant

Haskell 98 contravariant functors
http://hackage.haskell.org/package/contravariant
Other
73 stars 24 forks source link

Introduce idiomatic operators and revisit fixities for existing ones #57

Open chshersh opened 6 years ago

chshersh commented 6 years ago

This year talk by @gwils introduced operators for Contravariant family of typeclasses (Contravariant/Divisible/Decidable) and gave example of how this can be used for pretty-printing library:

Specifically:

(>$<) :: Contravariant f => (b -> a) -> f a -> f b
(>*<) :: Divisible     f => f a -> f b -> f (a, b)
(>|<) :: Decidable     f => f a -> f b -> f (Either a b)
(>*)  :: Divisible     f => f a -> f () -> f a
(*<)  :: Divisible     f => f () -> f a -> f a

With the following fixity declarations:

infixr 3 >$<
infixr 4 >*<
infixr 3 >|<
infixr 4 >*
infixr 4 *<

Unfortunately, this conflicts with fixities for existing operator:

infixl 4 >$<

The proposal was discussed under the following Twitter thread:

/cc @gwils @vrom911

P.S. I also propose to rename >$$< to >&< to have naming more consistent with existing operators.

ocharles commented 6 years ago

I'd really like to get >*< into this library. I ran across this on a zero-to-quake-3 stream, and wanted to use the idea in this comment: https://github.com/dhall-lang/dhall-haskell/issues/525#issuecomment-408451394. I just added >*< with one higher fixity and it seemed to work.

tomjaguarpaw commented 6 years ago

FWIW I don't find that these Divisible operators compose or generalise well. I prefer

\a b -> contramap fst a <> contramap snd b
\a b -> a <> contramap (const ()) b
\a b -> contramap (const ()) a <> b

Trying to make Divisible look too much like Applicative hasn't turned out to be very useful in my experience. At most I would suggest adding something like

fromUnit :: Contravariant f => f () -> f a
fromUnit = contramap (const ())
ocharles commented 6 years ago

@tomjaguarpaw I think it worked well enough here: https://github.com/ocharles/zero-to-quake-3/blob/master/src/Quake3/Vertex.hs#L28,L40. Sure, the tupling is a bit inconvenient, but it's not too bad.

tomjaguarpaw commented 6 years ago

Wouldn't you prefer

vertexFormat :: VertexFormat Vertex
vertexFormat =
   contramap vPos        v3_32sfloat
<> contramap vSurfaceUV  v2_32sfloat
<> contramap vLightmapUV v2_32sfloat
<> contramap vNormal     v3_32sfloat
<> contramap vColor      v4_8uint

?

typesanitizer commented 6 years ago

My 2c w.r.t. operator symbols - it would be more intuitive if the * was changed to / (Divisible is the opposite of Applicative). So more like (Option 1):

(>$<) :: Contravariant f => (b -> a) -> f a -> f b
(</>) :: Divisible     f => f a -> f b -> f (a, b)
(>|<) :: Decidable     f => f a -> f b -> f (Either a b) -- can't use <&> here due to <&> = flip fmap
(</)  :: Divisible     f => f a -> f () -> f a
(/>)  :: Divisible     f => f () -> f a -> f a

However, that isn't a perfect scheme because if you have </>, that'll collide with System.FilePath.(</>) :-/. Also, now the arrowheads don't all match up.

Perhaps the arrowheads can be kept flipped for consistency, but using / instead of *. So Option 2 would be:

(>$<) :: Contravariant f => (b -> a) -> f a -> f b
(>/<) :: Divisible     f => f a -> f b -> f (a, b)
(>|<) :: Decidable     f => f a -> f b -> f (Either a b)
(>/)  :: Divisible     f => f a -> f () -> f a
(/<)  :: Divisible     f => f () -> f a -> f a

Perhaps this scheme might break consistency with other things; in that case feel free to ignore my comment :sweat_smile: .

endgame commented 6 years ago

I think the duality with Applicative is a good reason to retain the *. The <> around an operator "lifts" it in some sense, and >< around an operator is the dual.

ocharles commented 6 years ago

@tomjaguarpaw that's a fair point. At first I wanted to reply with "No! That would let me rearrange things such that they don't follow the shape of the data type!". But now that I think about it... I'm not sure I care about that. I do care about the order of the <> calls as they define a memory layout, but there's no strong reason that the data type has to dictate that. So I'm happy with your suggestion (and will probably change my code as such).

gwils commented 6 years ago

Something I appreciate about Applicative style is that if I extend my data type, the compiler will remind me that I have to extend my applicative expressions as well. Consider a constructor Thing which takes three parameters, used in an applicative expression like so Thing <$> x <*> y <*> z if I add add an extra parameter to Thing, the compiler will tell me that my applicative expression no longer type checks and I'll have to come along and add something Thing <$> x <*> y <*> z <*> w.

In contravariant land, I find the (<>)-based style very convenient, but it does not have this property. When the data type changes, I won't get an error telling me to update my contramap f x <> ... chain. The (>*<)-based style can have this property if one's tupling function is defined with a pattern match rather than using record selectors. vector (Vector x y z w) = (x, (y, (z, w))) Now the compiler will make sure I update my divisible expression when I update my data type.

I prefer the tuple and (>*<) based style if I think my data type is likely to change in the future since it will lead to more help from the compiler when that time comes. If I think the data type isn't likely to change, I'm more likely to use the (<>)-based style for its convenience. In my sv library I've even got an optics-powered version of that style

Since I don't find one style strictly superior, I think both should exist.

I'll write a bit here about why I chose to use (>*<) in the talk in the way that I did, in case anyone finds it interesting or it can be helpful to the discussion. I chose it because I thought it more viscerally demonstrated the relationship to Applicative. I've found that when I tell someone there is a "contravariant form of Applicative" usually they start thinking about what a contravariant (<*>) :: f (a -> b) -> f a -> f b might look like. It turns out that divide is much more like a contravariant liftA2 than it is a contravariant (<*>). So in the talk I showed the audience Applicative in terms of liftA2 to make the connection to Divisible more visually obvious when I later introduced it. But I still felt like there wasn't a visceral sense of "applicativeness" conveyed, since defining Applicative in terms of liftA2 would already have been a new concept for most audience members. Introducing the infix operators at the end and then applying them to an example served to drive home that Divisible really is related to the Applicative we all know and love.

chshersh commented 6 years ago

Here's a little utility that can help with Divisible and Decidable adoption. Turned out, it's possible to implement generic version of adapt function that converts any data type to nested tuples. For example, if you have the following data types:

data Engine = Pistons Int | Rocket
  deriving (Generic, Show)

data Car = Car
    { carMake   :: String
    , carModel  :: String
    , carEngine :: Engine
    , carYears  :: Int
    } deriving (Generic)

You can then use generic adapt to convert it to nested tuples and Eithers:

ghci> :t adapt @Engine
adapt @Engine :: Engine -> Either Int ()
ghci> adapt (Pistons 3)
Left 3
ghci> adapt Rocket
Right ()

ghci> :t adapt @Car
adapt @Car :: Car -> (([Char], [Char]), (Engine, Int))
ghci> adapt (Car "foo" "bar" Rocket 3)
(("foo","bar"),(Rocket,3))

GHC rebalances generic representation, so for Car you have ((String, String), (Engine, Int)) instead of (String, (String, (Engine, Int))).

And here is the implementation:

This is my first Generic code ever, so it might be not that good (and doesn't automatically expand nested data types in a smart way). But I hope that it can help the situation.

tomjaguarpaw commented 6 years ago

@gwils: If you write in <>-style thus

doAThing (Vector x y z w) = thing x <> thing y <> thing z <> thing w

then you will indeed get a compiler error when your datatype changes.

gwils commented 6 years ago

That's true, I'll keep it in mind.

Rereading the thread, I notice that that style isn't an alternative way to work with Divisible, rather it's an alternative to Divisible entirely - use Semigroup instead. As such, it seems an unrelated concern to whether infix operators are added to Divisible.

Being able to use Semigroup instead also depends on <> and divide delta agreeing. Although divide delta must be obey the semigroup laws, there's no law stating that it must be the same semigroup as the Semigroup instance.

endgame commented 5 years ago

It would be really great if this was resolved. The open questions seem to be:

chshersh commented 5 years ago

@endgame

Is there a good way to grep hackage, to see the implication of removing it?

Yes, there's Uses button on Hoogle that uses Aelve's Codesearch to grep through all packages. And grepping for >$$< shows that's it not used that much and it shouldn't be too painful to replace it

tomjaguarpaw commented 5 years ago

I'd like to see some real-life use-cases of the Divisible operators that aren't better expressed with <> (or a Divisible-specialised equivalent).

endgame commented 5 years ago

I also just noticed that (<$>) is also infixl 4. I wonder if the fixities of the proposed operators could be twiddled in a way that you get nice code without needing to change the fixity of (>$<)?

chshersh commented 3 years ago

I believe, part of this issue is resolved by @Gabriel439, specifically:

For reference, here is the recent blog post that describes the technique and convenience of using the >*< operator:

tomjaguarpaw commented 3 years ago

I'm still baffled why >*< style is more appealing to some than contramap-<> style. See my response to Gabriella's Twitter thread for some more comparisons.

ekmett commented 3 years ago

i admit the fixity change is the only real problem i have with adding these as is.

ekmett commented 3 years ago

P.S. I also propose to rename >$$< to >&< to have naming more consistent with existing operators.

i like this move a lot.

ekmett commented 3 years ago

The problem with the >$$< -> >&< move and anything that changes the fixity of >$< is that those are all base-facing changes, as have been commented here.

What happens if we shift the priorities up by one everywhere?

echatav commented 2 years ago

71 adds the operators >* and *<