6

When you look at setting in lens-family-core package, you'll find that its type is Identical f => ((a -> b) -> s -> t) -> LensLike f s t a b, and Identical is defined as class (Traversable f, Applicative f) => Identical f.

I understand it needs Applicative and Identical as it uses pure and extract, but I'm not sure why it needs Traversable.

setting :: Identical f => ((a -> b) -> s -> t) -> LensLike f s t a b
setting sec f = pure . sec (extract . f)

I also found that Setter in lens package had Traversable constraint through Settable. So I guess a setter needs Traversable in general.

Why does a setter need Traversable in general?

snak
  • 6,483
  • 3
  • 23
  • 33

2 Answers2

4

Setters don't really need Traversable, but effectively you enforce having this constraint anyway because Identical / Settable are classes that only allows trivial instances. I.e., either Identity or something isomorphic to it, and all of these are obviously traversable as well.

Notice you could always write

idenTraverse :: (Identical t, Applicative g) => (a -> g b) -> t a -> g (t b)
idenTraverse f = fmap pure . f . extract

which is equivalent to the standard traverse. (Actually this needs only Functor g, incidentally.)

Explicitly requiring the standard Traversable interface makes for a cleaner, more consistent hierarchy, without actually changing what you can or can't do with the setters.

leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
4

Instances of both Settable from lens and Identical from lens-family must be isomorphic to Identity. (On why that must be so, see hao's answer to Isn't it redundant for Control.Lens.Setter to wrap types in functors? , and in particular the caveat at the very end of it.) Now, if our goal were merely to make sure a functor is isomorphic to Identity, we wouldn't necessarily have to set up a brand new class just for that purpose, as the following constraint would suffice:

type Setter s t a b = forall f. (Distributive f, Lone f) => (a -> f b) -> s -> f t

Here, Lone is a subclass of Traversable for functors which hold exactly one value, which doesn't rely on Applicative:

class Traversable f => Lone f where
    sequenceL :: Functor m => f (m a) -> m (f a)
    traverseL :: Functor m => (a -> m b) -> f a -> m (f b)

Distributive functors are isomorphic to functions, and Lone functors are isomorphic to pairs. If a functor is both Distributive and Lone, it must be isomorphic to a function functor and to a pair functor. That is only possible if the argument type of the function and the fixed-type component of the pair are both (isomorphic to) (), which in turn means the functor must be isomorphic to Identity.

Arranging the constraint for Setter in this way wouldn't have been alien to the style of optics type synonyms seen in lens. In fact, it would be reminiscent of how Getter relies on the combination of Functor and Contravariant to ensure the functor it uses is phantom on its argument. One inconvenience, however, is there is no canonical implementation of Lone in the ecosystem. Since lens would have to define the class anyway, it might as well set up a special purpose class like Settable instead, which in any case has the advantage of making signatures and type errors slightly more self-explanatory.

Given that Settable is ultimately a stand-in for the combination of Distributive and Lone, it makes sense to make it a subclass of Distributive and Traversable. Doing so delineates the provenance of the class, with Lone being just one further restriction away from Traversable. While lens-family-core omits the Distributive constraint (presumably to avoid depending on the distributive package), we can sense a similar motivation from a comment to the definition of Identical:

-- It would really be much better if comonads was in tranformers
class (Traversable f, Applicative f) => Identical f where
  extract :: f a -> a

Lone functors are all comonads, and adding a Comonad superclass to Identical in principle would allow not defining extract anew, while highlighting a different aspect of what an Identical functor is supposed to be like.

A couple notes to wrap things up. Firstly, you may have noticed I haven't mention the Applicative constraint at all so far. Even though the constraint is necessary in practice to make pure available for the implementation of setting, it is in a sense not as essential as the others. Distributive functors are necessarily applicative, and Distributive doesn't have Applicative as a superclass merely because (<*>) is awkward to express in terms of the distributive interface. pure itself, on the other hand, can be straightforwardly implemented in its terms:

pureD :: Distributive g => a -> g a
pureD = cotraverse getConst . Const 

Secondly, it is worth mentioning there is a more succinct way to specify that a Functor (that is, a Hask endofunctor) must be isomorphic to Identity: using the Adjunction class to state it must be adjoint to itself. Setter, then, might be made to look like this:

type Setter s t a b = forall f. Adjunction f f => (a -> f b) -> s -> f t

(Incidentally, Distributive functors are precisely the right adjoint endofunctors, and Lone functors are precisely the left adjoint ones.)

This constraint, however, is far from self-explanatory, and Adjunction isn't exactly a class in widespread use, so ditching Settable in favour of it doesn't look like a very attractive option.

duplode
  • 33,731
  • 7
  • 79
  • 150
  • 3
    You answered most of the questions I planed to ask even before I actually ask them. Thank you! – snak Jun 07 '23 at 04:42