4

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)
effectfully
  • 12,325
  • 2
  • 17
  • 40
  • For examples of combinators, see things like `over`, `view`, `preview`, `set`, `toListOf`, `sequenceOf`, etc. – Justin L. May 05 '18 at 22:28
  • All those super-classes are there because the whole idea of `Conjoined` is that it's either a function or an indexed function, and in either case those instances are all available. It's not the most principled thing. – dfeuer May 05 '18 at 22:48
  • @JustinL.. `over`, `set`, `toListOf` all have their indexed counterparts, not sure about the others. – effectfully May 05 '18 at 23:19
  • @dfeuer, yes, but why are they needed? You don't write something, because you can. You write something for a reason. Just because you can use `Indexable` somewhere downstream and need `MonadFix` for whatever reason? I'd rather prefer to see where exactly this is needed rather than permit to use those instances in every function that has the `Indexable` constraint. – effectfully May 05 '18 at 23:21
  • 1
    Edward Kmett has explicitly told me that `Conjoined` has all those superclasses because it can. The fact that you don't like that reasoning does not mean it's wrong. – dfeuer May 05 '18 at 23:38
  • @dfeuer, it's just strange to [scare people](http://fvisser.nl/post/2013/okt/11/why-i-dont-like-the-lens-library.html) with all those classes, just because you can write them. I mean, I have never seen this reasoning, is it applied somewhere else too? I thought it's more like "because that maximally constrains the class, so it's clearer that there aren't many instances". – effectfully May 05 '18 at 23:47
  • @user3237465, the `Comonad (Rep p)` superclass certainly gets a lot of use. The `Category p` superclass is nice to have too, as is the `Profunctor p` one. I'm not really sure which of them are used "in the wild". I think generally `Conjoined` is a rather peculiar thing to have, but it apparently exists for practical efficiency and convenience reasons rather than any theoretical ones. – dfeuer May 06 '18 at 03:16
  • @user3237465 `over` has an indexed counterpart, but you can still use indexed lenses with `over` if you want to ignore the index. If the library was designed differently, `over` wouldn't work as an index-ignoring over for indexed lenses. – Justin L. May 07 '18 at 08:37
  • @user3237465 `folded` uses this trick; it's technically an indexed fold, but it can be used as if it were a normal fold. So in defining `folded`, you can have both a normal fold and an indexed fold, and users using it as a normal fold wouldn't even know. – Justin L. May 07 '18 at 08:39
  • @JustinL., but then `iover` is not a cut name. Also the other my questions are exactly about why do I want to use `over` over an indexed lens. Thanks for the `folded` example, it's indeed a cut name. Here are a couple of others: `worded`, `lined`. I checked all matches of `IndexedFold` and `IndexedLensLike` in `Control.Lens.Fold` and do not see other such functions. I'd rather prefer to have `folded` and `ifolded`, but `worded` and `lined` seem nice, also I think there just should be a combinator for recovering indices in plain folds. – effectfully May 07 '18 at 12:13
  • @dfeuer, I do not think it's a bad class: efficiency concerns are as important as theoretical ones. I think that without `distrib` and that pile of superclasses it would be clearer (unless it doesn't make sense to remove something from the class, of course). It's "practical", but it looks "theoretical" and intimidating. – effectfully May 07 '18 at 13:07
  • I'm not really sure what `distrib` is for, or why there isn't a `contradistrib :: Contravariant f => p a b -> p (f b) (f a)`, but it's pretty simple. – dfeuer May 07 '18 at 18:52

0 Answers0