Question

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.)

Was it helpful?

Solution

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)
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top