Open chshersh opened 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.
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 ())
@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.
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
?
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: .
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.
@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).
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.
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 Either
s:
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.
@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.
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.
It would be really great if this was resolved. The open questions seem to be:
(>$$<)
? Data.Functor.Contravariant
is now in base
, so its removal might be complex. (Is there a good way to grep hackage, to see the implication of removing it?) We should at least alias (>&<) = (>$$<)
, IMHO.(>$<)
? Who is using it left-associatively, and why? How much would this break?(>$<)
, should we add the other operators? If so, what should their fixities be?(>*<)
-style. Am I wrong?@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
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).
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 (>$<)
?
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:
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.
i admit the fixity change is the only real problem i have with adding these as is.
P.S. I also propose to rename >$$< to >&< to have naming more consistent with existing operators.
i like this move a lot.
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?
>*
and *<
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:
With the following fixity declarations:
Unfortunately, this conflicts with fixities for existing operator:
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.