admin管理员组

文章数量:1182577

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

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

Share Improve this question asked Jan 26 at 23:35 raflrafl 12.3k2 gold badges57 silver badges77 bronze badges 2
  • 3 The general form of this runs into the same semantic issues as the oft-requested lens product, I'm afraid. – Daniel Wagner Commented Jan 27 at 0:09
  • The right way to handle this is to write a currentPlayerState lens by hand. It avoids the issues with the general case when it's limited to that kind of specific purpose. – Carl Commented Jan 27 at 3:05
Add a comment  | 

1 Answer 1

Reset to default 4

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

本文标签: haskellCombining quotLens39 s aquot and quota gt Lens39 s bquot into a quotLens39 s bquotStack Overflow