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

时间:2019-06-05 21:12:38

标签: haskell lens lenses

我有这些类型(还有更多):

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库获得该功能?

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

3 个答案:

答案 0 :(得分:4)

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

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。或者,您是否正在通过将字段名称与PointsData构造函数名称匹配来选择Player的字段?也可以通过Generic来做到这一点,但似乎并不值得。

答案 1 :(得分:4)

游览有点抽象的类。您的PointsDataPlayer类型有特殊关系。有点像Map Player Point,其特殊之处在于,对于每个可能的值Player,总是有一个 对应的Point。从某种意义上说,PointsData就像一个“归一化函数” Player -> Point

如果我们使PointsData的类型上的Points多态,它将适合Representable类型类。我们可以说PointsDataPlayer“代表”。

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


因此,一种可能的解决方案是将PointsData转换为实际的Map,但将实现隐藏在使用Player -> Point函数为所有可能的键进行初始化的智能构造函数的后面(它将与Representable的{​​{3}}方法相对应。)

用户应该不能从地图上删除键。但是我们可以搭载Map的{​​{3}}实例来提供遍历。

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)

答案 2 :(得分:0)

基于answer from Fyodor Soikin duplode 的评论,我最终使用了 lens makeLenses并编写了一个返回适当镜头的函数:

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
-- ..