well-typed / optics

Optics as an abstract interface
374 stars 24 forks source link

Add a function to traverse the contents of a lens #471

Open ocharles opened 1 year ago

ocharles commented 1 year ago

In lens, we have:

traverseOf :: LensLike f s t a b -> (a -> f b) -> s -> f t

When traverseOf is given a lens, we get

traverseOf :: Functor f => (a -> f b) -> s -> f t

The only corresponding function in optics is toLensVL. This has the same type and behavior, but the name is much less informative. I'd like to suggest optics gets a combinator that has a better name than toLensVL.

As an example of how I use this:

uninstallTool :: MonadThrow m => Finite numberOfSlots -> Toolbank numberOfSlots -> m (Tool, Toolbank numberOfSlots)
uninstallTool slotIndex (Toolbank toolbank) =
  getCompose $ Toolbank <$> traverseOf (V.ix slotIndex) (Compose . Slot.uninstallTool) toolbank

Here V.ix comes from Data.Vector.Sized. Slot.uninstallTool :: MonadThrow m => Slot -> m (Tool, Slot), so by using traverseOf with Compose, I essentially get the way to update a "toolbank slot", while also returning a Tool as a result.

Using toLensVL would make this much less obvious.

adamgundry commented 1 year ago

Hmm, we have traverseOf already, in Optics.Traversal. Although it only requires a Traversal and thus correspondingly the functor must be Applicative, so it is not a drop in replacement, but it should work in your example?

I suppose we could have traverseOfLens or something as a synonym for toLensVL, but I'm not sure it is really worth adding another name.

At the very least we could explain this better in Optics.Lens, though.

ocharles commented 1 year ago

Sorry, I should have said why traverseOf doesn't work. I'm traversing here with Compose ((,) Tool) m, and that is only an Applicative if we have Monoid Tool, which I don't have. This is because:

instance Monoid a => Applicative ((,) a)

This is why I'm looking for a lens-specific traversal, where I don't need as much as Applicative.

adamgundry commented 1 year ago

I see, sorry for not reading your example closely enough to spot that it doesn't support Applicative.

We currently have (excluding indexed versions)

toIsoVL     :: Is k An_Iso => Optic k is s t a b -> IsoVL s t a b
toPrismVL   :: Is k A_Prism => Optic k is s t a b -> PrismVL s t a b
toLensVL    :: Is k A_Lens => Optic k is s t a b -> LensVL s t a b
traverseOf  :: (Is k A_Traversal, Applicative f) => Optic k is s t a b -> (a -> f b) -> s -> f t
atraverseOf :: (Is k An_AffineTraversal, Functor f) => Optic k is s t a b -> (forall r. r -> f r) -> (a -> f b) -> s -> f t
traverseOf_ :: (Is k A_Fold, Applicative f)  => Optic' k is s a  -> (a -> f r) -> s -> f ()

which feels a bit inconsistent.

So it is plausible to add (some name for)

traverseOf' :: (Is k A_Lens, Functor f) => Optic k is s t a b -> (a -> f b) -> (s -> f t)

and perhaps we might also add

toTraversalVL :: Is k A_Traversal => Optic k is s t a b -> TraversalVL s t a b
toAffineTraversalVL :: Is k An_AffineTraversal => Optic k is s t a b -> AffineTraversalVL s t a b
toFoldVL :: Is k A_Fold => Optic' k is s a -> FoldVL s a

(and indexed versions).

Of course these are somewhat redundant, but the fact they are redundant is a deep and interesting property of the van Laarhoven representation. So I can see that application code might well want "traverseOf for a lens" without bringing the VL representation into it. And by having names for both we can more easily document the fact that e.g. traverseOf === toTraversalVL.

To summarise: I'm convinced we should add this, I'm just not sure what to call it. Neither traverseOfLens nor traverseOf' feels completely satisfactory, and nor does introducing an inevitable import clash by calling it traverseOf...

ocharles commented 1 year ago

No worries! Naming is indeed hard. My intuitive thoughts are something like traverseOneOf, but now that I write it, it's sounds like "one of many traversals", so that's no good. traverseLens doesn't sound so bad though - I don't think you need the Of once you're talking about lenses.

phadej commented 1 year ago

The naming problem comes already from how to name a class:

class Traversable t => Singleton t where
  -- the member really should be verb... compare: traverse, map, ..., ...
  singleton :: Functor f => (a -> f b) -> t a -> f (t b)

It only has instances for types isomorphic to (c, ) for some c. (Thus I call it singleton - traversable with a single element).

Agda calls it Decoration with traverseF: https://hackage-search.serokell.io/viewfile/Agda-2.6.2.2/src/full/Agda/Utils/Functor.hs#line-56 and I couldn't (quickly) find anything else on Hackage.

phadej commented 1 year ago

And FWIW, traverseOneOf is particularly bad name, as there is traverse1 in the wild, which does something else what traverseOneOf would do, even as optics doesn't support Traversable1 things.

EDIT: In fact AffineTraversal has similar function as well, atraverseOf.

So we have: