Diagonal setters
Opened this issue · 3 comments
Sometimes you have a functor from a product category:
class Bifunctor t
where
mapBoth :: (a -> a') -> (b -> b') -> t a b -> t a' b'
You can equivalently represent such a functor by a pair of mapping operations:
mapFirst :: (a -> b) -> t a x -> t b x
mapSecond :: (a -> b) -> t x a -> t x b
mapBoth f g = mapFirst f . mapSecond g
mapFirst = flip mapBoth id
mapSecond = mapBoth id
The basic idea behind Setter
is to generalize the type (if not the mathematical concept) of fmap
. So whereas:
fmap :: forall a b. (a -> b) -> f a -> f b
we have:
type Setter s t a b = (a -> b) -> s -> t
This gives:
fmap :: forall a b. Setter (f a) (f b) a b
And similarly:
mapFirst :: forall a b x. Setter (t a x) (t b x) a b
mapSecond :: forall a b x. (t x a) (t x b) a b
So far so good. Now, sometimes, it is useful to view a Bifunctor
as a simple Functor
, and use the same morphism to map both type parameters. This can be viewed as precomposing a "diagonal functor" with our Bifunctor
:
newtype Diagonal :: (* -> * -> *) -> (* -> *)
newtype Diagonal t a = Diagonal { getDiagonal :: t a a }
instance Bifunctor t => Functor (Diagonal t)
where
fmap f = Diagonal . mapFirst f . mapSecond f . getDiagonal
Since we're working with the lens
library, we want to forget all about classes and newtypes and work with first class values. So pretending that mapFirst
and mapSecond
are some arbitrary setters, the basic operation that is being done to them to obtain fmap @Diagonal t
above is:
diagonal mapFirst mapSecond = \f -> mapFirst f . mapSecond f
We can annotate this with a partially filled out type to see what was inferred:
diagonal :: Setter _ _ _ _ -> Setter _ _ _ _ -> Setter _ _ _ _
diagonal mapFirst mapSecond = \f -> mapFirst f . mapSecond f
We can start out by filling everything in the parameters with unrelated type variables, and see what breaks:
diagonal :: Setter s t a b -> Setter s' t' a' b' -> Setter _ _ _ _ -- this no longer typechecks
diagonal mapFirst mapSecond = \f -> mapFirst f . mapSecond f
The minimum constraints we need to get this to typecheck again are:
a ~ a'
b ~ b'
s ~ t'
Unifying these variables and renaming, we have:
diagonal :: Setter x o a b -> Setter i x a b -> Setter _ _ _ _
diagonal mapFirst mapSecond = \f -> mapFirst f . mapSecond f
And we can just read off the inferred types to fill out the rest of the holes:
diagonal :: Setter x o a b -> Setter i x a b -> Setter i o a b
diagonal mapFirst mapSecond = \f -> mapFirst f . mapSecond f
I'd like to add the diagonal
combinator to Control.Lens.Setter
, but:
- I don't know whether this operator already exists or is a specialization of something else in the lens library
- I'd like for it to be an infix operator, or if named in natural language, to be something like a preposition that reads naturally in between the names of two setters. I can't think of a good name of either kind.
What do you folks think?
Diagonal is called Join
in https://hackage.haskell.org/package/bifunctors-5.5.10/docs/Data-Bifunctor-Join.html
This is not "safe" composition:
{-# LANGUAGE RankNTypes #-}
import Control.Lens
setter :: Setter' Int Int
setter = id
-- setter laws are:
--
-- over s id = id
-- over s f . over s g = over s (f . g)
diagonal :: Setter x o a b -> Setter i x a b -> Setter i o a b
diagonal s1 s2 = setting $ \f -> over s1 f . over s2 f
setter2 :: Setter' Int Int
setter2 = diagonal setter setter
lhs = over setter2 (+ 1) . over setter2 (* 2) $ 1 -- = 6
rhs = over setter2 ((+ 1) . (* 2)) $ 1 -- = 7
So at best it would live in https://hackage.haskell.org/package/lens-5.0.1/docs/Control-Lens-Unsound.html
This is not a problem with Bifunctor
definition as there parametricity ensures that parts are disjoint. (So both
is valid traversal & setter). If we however take arbitrary Setter
s, that is not true anymore.
Well, not every Bifunctor
is Bitraversable
, so it seems to me you'd still want some analog of both
for simple Bifunctor
s.
That said, this scheme will work for a diagonalization of anything that forms an n-ary functor in the mathematical sense, including e.g. ones found in mono-traversable (which don't actually form Bifunctor
s proper), or over 2/3 positions of a trifunctor, etc. So whether the general form of diagonal
lives in Control.Lens.Unsound
or elsewhere I believe it would be useful to have in the library.
Do you want
Bifunctor
analogue ofboth
, i.e.? :: Bifunctor p => Setter (p a a) (p b b) a b
- setter "parallel" composition, i.e.
setterParComp :: Setter x o a b -> Setter i x a b -> Setter i o a b
(it forms lawful setter when setters don't touch same bits, likediagonal id id
example I showed) - both?
I'm fine with any of the three. please make a PR, and try to get @ekmett to agree on a naming.
My idea of setterParComp
is just saying what it is, like prismSum
and lensProduct
, the name which is probably unusable for you as you wish something "usable". I'm against having operator for it, it would be obscure outlier in a consistent (though large) operator soup. If there is a word which is usable as infix operator, that might work.
I don't know what would be a good name for Bifunctor
both
. There is both1
which is Traversable1
variant.