Haskell 镜头:如何优雅地测试列表项

Haskell Lens: How to elegantly test list item

data Game = Game {
 _players :: [Player]
}

data Player = Player {
 _cards :: [Card]
}

data Card = Card {
 _status :: CardStatus
}

data CardStatus = Face | Back

那我就makeLenses上面的数据类型

退出游戏的条件是玩家手中的牌全部退回。 所以在我的 Game StateT monad 中,我访问所有 CardStatus 并测试它们。因为我是 Lens 的新手,所以我想知道写这篇文章的优雅方式是什么。 有点迷失在镜头操作员的森林里。

模块Control.Lens.Fold has many combinators for testing targets of lenses/folds/traversals: has (useful for checking that a prism matches), anyOf, noneOf, allOf...

在你的例子中(假设我们也有 generated the prisms for CardStatus)我们可以做这样的事情:

endGame :: Game -> Bool
endGame = anyOf (players.folded) (allOf (cards.folded.status) (has _Back))

此外,要找到 哪些 玩家有获胜的牌,我们可以使用 filtered:

winners :: Fold Game Player
winners = players.folded.filtered (allOf (cards.folded.status) (has _Back))

这些函数类似于典型的列表函数,但可以直接应用于折叠,因此它们不会把你从镜头世界中拉出来。例如,我们可以继续组合 winners 和另一个 Fold.

所以你需要一个光学元件来告诉你是否所有的牌都是 Back。道德上

allBack :: Getter Player Bool

这显然是某种形式

allBack = cards . _

...不要再进一步了,问问 GHC 这是否有意义:

$ ghc wtmpf-file11136.hs
wtmpf-file11136.hs:26:19: error:
    • Found hole: _ :: (Bool -> f Bool) -> [Card] -> f [Card]

好的,听起来很明智。这个签名看起来很可疑

<a href="http://hackage.haskell.org/package/base-4.11.0.0/docs/Data-Traversable.html#v:traverse" rel="nofollow noreferrer">traverse</a> :: (a -> f b) -> [a] -> f [b]

...这确实是整个 Van Laarhoven 镜头形式主义的原型,并且在构建实际镜头组合链时经常有用。显然,我们还需要关注更多,但首先是:

allBack = cards . traverse . _

给予

wtmpf-file11136.hs:26:19: error:
    • Could not deduce (Applicative f) arising from a use of ‘traverse’
       ...

wtmpf-file11136.hs:26:30: error:
    • Found hole: _ :: (Bool -> f Bool) -> Card -> f Card

好的,这里的问题是 Getter 应该立即关注单个元素。但实际上,我们首先需要遍历多个元素以将 (fold) 压缩为单个 Bool。这意味着我们需要将签名从 Getter 更改为 Fold(这在幕后提供了 Applicative 约束):

allBack :: Fold Player Bool
allBack = cards . traverse . _
    • Found hole: _ :: (Bool -> f Bool) -> Card -> f Card
        ...
    • No instance for (Monoid Bool) arising from a use of ‘allBack’

好的,这是有道理的——我们指定我们想以某种方式减少列表,但是有不止一种方法可以减少列表中的布尔值。在我们的例子中,我们希望它们 都为真 ,即我们需要从 Bool 切换到 All monoid:

import Data.Monoid (All(..))

allBack :: Fold Player All
allBack = cards . traverse . _
wtmpf-file11136.hs:26:30: error:
    • Found hole: _ :: (All -> f All) -> Card -> f Card

好的,看起来不错。现在我们需要指定我们要检查的卡片属性是什么。嗯,关于它的状态:

allBack = cards . traverse . status . _
    • Found hole: _ :: (All -> f All) -> CardStatus -> f CardStatus

在这一点上我们现在需要一个决定,即我们需要投入一个棱镜。有人可能认为这是 _Back 棱镜,但实际上它代表了“无聊”的情况。我们要触发失败的情况是_Face:

allBack = cards . traverse . status . _Face . _
    • Found hole: _ :: (All -> f All) -> () -> f ()

在这里,剩下要做的就是宣布这个_Face是一个失败案例:

allBack = cards . traverse . status . _Face . like (All False)

这是有效的,尽管正如 Willem Van Onsem 评论的那样,纯粹使用镜头在这里并不是最佳选择。将其写成 函数 更明智, 取得了很好的平衡。