6
import Control.Lens
import Control.Lens.TH

data Foo = Foo {
    _bar, _baz :: Int
   }
makeLenses ''Foo

Now if I want to modify both int fields, I can do

barbaz :: Setter' Foo Int
barbaz = sets $ \foo f -> foo & bar %~ f
                              & baz %~ f

but this seems like a pretty ugly manual way to do it.

Can the same be achieved directly with lens/arrow combinators?

leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
  • This seems to answer your question: http://stackoverflow.com/questions/17528119/combining-lenses – Shersh Mar 14 '17 at 16:25
  • Were you using a tuple instead of `Foo`, you could do something like [`(1,2) & both .~ 10`](https://hackage.haskell.org/package/lens-4.15.1/docs/Control-Lens-Traversal.html#v:both) to set both elements of the tuple to `10`. – Alec Mar 14 '17 at 17:43
  • You can combine setters like `bar %~ f` using [compose](https://wiki.haskell.org/Compose) – Janus Troelsen Nov 25 '20 at 03:34

2 Answers2

2

lens doesn't a ready-made combinator for that, presumably beacuase you can get illegal setters (or lenses) if the focuses of the components overlap.

data Trio a = Trio a a a
    deriving (Show)

oneTwo :: Setter' (Trio a) (a, a)
oneTwo = sets $ \f (Trio x y z) -> let (x', y') = f (x, y) in Trio x' y' z

twoThree :: Setter' (Trio a) (a, a)
twoThree = sets $ \f (Trio x y z) -> let (y', z') = f (y, z) in Trio x y' z'

cheating :: Setter' (Trio a) (a, a)
cheating = sets $ \f x -> x & oneTwo %~ f & twoThree %~ f
GHCi> Trio 1 1 1 & cheating %~ bimap (2*) (2*) & cheating %~ bimap (3+) (3+)
Trio 5 10 5
GHCi> Trio 1 1 1 & cheating %~ (bimap (2*) (2*) <&> bimap (3+) (3+))
Trio 5 13 5

In your case, the nicest alternative to building the setter/traversal by hand (as you and Cristoph Hegemann are doing) seems to be liftA2 (>=>) :: ASetter' s a -> ASetter' s a -> ASetter' s a, as suggested elsewhere by bennofs (thanks Shersh for the link). If you happen to have a lens to a homogeneous pair (or some other Bitraversable) lying around, you can get the traversal out of it with both:

data Foo = Foo
   { _bar, _baz :: Int
   } deriving (Show)
makeLenses ''Foo

barBaz :: Iso' Foo (Int, Int)
barBaz = iso ((,) <$> view bar <*> view baz) (Foo <$> fst <*> snd)
GHCi> Foo 1 2 & barBaz . both %~ (2*)
Foo {_bar = 2, _baz = 4}

Yet another possibility is exploiting Data.Data.Lens to get a traversal of all fields of a certain type:

{-# LANGUAGE DeriveDataTypeable #-}

import Control.Lens
import Data.Data.Lens
import Data.Data

data Foo = Foo
   { _bar, _baz :: Int
   } deriving (Show, Data, Typeable)
makeLenses ''Foo

barBaz :: Traversal' Foo Int
barBaz = template
GHCi> Foo 1 2 & barBaz %~ (2*)
Foo {_bar = 2, _baz = 4}
Community
  • 1
  • 1
duplode
  • 33,731
  • 7
  • 79
  • 150
1

I've had this problem before and have a solution that works for two lenses by combining them into a Traversal:

fields2 :: Lens' s a -> Lens' s a -> Traversal' s a
fields2 f1 f2 f s = (\v1 v2 -> s & f1 .~ v1 & f2 .~ v2) <$> f (s ^. f1) <*> f (s ^. f2)

barbaz = fields2 bar baz

This can than be used like:

foo & barbaz %~ f

It's a little dirty and doesn't scale but it works for me :D If someone posts a nicer answer I'll be very happy!

Christoph Hegemann
  • 1,434
  • 8
  • 13