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

Introduce idiomatic operators and revisit fixities for existing ones #57

Open
chshersh opened this issue Aug 23, 2018 · 21 comments
Open

Comments

@chshersh
Copy link

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
Copy link
Contributor

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: dhall-lang/dhall-haskell#525 (comment). I just added >*< with one higher fixity and it seemed to work.

@tomjaguarpaw
Copy link

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
Copy link
Contributor

ocharles commented Sep 26, 2018

@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
Copy link

tomjaguarpaw commented Sep 26, 2018

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
Copy link

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 😅 .

@endgame
Copy link

endgame commented Sep 27, 2018

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
Copy link
Contributor

@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
Copy link
Contributor

gwils commented Oct 4, 2018

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
Copy link
Author

chshersh commented Oct 4, 2018

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
Copy link

@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
Copy link
Contributor

gwils commented Nov 20, 2018

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
Copy link

endgame commented Jul 13, 2019

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

  • What do we do about (>$$<)? 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.
  • What do we do about the fixity of (>$<)? Who is using it left-associatively, and why? How much would this break?
  • If we can tweak the fixity of (>$<), should we add the other operators? If so, what should their fixities be?
  • (Half-open) What should the operator names be? I acknowledge @theindigamer 's suggestions around operators, but I believe the majority opinion supports the (>*<)-style. Am I wrong?

@chshersh
Copy link
Author

@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
Copy link

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
Copy link

endgame commented Jul 13, 2019

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
Copy link
Author

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
Copy link

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
Copy link
Owner

ekmett commented Oct 21, 2021

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

@ekmett
Copy link
Owner

ekmett commented Oct 21, 2021

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

i like this move a lot.

@ekmett
Copy link
Owner

ekmett commented Oct 21, 2021

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
Copy link

echatav commented Feb 28, 2022

#71 adds the operators >* and *<

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

No branches or pull requests

8 participants