用于获取或设置由运行时参数确定的记录字段的镜头

A lens for getting or setting a record field determined by a runtime argument

我有这些类型(以及更多):

data Player = PlayerOne | PlayerTwo deriving (Eq, Show, Read, Enum, Bounded)

data Point = Love | Fifteen | Thirty deriving (Eq, Show, Read, Enum, Bounded)

data PointsData =
  PointsData { pointsToPlayerOne :: Point, pointsToPlayerTwo :: Point }
  deriving (Eq, Show, Read)

我正在做 Tennis kata,作为实现的一部分,我想使用一些函数来获取或设置任意玩家的点数,这些点数仅在运行时已知。

形式上,我需要这样的函数:

pointFor :: PointsData -> Player -> Point
pointFor pd PlayerOne = pointsToPlayerOne pd
pointFor pd PlayerTwo = pointsToPlayerTwo pd

pointTo :: PointsData -> Player -> Point -> PointsData
pointTo pd PlayerOne p = pd { pointsToPlayerOne = p }
pointTo pd PlayerTwo p = pd { pointsToPlayerTwo = p }

如上所示,我的问题不是我无法实现这些功能。

但是,在我看来,它们确实 像镜头一样 ,所以我想知道我是否可以通过 lens 库获得该功能?

大多数镜头教程展示了如何获取或设置更大数据结构的特定命名部分。这似乎不太适合我在这里尝试做的事情;相反,我正在尝试获取或设置在运行时确定的子部分。

您可以创建一个函数,在给定 Player 的情况下生成 Lens,如下所示:

playerPoints :: Player -> Lens' PointsData Point
playerPoints PlayerOne = field @"pointsToPlayerOne"
playerPoints PlayerTwo = field @"pointsToPlayerTwo"

(这是使用 field from generic-lens

用法是这样的:

pts :: PointsData

pl1 = pts ^. playerPoints PlayerOne
pl2 = pts ^. playerPoints PlayerTwo

newPts = pts & playerPoints PlayerOne .~ 42

P.S。或者您是否正在寻找通过将字段名称与 Player 构造函数名称匹配来选择 PointsData 的字段?这也可以通过 Generic,但似乎不值得这么麻烦。

对有点抽象的类型类的探索。您的 PointsDataPlayer 类型有着特殊的关系。它有点像 Map Player Point,具有特殊性,对于 Player 每个 可能的值,都有 always 对应Point。在某种程度上,PointsData 就像 "reified function" Player -> Point.

如果我们在 Points 类型上使 PointsData 多态,它将适合 Representable 类型类。我们会说 PointsData 是 "represented" by Player.

Representable 通常用作表格数据的接口,例如在 grids 包中。


所以一个可能的解决方案是将 PointsData 变成实际的 Map,但将实现隐藏在智能构造函数后面,该构造函数采用 Player -> Point 函数为所有可能的初始化它键(它将对应于 Representabletabulate 方法)。

用户不能从地图中删除键。但是我们可以搭载 MapIxed 实例来提供遍历。

import Control.Lens
import Data.Map.Strict -- from "containers"

newtype PointsData = PointsData { getPoints :: Map Player Point } 

init :: (Player -> Point) -> PointsData
init f = PointsData (Data.Map.Strict.fromList ((\p -> (p, f p)) <$> [minBound..maxBound]))


playerPoints :: Player -> Lens' PointsData Point
playerPoints pl = Control.Lens.singular (iso getPoints PointsData . ix pl)

基于 duplode 的评论,我最终使用了 lensmakeLenses 和编写一个 returns 合适镜头的函数:

data PointsData =
  PointsData { _pointsToPlayerOne :: Point, _pointsToPlayerTwo :: Point }
  deriving (Eq, Show, Read)
makeLenses ''PointsData

playerPoint :: Player -> Lens' PointsData Point
playerPoint PlayerOne = pointsToPlayerOne
playerPoint PlayerTwo = pointsToPlayerTwo

它可以像这个更大函数的片段一样使用:

score :: Score -> Player -> Score
-- ..
score (Points pd) winner = Points $ pd & playerPoint winner %~ succ
-- ..