Combining lenses
Asked Answered
N

2

17

Using a lens library I can apply a modification function to individual targets, like so:

Prelude Control.Lens> (1, 'a', 2) & _1 %~ (*3)
(3,'a',2)
Prelude Control.Lens> (1, 'a', 2) & _3 %~ (*3)
(1,'a',6)

How can I combine those individual lenses (_1 and _3) to be able to perform this update to both of the targets at once? I expect something in the spirit of the following:

Prelude Control.Lens> (1, 'a', 2) & ??? %~ (*3)
(3,'a',6)
Naominaor answered 8/7, 2013 at 13:38 Comment(4)
I'm not sure that there is a sensible implementation of such an operation. Say that the operator that combines your two lenses is (&&&). It must have a type something like (&&&) :: Lens a b -> Lens a b -> Lens a b so that you can use it in the same way as the two lenses that you combine to make it. Given that view _1 (1,2) = 1 and view _2 (1,2) = 2, what do you expect the result of view (_1 &&& _2) (1,2) to be?Shanitashank
@ChrisTaylor I don't really need the "getter" functionality. Although AFAIU in this library it is conventional to approach such cases with monoid, e.g. the Traversal.Naominaor
I linked this in the comments below, but in case someone misses it, there's a lens issue about combining traversals and the trouble with it.Knight
You cannot combine lenes, or setters safely in such a way in general because there is no way to enforce that the two lens or setters do not overlap.Causation
T
19

Using untainted from the Settable type class in Control.Lens.Internal.Setter, it is possible to combine two setters, but the result will also only be a setter and not a getter.

import Control.Lens.Internal.Setter

-- (&&&) is already taken by Control.Arrow
(~&~) :: (Settable f) => (c -> d -> f a) -> (c -> a -> t) -> c -> d -> t
(~&~) a b f = b f . untainted . a f

You can test this:

>>> import Control.Lens
>>> (1, 'a', 2) & (_1 ~&~ _3) %~ (*3)
(3,'a',6)

EDIT

You don't actually need to use internal functions. You can use the fact that Mutator is a monad:

{-# LANGUAGE NoMonomorphismRestriction #-}

import Control.Monad
import Control.Applicative

(~&~) = liftA2 (>=>)

-- This works too, and is maybe easier to understand: 
(~&~) a b f x = a f x >>= b f
Telethon answered 8/7, 2013 at 14:40 Comment(5)
Thanks! Is there a way to maybe construct a Traversal?Naominaor
No. It unfortunately doesn't really work in general.Knight
(_1 ~&~ _1) does not satisfy the Setter (aka SEC) laws.Causation
((_1 ~&~ _1) %~ ('y':)) . ((_1 ~&~ _1) %~ ('z':)) $ ("",()) === ("yyzz",()) while (_1 ~&~ _1) %~ (('y':) . ('z':)) $ ("",()) === ("yzyz",())Causation
@RussellO'Connor So in other words, setter1 ~&~ setter2 is only a valid setter if setter1 and setter2 are "orthogonal"?Anderegg
S
7

There is a variation on what you are asking for which is more general:

(/\)
    :: (Functor f)
    => ((a -> (a, a)) -> (c -> (a, c)))
    -- ^ Lens' c a
    -> ((b -> (b, b)) -> (c -> (b, c)))
    -- ^ Lens' c b
    -> (((a, b) -> f (a, b)) -> (c -> f c))
    -- ^ Lens' c (a, b)
(lens1 /\ lens2) f c0 =
    let (a, _) = lens1 (\a_ -> (a_, a_)) c0
        (b, _) = lens2 (\b_ -> (b_, b_)) c0
        fab = f (a, b)
    in fmap (\(a, b) ->
            let (_, c1) = lens1 (\a_ -> (a_, a)) c0
                (_, c2) = lens2 (\b_ -> (b_, b)) c1
            in c2
            ) fab

infixl 7 /\

Just focus on the type signature with lens type synonyms:

Lens' c a -> Lens' c b -> Lens' c (a, b)

It takes two lenses and combines them into a lens to a pair of fields. This is slightly more general and works for combining lenses that point to fields of different types. However, then you'd have to mutate the two fields separately.

I just wanted to throw this solution out there in case people were looking for something like this.

Sheryllshetland answered 8/7, 2013 at 18:0 Comment(1)
A pithier type, for those like them (/\) :: LensLike' ((,) a) c a -> LensLike' ((,) b) c b -> Lens' c (a, b). Also, the obvious restriction, (/|\) :: LensLike' ((,) a) c a -> LensLike' ((,) a) c a -> Traversal' c a with a /|\ b = (a /\ b) . both. Why aren't these in lens?Heisler

© 2022 - 2024 — McMap. All rights reserved.