rafl
rafl

Reputation: 12341

Combining "Lens' s a" and "a -> Lens' s b" into a "Lens' s b"

This is a simplified example of situations I somewhat commonly run into when working with Control.Lens. It intends to represent a turn-based two-player game.

data PlayerState = PlayerState { _score :: Int, ... }
$(makeLenses ''PlayerState)

data Player = PlayerA | PlayerB

data GameState = GameState { _playerA, _playerB :: PlayerState,
                             _curPlayer :: Player }
$(makeLenses ''GameState)

Getting a lens for the current player's state given the current player is simple:

playerState :: Player -> Lens' Game PlayerState
playerState PlayerA = playerA
playerState PlayerB = playerB

However, I'd like to be able to focus on the current player's state within a game state:

curPlayerState :: Lens' GameState PlayerState
curPlayerState = ???

-- enabling things along the lines of
-- someGame&curPlayerState.score += 42

One way of implementing the above is using a separate setter and getter:

curPlayerState = lens getter setter
  where getter g   = g ^. (playerState (g ^. curPlayer))
        setter g s = g &  (playerState (g ^. curPlayer)) .~ s

However, this seems like I'm underusing or misusing Control.Lens, as both getter and setter combine the same lenses in pretty much the same way, only differing in how the resulting lens is used.

Is there a more general way of combining a Lens' s a and an a -> Lens' s b into a Lens' s b? It feels like I want something similar to >>=, but I've not yet been able to find the right combination of combinators to achieve the desired result with my lack of understanding of the actual types underlying Lens'.

Upvotes: 1

Views: 96

Answers (1)

Daniel Wagner
Daniel Wagner

Reputation: 152707

It's easy enough to write the combinator you're asking for.

unsafeBind :: Lens' s a -> (a -> Lens' s b) -> Lens' s b
unsafeBind a f b s = f (s ^. a) b s

But the fact that we mention s twice on the right hand side should make your hackles rise. Indeed, this isn't safe. Consider:

aBitOdd :: Player -> Lens' GameState (Player, PlayerState)
aBitOdd pl f s = case pl of
    PlayerA -> f (_curPlayer s, _playerA s) <&> \(cur', a') -> s { _curPlayer = cur', _playerA = a' }
    PlayerB -> f (_curPlayer s, _playerB s) <&> \(cur', b') -> s { _curPlayer = cur', _playerB = b' }

bad :: Lens' GameState (Player, PlayerState)
bad = unsafeBind curPlayer aBitOdd

exampleGS :: GameState
exampleGS = GameState
    { _playerA = PlayerState { _score = 0 }
    , _playerB = PlayerState { _score = 1 }
    , _curPlayer = PlayerA
    }

exampleUpdate :: (Player, PlayerState)
exampleUpdate = (PlayerB, PlayerState { _score = 2 })

You may verify that both aBitOdd PlayerA and aBitOdd PlayerB are valid lenses by themselves. But bad violates the law saying that set and then get is the identity:

> (set bad exampleUpdate exampleGS ^. bad) == exampleUpdate
False

Now, for the exact invocation you're proposing (unsafeBind curPlayer playerState), there's no problem. So, take inspiration from the implementation of unsafeBind, and write this:

curPlayerState ps gs = case gs ^. curPlayer of
    PlayerA -> playerA ps gs
    PlayerB -> playerB ps gs
-- OR, if playerState is useful on its own in other contexts,
curPlayerState ps gs = playerState (gs ^. curPlayer) ps gs

Upvotes: 5

Related Questions