Updating multiple subfields of a field using ekmett’s Lens
Asked Answered
B

1

7

Let’s play a game. There are two piles we’re going to use, both consisting of black/white sided chips.

data Pile = Pile { _blacks, _whites :: Int }
makeLenses ''Pile

data Game = Game { _pileA, _pileB :: Pile }
makeLenses ''Game

A really clever move would be to turn over a black chip in pile A, and a white chip — in pile B. But how?

cleverMove :: Game -> Game
cleverMove game = game & pileA . blacks -~ 1
                       & pileA . whites +~ 1
                       & pileB . blacks +~ 1
                       & pileB . whites -~ 1

Not very elegant. How can I do it without referencing each pile twice?

The only thing I came up with (and I don’t like it):

cleverMove game = game & pileA %~ (blacks -~ 1)
                                . (whites +~ 1)
                       & pileB %~ (blacks +~ 1)
                                . (whites -~ 1)

(Sorry in advance if it’s obvious — I’m kinda new to lenses and I feel lost in the sea of combinators and operators lens offers. There’s probably everything for everybody’s needs hiding there. Not that it’s bad, of course! but I wish there was also a complete manual included.)

Beachhead answered 3/6, 2013 at 0:4 Comment(5)
What do you not like about option 2? It can't get much more concise than that, can it?Clie
@Clie I don’t like that I had to use brackets, which become clumsy when there are multiline expressions involved — such as do blocks and further levels of nesting.Beachhead
I think it would help if you showed some rough pseudo-code corresponding to what your ideal syntax would look like.Battles
@GabrielGonzalez My ideal syntax is the same as my “non-ideal” syntax, except without brackets. The thing is, I want to know the idiomatic solution, if there is one (and since updating multiple subfields should be a rather common task, I’d be surprised to know that there isn’t one).Beachhead
If the individual updates start to consume multiple lines, it is probably time to step back, start a let or where block, and start naming things.Bow
S
10

A Traversal is a generalization of Lens which "focuses" on multiple values. Think of it like traverse which allows you to step through a Traversable t modifying values in an Applicative (traverse :: (Applicative f, Traversable t) => (a -> f b) -> t a -> f (t b) looks pretty much like the type of a Lens already, you'll notice—just think of t b ~ whole).

For a Traversal we can just pick off the values we want to change. For instance, let me generalize your Pile a bit and built a Traversal.

data Pile = Pile { _blacks :: Int, _whites :: Int, _name :: String } deriving (Show)
$(makeLenses ''Pile)

counts :: Traversal' Pile Int
counts f (Pile blacks whites name) = 
  Pile <$> f blacks <*> f whites <*> pure name

so as you can see, I visit both the blacks and the whites with f but leave name pure. This is almost the same way you write a Traversable instance except you always visit all of the (homogenous) elements contained in the Traversable structure.

Main*> Pile 0 0 "test" & counts +~ 1
Pile {_blacks = 1, _whites = 1, _name = "test"}

This isn't enough to do what you want, though, since you need to update your fields in different ways. For that, you need to specify your logic and ensure it upholds an entirely different set of rules.

blackToWhite :: Pile -> Pile
blackToWhite = (blacks -~ 1) . (whites +~ 1)
Switchman answered 3/6, 2013 at 14:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.