在索引容器中使用 return 值缩放 StateT

Zooming StateT with return value in indexed container

我无法找出最干净的方法来缩放像 StateT 这样的效果,returns 一个值到一个索引容器,如矢量或地图。

例如,假设我有一些纸牌游戏的结构:

data Card = Card
    { cardValue :: Int
    } deriving (Show, Eq)
makeFields ''Card

data Player = Player
    { playerCards :: [Card]
    } deriving (Show, Eq)
makeFields ''Player

data Game = Game
    { gamePlayers :: M.Map Int Player
    } deriving (Show, Eq)
makeFields ''Game

data Action = GiveCard Card | DoNothing

以及一个处理玩家在回合中使用 StateT 效果的移动的函数:

playerAction :: (MonadIO m) => StateT Player m Action
playerAction = do
    cards' <- use cards
    case cards' of
        (c:rest) -> GiveCard c <$ (cards .= rest)
        _        -> return DoNothing

我想做的是在游戏中的玩家内部建立索引,并将这个 StateT 应用到那个玩家。看起来像这样的东西:

gameAction :: (MonadIO m) => Int -> StateT Game m ()
gameAction i = do
    Just action <- zoom (players . at i . mapJust) playerAction
    case action of
        GiveCard c -> liftIO $ print c
        DoNothing  -> liftIO $ putStrLn "Doing nothing"

在遍历中添加_Just或将at i替换为ix i导致此编译错误:

    • Could not deduce (Monoid Action) arising from a use of ‘_Just’
      from the context: MonadIO m
        bound by the type signature for:
                   gameAction :: forall (m :: * -> *).
                                 MonadIO m =>
                                 Int -> StateT Game m ()
        at src/MainModule.hs:36:1-52
    • In the second argument of ‘(.)’, namely ‘_Just’
      In the second argument of ‘(.)’, namely ‘at i . _Just’
      In the first argument of ‘zoom’, namely ‘(players . at i . _Just)’
   |
38 |     action <- zoom (players . at i . _Just) playerAction
   |                                      ^^^^^

我可以将 non 与虚拟播放器值一起使用,但如果索引不存在,那么它会在虚拟值上静默运行函数,这不是我想要的:

emptyPlayer :: Player
emptyPlayer = Player []

gameAction :: (MonadIO m) => Int -> StateT Game m ()
gameAction i = do
    action <- zoom (players . at i . non emptyPlayer) playerAction
    case action of
        GiveCard c -> liftIO $ print c
        DoNothing  -> liftIO $ putStrLn "Doing nothing"

我可以用preuse抓取播放器,修改它并设置修改后的值。调用执行此操作的函数非常冗长,因为它必须接受 runMonad 函数以及 getter 和 setter 镜头。

prezoom run get set m = do
    maybeS <- preuse get
    case maybeS of
        Just s -> do
            (r, s') <- lift $ run m s
            set .= s'
            return $ Just r
        Nothing -> return Nothing

gameAction :: (MonadIO m) => Int -> StateT Game m ()
gameAction i = do
    Just action <- prezoom runStateT (players . ix i) (players . ix i) playerAction
    case action of
        GiveCard c -> liftIO $ print c
        DoNothing  -> liftIO $ putStrLn "Doing nothing"

我不太喜欢上述放大索引容器的方法。有没有更简单、更简洁的方法来做到这一点?

你想要它做什么?碰撞? zoom (players . singular (ix i)) 会这样做。

听起来您已经掌握了潜在的语义问题,但为了清楚起见,让我重申一下。

at i 是一个 Lens 到一个 return 是一个 Maybe 的容器,因为该项目可能从容器中丢失(也许索引超出了末尾名单)。将这样的 Lens 与像 _Just 这样的 Prism 组合在一起会将整个事情变成 Traversal:

players . at i . _Just :: Traversal' Game Player

现在,zoom 可以与 Traversal 一起使用,但它需要一个 Monoid 作为有状态操作的 return 值。来自 the docs:

When applied to a Traversal' over multiple values, the actions for each target are executed sequentially and the results are aggregated.

A Traversal 可能 return 零个或多个结果,因此 zoom 将执行 monadic 操作零次或多次,将 mempty 填写为默认值并将多个结果与 mappend 组合。该文档还具有以下针对 zoom 的专用类型签名,它演示了 Monoid 约束:

zoom :: (Monad m, Monoid c) => Traversal' s t -> StateT t m c -> StateT s m c

这就是为什么您的错误消息显示“无法推断 (Monoid Action)”:playerAction returns an Action and zoom needs a Monoid 对于 Action 因为你给了它一个 Traversal.

因此解决方法是从有状态操作中选择 Monoid 到 return。我们知道 Traversal 将命中一个或零个目标 - at i 永远不会 return 多个结果 - 所以我们正在寻找的 Monoid 的正确语义是“第一个结果或失败”。 Monoid 就是 First。 (我们不必担心会丢弃额外的结果,因为不会有任何结果。)

action <- getFirst <$> zoom (players . at i . _Just) (fmap (First . Just) playerAction)
-- here action :: Maybe Action

(我在 phone 上,所以我还没有测试这段代码!)你可以使用 ala.

稍微清理一下