There are some very helpful explanations in this reddit thread of how indexed lenses are defined, but I'd like to know the details.
So the main point is that indexed lenses are defined such that they can be treated as non-indexed ones too. These two classes are responsible for that:
class (Choice p, Corepresentable p,
Comonad (Corep p), Traversable (Corep p),
Strong p, Representable p, Monad (Rep p),
MonadFix (Rep p), Distributive (Rep p),
ArrowLoop p, ArrowApply p, ArrowChoice p
) => Conjoined p where
distrib :: Functor f => p a b -> p (f a) (f b)
conjoined :: (p ~ (->) => q (a -> b) r) -> q (p a b) r -> q (p a b) r
class Conjoined p => Indexable i p where
indexed :: p a b -> i -> a -> b
Questions:
0) Why is it useful to treat indexed lenses as non-indexed ones? Edward (see the link above) says
This let us cut out 30+ combinator names and let us share one combinator for the indexed and unindexed versions of most operations.
What are those combinators? I'd appreciate a few examples.
No matter how Conjoined
is smart, I guess it's still more efficient (no need to prey for full inlining so dictionaries are not passed around) and flexible to use non-indexed optics when possible. And indeed I see this:
-- If you don't need access to the index, then 'mapped' is more flexible in what it accepts.
imapped :: IndexedSetter i (f a) (f b) a b
(Since it's class Functor f => FunctorWithIndex i f | f -> i
would it be better to replace the imap (const id) ≡ id
law with the imap (const f) ≡ fmap f
one?)
The itraversed . itraversed
behavior which ignores the former index and returns the latter looks rather implicit and confusing to me. And this complicates type inference and errors. Also, I saw somewhere that traversed . traversed
doesn't work for some definition of "work", is this correct?
Should I write only indexed optics for my library and be fine with it or is it still required to provide both indexed and non-indexed variants of combinators?
1) Conjoin
is a very neat trick, but it's far from being obvious and it's underdocumented. I suppose, some people who define their own indexed optics combinators (is it common?) do not know they should use conjoin
. Is it possible to enforce the usage of conjoin
somehow?
Are even MonadFix (Rep p)
and ArrowLoop p
constraints required for Conjoin
to work? What is distrib
for? How does it relate to Mapping
?
class (Traversing p, Closed p) => Mapping p where
map' :: Functor f => p a b -> p (f a) (f b)
What is the Conjoined ReifiedGetter
instance for?
2) Is there a less ad hoc structure than Indexable
? Say, something like
class Functor f => StarLike p f | p -> f where
runStarLike :: p a b -> a -> f b
type Indexable i p = (Conjoined p, StarLike p ((->) i)