15

我最近在 Hackage 上发现了 lens 包,并且现在一直在尝试在一个小型测试项目中使用它,如果我继续努力的话,在很遥远的一天可能会变成 M​​UD/MUSH 服务器。

这是我的代码的最小化版本,说明了我现在使用用于访问键/值容器的 at 镜头面临的问题(在我的情况下为 Data.Map.Strict)

{-# LANGUAGE OverloadedStrings, GeneralizedNewtypeDeriving, TemplateHaskell #-}
module World where
import Control.Applicative ((<$>),(<*>), pure)
import Control.Lens
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as DM
import Data.Maybe
import Data.UUID
import Data.Text (Text)
import qualified Data.Text as T
import System.Random (Random, randomIO)

newtype RoomId = RoomId UUID deriving (Eq, Ord, Show, Read, Random)
newtype PlayerId = PlayerId UUID deriving (Eq, Ord, Show, Read, Random)

data Room =
  Room { _roomId :: RoomId 
       , _roomName :: Text
       , _roomDescription :: Text
       , _roomPlayers :: [PlayerId]
       } deriving (Eq, Ord, Show, Read)

makeLenses ''Room

data Player =
  Player { _playerId :: PlayerId
         , _playerDisplayName :: Text
         , _playerLocation :: RoomId
         } deriving (Eq, Ord, Show, Read)

makeLenses ''Player

data World =
  World { _worldRooms :: Map RoomId Room
        , _worldPlayers :: Map PlayerId Player
        } deriving (Eq, Ord, Show, Read)

makeLenses ''World

mkWorld :: IO World
mkWorld = do
  r1 <- Room <$> randomIO <*> (pure "The Singularity") <*> (pure "You are standing in the only place in the whole world") <*> (pure [])
  p1 <- Player <$> randomIO <*> (pure "testplayer1") <*> (pure $ r1^.roomId)
  let rooms = at (r1^.roomId) ?~ (set roomPlayers [p1^.playerId] r1) $ DM.empty
      players = at (p1^.playerId) ?~ p1 $ DM.empty in do
    return $ World rooms players

viewPlayerLocation :: World -> PlayerId -> RoomId
viewPlayerLocation world playerId=
  view (worldPlayers.at playerId.traverse.playerLocation) world  

由于房间、玩家和类似对象在整个代码中都被引用,因此我将它们存储在我的 World 状态类型中,作为其数据对象的 Id(新类型 UUID)映射。

要检索那些有镜头的人,我需要以某种方式处理 at 镜头返回的 Maybe (如果键不在地图中,这就是 Nothing)。在我的最后一行中,我尝试通过 traverse 执行此操作,只要最终结果是 Monoid 的实例,它就会进行类型检查,但通常情况并非如此。这不是因为 playerLocation 返回一个没有 Monoid 实例的 RoomId。

No instance for (Data.Monoid.Monoid RoomId)
  arising from a use of `traverse'
Possible fix:
  add an instance declaration for (Data.Monoid.Monoid RoomId)
In the first argument of `(.)', namely `traverse'
In the second argument of `(.)', namely `traverse . playerLocation'
In the second argument of `(.)', namely
  `at playerId . traverse . playerLocation'

由于 traverse 只需要 Monoid,因为 traverse 泛化到大小大于 1 的容器,我现在想知道是否有更好的方法来处理这个问题,它不需要我想要的对象中可能包含的所有类型的语义上无意义的 Monoid 实例存储在地图中。

或者我可能完全误解了这里的问题,我需要使用一个完全不同的相当大的镜头包?

4

3 回答 3

24

如果你有 aTraversal并且你想Maybe为第一个元素获取 a ,你可以使用headOf而不是view,即

viewPlayerLocation :: World -> PlayerId -> Maybe RoomId
viewPlayerLocation world playerId =
  headOf (worldPlayers.at playerId.traverse.playerLocation) world  

的中缀版本headOf称为^?. 您还可以使用它toListOf来获取所有元素的列表,以及根据您想要执行的其他功能。请参阅Control.Lens.Fold文档。

在哪个模块中查找您的函数的快速启发式方法:

  • AGetter是只有一个值的只读视图
  • ALens是只有一个值的读写视图
  • ATraversal是零个或多个值的读写视图
  • AFold是零个或多个值的只读视图
  • ASetter是零个或多个值(实际上可能是无数个值)的只写(好吧,只修改)视图
  • AnIso是,嗯,一个同构 - aLens可以在任何一个方向
  • 想必你知道什么时候在使用一个Indexed函数,所以你可以在对应的Indexed模块中查找

想一想您要做什么以及将其放入的最通用模块是什么。:-) 在这种情况下,你有一个Traversal,但你只是想查看,而不是修改,所以你想要的功能在.Fold. 如果您还保证它指的是一个值,那么它将在.Getter.

于 2012-11-18T01:14:45.517 回答
1

简短的回答:镜头包并不神奇。

在不告诉我错误或默认值是什么的情况下,您想要:

viewPlayerLocation :: World -> PlayerId -> RoomId

你知道两件事,那就是

要检索那些有镜头的我需要处理由 at 镜头返回的 Maybe

只要最终结果是 Monoid 的实例,它就会进行类型检查

当查找失败时,Monoid您将获得默认值。mempty :: Monoid m => m

什么会失败:PlayerId不能在_worldPlayers_playerLocation不能在_worldRooms

那么如果查找失败,你的代码应该怎么做呢?这是“不可能”吗?如果是这样,那么使用fromMaybe (error "impossible") :: Maybe a -> a崩溃。

如果查找可能失败,那么是否存在合理的默认值?也许返回Maybe RoomId并让调用者决定?

于 2012-11-17T22:58:01.550 回答
1

^?!它可以让你从调用中解放出来fromMaybe

于 2014-12-03T00:20:21.020 回答