如何处理 Control.Lens.Indexed 中 at 函数的 Maybe 结果,而不需要 Monoid 实例

15

我最近在Hackage上发现了lens包,并尝试在一个小测试项目中使用它,如果我继续努力,这个项目可能会变成一个MUD/MUSH服务器。

下面是我的代码的简化版本,展示了我目前遇到的问题,这些问题涉及用于访问键/值容器(在我的情况下是Data.Map.Strict)的at lens。

{-# 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)到它们的数据对象的映射。

为了使用lenses检索这些对象,我需要以某种方式处理at lens返回的Maybe(如果键不在map中,则为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来泛化大于1的容器,所以我现在想知道是否有更好的方法来处理这个问题,而不需要在我要存储在map中的所有类型可能包含的对象上使用语义上无意义的Monoid实例。

或者也许我完全误解了这个问题,我需要使用一个完全不同的非常庞大的镜头包?


使用Data.Monoid中的FirstLast单子怎么样? - Nathan Howell
3个回答

24

如果您有一个Traversal,并且想要获取第一个元素的Maybe,您可以使用headOf而不是view

viewPlayerLocation :: World -> PlayerId -> Maybe RoomId
viewPlayerLocation world playerId =
  headOf (worldPlayers.at playerId.traverse.playerLocation) world  
headOf 的中缀版本称为 ^?。您还可以使用 toListOf 获取所有元素的列表,以及其他函数,具体取决于您想要做什么。请查看Control.Lens.Fold文档。
查找函数所在模块的快速启发式方法:
- Getter 是对一个值的只读视图 - Lens 是对一个值的读写视图 - Traversal 是对零个或多个值的读写视图 - Fold 是对零个或多个值的只读视图 - Setter 是对零个或多个值(可能是不可数的多个值)的唯写(修改)视图 - Iso 是一个同构——一个可以在两个方向上进行 Lens 的操作。 - 假设您知道何时使用 Indexed 函数,则可以在相应的 Indexed 模块中查找。
考虑您要做什么以及将其放入最通用模块中的最佳方式。 :-) 在这种情况下,您有一个 Traversal,但是您只想查看而不修改,因此您要使用的功能位于 .Fold 中。如果您还保证它是指向一个值,那么它将在 .Getter 中。

谢谢,headOf 看起来提供了我需要的功能。而且,确实有点棘手要找出哪个模块可能包含我需要的函数。你的启发式方法对此会非常有用。 - Matthias Hörmann

1

有一个^?!可以使你不必调用fromMaybe


1

简短回答:lens包不是魔法。

不告诉我错误或默认值,你想要:

viewPlayerLocation :: World -> PlayerId -> RoomId

你知道两件事,那就是

要使用 lenses 检索,需要处理 at lens 返回的 Maybe

以及

只要最终结果是 Monoid 的实例,traverse 就能通过类型检查

有了 Monoid,当查找失败时,您会得到 mempty :: Monoid m => m 作为默认值。

可能会失败的情况: PlayerId 可能不在 _worldPlayers 中,而 _playerLocation 可能不在 _worldRooms 中。

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

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

是的,我应该提到,如果可能的话,我希望它返回一个 Maybe RoomId,并向外传播该 Maybe 值。对于设置器,如果具有匹配 Id 的映射条目不存在,我可以什么都不做,但最好还是进行某种错误报告。 - Matthias Hörmann

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接